nandi/rustnimpublic Fork 0
8ac32af
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.

Add the differential test runner, and a lowering to measure with it

The runner comes first deliberately. The transpiler this project replaces
reported success while emitting nothing, so the thing to build before any
more lowering is the apparatus that makes that impossible to do unnoticed.

tests/differential.rs compiles each tests/cases/*.rs with rustc and runs it,
transpiles it with rustnim and compiles+runs that with the vendored Nim, and
requires identical stdout *and* identical exit status. Each stage is checked
and reported separately, and three guards exist specifically because of how
the other tool failed -- rustnim exiting 0 with no output file, rustnim
exiting 0 with an empty output file, and an empty corpus are all failures.
All three were verified by breaking the transpiler and watching the runner
go red.

Cases carry `//@` directives; `//@ reject: <substring>` asserts that rustnim
*fails* with a given message, which is how the "never approximate, fail
loudly" rule gets tested rather than merely stated.

With that in place, enough of the lowering to have something to measure:
lower.rs (items, statements, expressions), fmt.rs (format strings),
prelude.nim (Option/Result/panic, and Rust's Display and Debug, which Nim's
`$` matches for neither floats nor sequences nor strings), and a real CLI.

The load-bearing part is expected-type propagation. Rust defaults an
unconstrained integer literal to i32 and Nim to 64-bit int, so the expected
type is threaded into every literal position and every binding is annotated.
A width the lowering gets wrong then surfaces as a Nim compile error -- a
loud failure the runner reports -- instead of as a plausible wrong answer.

21 cases pass: 17 behavioural, 4 rejections. Among them base16ct's
constant-time nibble decoder, the expression whose exact i16 wrapping and
arithmetic shift the other transpiler's float64 universal AST cannot
represent at all.

DESIGN.md items 3 and 4 are now settled and pinned by cases: we model
rustc's debug profile (signed overflow traps on both sides, and Nim Defects
are mapped to Rust's exit code 101), and char/Rune round-trips.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-18T19:04:26-07:00 Browse files
8ac32af parent: 87c9cc8
modified DESIGN.md +76 -15
@@ -2,8 +2,16 @@
22
33 ## Status
44
5-**Early scaffold.** `src/ty.rs` (type mapping) is written. `src/main.rs` is
6-still cargo's default hello-world. Nothing transpiles yet.
5+**Transpiling, and measured.** The differential runner is in place and the
6+whole corpus is green: 21 cases, 17 behavioural and 4 rejections, every one
7+of which compiles under both rustc and Nim and produces identical stdout and
8+exit status. Run it with `cargo test`.
9+
10+Passing today: functions, `impl` methods, structs, `let`/`let mut`, the full
11+integer and float operator set at exact widths, `as` casts, `if`/`while`/
12+`loop`/`for`, `match`, `Vec`/slices/arrays, and `println!`/`format!` with
13+`{}`, `{:?}`, `{:x}`, `{:b}`, positional and inline-named arguments, and
14+zero/space padding.
715
816 ## Why this exists
917
@@ -55,10 +63,22 @@ Planned modules:
5563 | file | role | state |
5664 |---|---|---|
5765 | `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 |
66+| `src/lower.rs` | items, statements, expressions → Nim | written |
67+| `src/fmt.rs` | `println!`/`format!` format-string handling | written |
68+| `src/prelude.nim` | `Option`/`Result`/panic/`Display`/`Debug` runtime | written |
69+| `src/main.rs` | CLI: `rustnim <in.rs> -o <out.nim>` | written |
70+| `tests/differential.rs` | the runner described below | written |
71+
72+### Type propagation is load-bearing
73+
74+Rust infers an unsuffixed integer literal's type from context and falls back
75+to `i32`; Nim falls back to 64-bit `int`. So `lower.rs` threads an *expected
76+type* down through every expression — into `let` annotations, call arguments,
77+`match` patterns, compound assignments and both operands of a binary — and
78+annotates every binding it emits. Without that, `let x: u8 = 200; x + 100`
79+means two different things in the two languages. With it, a width the lowering
80+gets wrong becomes a Nim compile error (a loud failure, reported by the
81+runner) rather than a wrong answer.
6282
6383 ## Mapping decisions made so far
6484
@@ -84,14 +104,28 @@ Planned modules:
84104 `wrapping_*`. `uint8(200) + 100` = `44` in Nim; `200u8.wrapping_add(100)`
85105 = `44` in Rust. So `wrapping_add` on an unsigned type is just `+`.
86106
107+3. **We model rustc's debug profile.** Rust debug builds panic on signed
108+ integer overflow; Nim's default build raises `OverflowDefect` on it. Those
109+ are the matching pair, so the runner invokes `rustc` without `-O` and `nim
110+ c` with its defaults, and `tests/cases/016` pins the behaviour. A Rust
111+ panic exits 101 where a Nim Defect exits 1, so every generated module ends
112+ with a handler that maps one to the other — otherwise the runner's
113+ exit-status comparison would be vacuous. `wrapping_*` is therefore an
114+ explicit operation on both sides: unsigned maps to the bare operator (item
115+ 2), signed is routed through the unsigned view of the same width.
116+4. **`char` round-trips.** Rust `char` → Nim `Rune`, confirmed for ASCII and
117+ non-ASCII scalars in both `{}` and `{:?}`, and across `as u32`
118+ (`tests/cases/014`).
119+
87120 ### Still open
88121
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
94- `std/unicode`. Confirm round-tripping.
122+5. `checked_*` and `saturating_*` are not mapped yet; they are currently
123+ rejected as unsupported methods rather than approximated.
124+6. Generics, traits, enums, closures, iterator adaptors and `?` are all
125+ rejected with a reason. `base16ct` needs enums and `Result`-carrying
126+ functions, so those are next.
127+7. Float formatting matches Rust for ordinary values and for `inf`/`NaN`, but
128+ the exponent-form thresholds have only been checked at `1e21`.
95129
96130 ## Testing: differential, not golden
97131
@@ -105,9 +139,36 @@ diff expected actual
105139 ```
106140
107141 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.
142+stdout *and* exit with the same status.
143+
144+`tests/differential.rs` implements this, and checks each stage separately so a
145+failure says where it went wrong: `rustnim`, `rustc`, `nim`, or `diff`. Three
146+guards exist specifically because of how the other transpiler failed:
147+
148+- `rustnim` exiting 0 while writing **no output file** is a failure.
149+- `rustnim` exiting 0 while writing an **empty output file** is a failure.
150+- An **empty corpus** is a failure, so the runner cannot pass by finding
151+ nothing to do.
152+
153+All three have been verified by deliberately breaking the transpiler and
154+confirming the runner goes red.
155+
156+Cases carry directives in leading `//@` comments:
157+
158+| directive | meaning |
159+|---|---|
160+| `//@ reject: <substring>` | `rustnim` must *fail*, with this in its message |
161+| `//@ skip: <reason>` | not run; reported as skipped |
162+| `//@ args: <argv>` | passed to both binaries |
163+| `//@ stdin: <line>` | fed to both binaries |
164+
165+`reject` cases are how the "fail loudly" rule is tested rather than merely
166+stated: `900`–`904` pin the rejections of `i128`, an unmapped standard-library
167+method, a float→int cast, an unimplemented format spec, and a closure.
168+
169+Run one case with `RUSTNIM_CASE=005 cargo test --test differential --
170+--nocapture`. Nim is found at `.nim-toolchain/bin/nim` in the repository root
171+or any parent, or via `RUSTNIM_NIM`.
111172
112173 ## Toolchain
113174
@@ -2,8 +2,16 @@
2 2
3 ## Status3 ## Status
4 4
5-**Early scaffold.** `src/ty.rs` (type mapping) is written. `src/main.rs` is5+**Transpiling, and measured.** The differential runner is in place and the
6-still cargo's default hello-world. Nothing transpiles yet.6+whole corpus is green: 21 cases, 17 behavioural and 4 rejections, every one
7+of which compiles under both rustc and Nim and produces identical stdout and
8+exit status. Run it with `cargo test`.
9+
10+Passing today: functions, `impl` methods, structs, `let`/`let mut`, the full
11+integer and float operator set at exact widths, `as` casts, `if`/`while`/
12+`loop`/`for`, `match`, `Vec`/slices/arrays, and `println!`/`format!` with
13+`{}`, `{:?}`, `{:x}`, `{:b}`, positional and inline-named arguments, and
14+zero/space padding.
7 15
8 ## Why this exists16 ## Why this exists
9 17
@@ -55,10 +63,22 @@ Planned modules:
55 | file | role | state |63 | file | role | state |
56 |---|---|---|64 |---|---|---|
57 | `src/ty.rs` | Rust type → Nim type, exact widths, explicit rejections | written |65 | `src/ty.rs` | Rust type → Nim type, exact widths, explicit rejections | written |
58-| `src/lower.rs` | items, statements, expressions → Nim | todo |66+| `src/lower.rs` | items, statements, expressions → Nim | written |
59-| `src/fmt.rs` | `println!`/`format!` format-string handling | todo |67+| `src/fmt.rs` | `println!`/`format!` format-string handling | written |
60-| `src/prelude.nim` | `Option`/`Result`/panic runtime, embedded in output | todo |68+| `src/prelude.nim` | `Option`/`Result`/panic/`Display`/`Debug` runtime | written |
61-| `src/main.rs` | CLI: `rustnim <in.rs> -o <out.nim>` | todo |69+| `src/main.rs` | CLI: `rustnim <in.rs> -o <out.nim>` | written |
70+| `tests/differential.rs` | the runner described below | written |
71+
72+### Type propagation is load-bearing
73+
74+Rust infers an unsuffixed integer literal's type from context and falls back
75+to `i32`; Nim falls back to 64-bit `int`. So `lower.rs` threads an *expected
76+type* down through every expression — into `let` annotations, call arguments,
77+`match` patterns, compound assignments and both operands of a binary — and
78+annotates every binding it emits. Without that, `let x: u8 = 200; x + 100`
79+means two different things in the two languages. With it, a width the lowering
80+gets wrong becomes a Nim compile error (a loud failure, reported by the
81+runner) rather than a wrong answer.
62 82
63 ## Mapping decisions made so far83 ## Mapping decisions made so far
64 84
@@ -84,14 +104,28 @@ Planned modules:
84 `wrapping_*`. `uint8(200) + 100` = `44` in Nim; `200u8.wrapping_add(100)`104 `wrapping_*`. `uint8(200) + 100` = `44` in Nim; `200u8.wrapping_add(100)`
85 = `44` in Rust. So `wrapping_add` on an unsigned type is just `+`.105 = `44` in Rust. So `wrapping_add` on an unsigned type is just `+`.
86 106
107+3. **We model rustc's debug profile.** Rust debug builds panic on signed
108+ integer overflow; Nim's default build raises `OverflowDefect` on it. Those
109+ are the matching pair, so the runner invokes `rustc` without `-O` and `nim
110+ c` with its defaults, and `tests/cases/016` pins the behaviour. A Rust
111+ panic exits 101 where a Nim Defect exits 1, so every generated module ends
112+ with a handler that maps one to the other — otherwise the runner's
113+ exit-status comparison would be vacuous. `wrapping_*` is therefore an
114+ explicit operation on both sides: unsigned maps to the bare operator (item
115+ 2), signed is routed through the unsigned view of the same width.
116+4. **`char` round-trips.** Rust `char` → Nim `Rune`, confirmed for ASCII and
117+ non-ASCII scalars in both `{}` and `{:?}`, and across `as u32`
118+ (`tests/cases/014`).
119+
87 ### Still open120 ### Still open
88 121
89-3. Rust debug builds **panic** on signed integer overflow; release builds wrap.122+5. `checked_*` and `saturating_*` are not mapped yet; they are currently
90- Nim raises `OverflowDefect` on signed overflow. Item 2 settles the *unsigned*123+ rejected as unsupported methods rather than approximated.
91- case only. Decide which Rust profile we model, state it in the README, and124+6. Generics, traits, enums, closures, iterator adaptors and `?` are all
92- map `checked_*`/`saturating_*` explicitly.125+ rejected with a reason. `base16ct` needs enums and `Result`-carrying
93-4. `char`: Rust `char` is a Unicode scalar; mapped to `Rune`, which needs126+ functions, so those are next.
94- `std/unicode`. Confirm round-tripping.127+7. Float formatting matches Rust for ordinary values and for `inf`/`NaN`, but
128+ the exponent-form thresholds have only been checked at `1e21`.
95 129
96 ## Testing: differential, not golden130 ## Testing: differential, not golden
97 131
@@ -105,9 +139,36 @@ diff expected actual
105 ```139 ```
106 140
107 A case only counts as passing when both binaries build *and* produce identical141 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 it142+stdout *and* exit with the same status.
109-should come before any more of the lowering, so that progress is measured143+
110-rather than asserted.144+`tests/differential.rs` implements this, and checks each stage separately so a
145+failure says where it went wrong: `rustnim`, `rustc`, `nim`, or `diff`. Three
146+guards exist specifically because of how the other transpiler failed:
147+
148+- `rustnim` exiting 0 while writing **no output file** is a failure.
149+- `rustnim` exiting 0 while writing an **empty output file** is a failure.
150+- An **empty corpus** is a failure, so the runner cannot pass by finding
151+ nothing to do.
152+
153+All three have been verified by deliberately breaking the transpiler and
154+confirming the runner goes red.
155+
156+Cases carry directives in leading `//@` comments:
157+
158+| directive | meaning |
159+|---|---|
160+| `//@ reject: <substring>` | `rustnim` must *fail*, with this in its message |
161+| `//@ skip: <reason>` | not run; reported as skipped |
162+| `//@ args: <argv>` | passed to both binaries |
163+| `//@ stdin: <line>` | fed to both binaries |
164+
165+`reject` cases are how the "fail loudly" rule is tested rather than merely
166+stated: `900`–`904` pin the rejections of `i128`, an unmapped standard-library
167+method, a float→int cast, an unimplemented format spec, and a closure.
168+
169+Run one case with `RUSTNIM_CASE=005 cargo test --test differential --
170+--nocapture`. Nim is found at `.nim-toolchain/bin/nim` in the repository root
171+or any parent, or via `RUSTNIM_NIM`.
111 172
112 ## Toolchain173 ## Toolchain
113 174
added src/fmt.rs +187 -0
new file mode 100644
@@ -0,0 +1,187 @@
1+//! Rust format strings (`println!`, `format!`) -> Nim string expressions.
2+//!
3+//! Only the subset that is reproduced exactly is accepted. An unrecognised
4+//! format spec is an error, never a best-effort guess: `{:>8.3}` silently
5+//! rendered as `{}` would produce plausible-looking wrong output, which is the
6+//! failure mode this project exists to avoid.
7+
8+/// One piece of a parsed format string.
9+#[derive(Debug, PartialEq)]
10+pub enum Piece {
11+ Lit(String),
12+ Arg { r#ref: Ref, spec: Spec },
13+}
14+
15+#[derive(Debug, PartialEq)]
16+pub enum Ref {
17+ /// `{}` — consumes the next positional argument.
18+ Next,
19+ /// `{0}` — an explicit index.
20+ Index(usize),
21+ /// `{name}` — an inline captured identifier.
22+ Named(String),
23+}
24+
25+#[derive(Debug, PartialEq, Default)]
26+pub struct Spec {
27+ pub debug: bool,
28+ /// `x`, `X`, `b`, `o` — `None` for Display/Debug.
29+ pub radix: Option<char>,
30+ pub width: usize,
31+ pub zero: bool,
32+}
33+
34+pub fn parse(s: &str) -> Result<Vec<Piece>, String> {
35+ let mut out = Vec::new();
36+ let mut lit = String::new();
37+ let mut it = s.chars().peekable();
38+
39+ while let Some(c) = it.next() {
40+ match c {
41+ '{' if it.peek() == Some(&'{') => {
42+ it.next();
43+ lit.push('{');
44+ }
45+ '}' if it.peek() == Some(&'}') => {
46+ it.next();
47+ lit.push('}');
48+ }
49+ '}' => return Err("unmatched `}` in format string".into()),
50+ '{' => {
51+ if !lit.is_empty() {
52+ out.push(Piece::Lit(std::mem::take(&mut lit)));
53+ }
54+ let mut body = String::new();
55+ let mut closed = false;
56+ for c in it.by_ref() {
57+ if c == '}' {
58+ closed = true;
59+ break;
60+ }
61+ body.push(c);
62+ }
63+ if !closed {
64+ return Err("unmatched `{` in format string".into());
65+ }
66+ let (name, spec) = match body.split_once(':') {
67+ Some((n, s)) => (n, parse_spec(s)?),
68+ None => (body.as_str(), Spec::default()),
69+ };
70+ let r#ref = if name.is_empty() {
71+ Ref::Next
72+ } else if let Ok(i) = name.parse::<usize>() {
73+ Ref::Index(i)
74+ } else if name.chars().all(|c| c.is_alphanumeric() || c == '_') {
75+ Ref::Named(name.to_string())
76+ } else {
77+ return Err(format!("unsupported format argument `{name}`"));
78+ };
79+ out.push(Piece::Arg { r#ref, spec });
80+ }
81+ _ => lit.push(c),
82+ }
83+ }
84+ if !lit.is_empty() {
85+ out.push(Piece::Lit(lit));
86+ }
87+ Ok(out)
88+}
89+
90+fn parse_spec(s: &str) -> Result<Spec, String> {
91+ let mut spec = Spec::default();
92+ let mut rest = s;
93+
94+ match rest.chars().last() {
95+ Some('?') => {
96+ spec.debug = true;
97+ rest = &rest[..rest.len() - 1];
98+ }
99+ Some(r @ ('x' | 'X' | 'b' | 'o')) => {
100+ spec.radix = Some(r);
101+ rest = &rest[..rest.len() - 1];
102+ }
103+ _ => {}
104+ }
105+
106+ if let Some(r) = rest.strip_prefix('0') {
107+ spec.zero = true;
108+ rest = r;
109+ }
110+ if !rest.is_empty() {
111+ spec.width = rest
112+ .parse::<usize>()
113+ .map_err(|_| format!("unsupported format spec `:{s}` (precision, alignment and fill are not implemented)"))?;
114+ }
115+ Ok(spec)
116+}
117+
118+/// Build the Nim expression for one argument, given its already-lowered value.
119+pub fn render_arg(value: &str, spec: &Spec) -> String {
120+ let core = match spec.radix {
121+ Some(r) => format!(
122+ "rsRadix({}, {}, {})",
123+ value,
124+ match r {
125+ 'x' | 'X' => 16,
126+ 'b' => 2,
127+ _ => 8,
128+ },
129+ r == 'X'
130+ ),
131+ None if spec.debug => format!("rsDebug({value})"),
132+ None => format!("rsDisplay({value})"),
133+ };
134+ if spec.width > 0 {
135+ format!("rsPad({}, {}, {})", core, spec.width, spec.zero)
136+ } else {
137+ core
138+ }
139+}
140+
141+/// Nim string literal with Rust's escaping rules applied to the bytes we emit.
142+pub fn nim_str(s: &str) -> String {
143+ let mut out = String::from("\"");
144+ for c in s.chars() {
145+ match c {
146+ '"' => out.push_str("\\\""),
147+ '\\' => out.push_str("\\\\"),
148+ '\n' => out.push_str("\\n"),
149+ '\t' => out.push_str("\\t"),
150+ '\r' => out.push_str("\\r"),
151+ c => out.push(c),
152+ }
153+ }
154+ out.push('"');
155+ out
156+}
157+
158+#[cfg(test)]
159+mod tests {
160+ use super::*;
161+
162+ #[test]
163+ fn braces_and_specs() {
164+ assert_eq!(parse("a{{b").unwrap(), vec![Piece::Lit("a{b".into())]);
165+ assert_eq!(
166+ parse("{:02x}").unwrap(),
167+ vec![Piece::Arg {
168+ r#ref: Ref::Next,
169+ spec: Spec { debug: false, radix: Some('x'), width: 2, zero: true }
170+ }]
171+ );
172+ assert_eq!(
173+ parse("{n:?}").unwrap(),
174+ vec![Piece::Arg {
175+ r#ref: Ref::Named("n".into()),
176+ spec: Spec { debug: true, ..Spec::default() }
177+ }]
178+ );
179+ }
180+
181+ #[test]
182+ fn unsupported_specs_are_rejected_not_guessed() {
183+ assert!(parse("{:>8}").is_err());
184+ assert!(parse("{:.3}").is_err());
185+ assert!(parse("{").is_err());
186+ }
187+}
new file mode 100644
@@ -0,0 +1,187 @@
1+//! Rust format strings (`println!`, `format!`) -> Nim string expressions.
2+//!
3+//! Only the subset that is reproduced exactly is accepted. An unrecognised
4+//! format spec is an error, never a best-effort guess: `{:>8.3}` silently
5+//! rendered as `{}` would produce plausible-looking wrong output, which is the
6+//! failure mode this project exists to avoid.
7+
8+/// One piece of a parsed format string.
9+#[derive(Debug, PartialEq)]
10+pub enum Piece {
11+ Lit(String),
12+ Arg { r#ref: Ref, spec: Spec },
13+}
14+
15+#[derive(Debug, PartialEq)]
16+pub enum Ref {
17+ /// `{}` — consumes the next positional argument.
18+ Next,
19+ /// `{0}` — an explicit index.
20+ Index(usize),
21+ /// `{name}` — an inline captured identifier.
22+ Named(String),
23+}
24+
25+#[derive(Debug, PartialEq, Default)]
26+pub struct Spec {
27+ pub debug: bool,
28+ /// `x`, `X`, `b`, `o` — `None` for Display/Debug.
29+ pub radix: Option<char>,
30+ pub width: usize,
31+ pub zero: bool,
32+}
33+
34+pub fn parse(s: &str) -> Result<Vec<Piece>, String> {
35+ let mut out = Vec::new();
36+ let mut lit = String::new();
37+ let mut it = s.chars().peekable();
38+
39+ while let Some(c) = it.next() {
40+ match c {
41+ '{' if it.peek() == Some(&'{') => {
42+ it.next();
43+ lit.push('{');
44+ }
45+ '}' if it.peek() == Some(&'}') => {
46+ it.next();
47+ lit.push('}');
48+ }
49+ '}' => return Err("unmatched `}` in format string".into()),
50+ '{' => {
51+ if !lit.is_empty() {
52+ out.push(Piece::Lit(std::mem::take(&mut lit)));
53+ }
54+ let mut body = String::new();
55+ let mut closed = false;
56+ for c in it.by_ref() {
57+ if c == '}' {
58+ closed = true;
59+ break;
60+ }
61+ body.push(c);
62+ }
63+ if !closed {
64+ return Err("unmatched `{` in format string".into());
65+ }
66+ let (name, spec) = match body.split_once(':') {
67+ Some((n, s)) => (n, parse_spec(s)?),
68+ None => (body.as_str(), Spec::default()),
69+ };
70+ let r#ref = if name.is_empty() {
71+ Ref::Next
72+ } else if let Ok(i) = name.parse::<usize>() {
73+ Ref::Index(i)
74+ } else if name.chars().all(|c| c.is_alphanumeric() || c == '_') {
75+ Ref::Named(name.to_string())
76+ } else {
77+ return Err(format!("unsupported format argument `{name}`"));
78+ };
79+ out.push(Piece::Arg { r#ref, spec });
80+ }
81+ _ => lit.push(c),
82+ }
83+ }
84+ if !lit.is_empty() {
85+ out.push(Piece::Lit(lit));
86+ }
87+ Ok(out)
88+}
89+
90+fn parse_spec(s: &str) -> Result<Spec, String> {
91+ let mut spec = Spec::default();
92+ let mut rest = s;
93+
94+ match rest.chars().last() {
95+ Some('?') => {
96+ spec.debug = true;
97+ rest = &rest[..rest.len() - 1];
98+ }
99+ Some(r @ ('x' | 'X' | 'b' | 'o')) => {
100+ spec.radix = Some(r);
101+ rest = &rest[..rest.len() - 1];
102+ }
103+ _ => {}
104+ }
105+
106+ if let Some(r) = rest.strip_prefix('0') {
107+ spec.zero = true;
108+ rest = r;
109+ }
110+ if !rest.is_empty() {
111+ spec.width = rest
112+ .parse::<usize>()
113+ .map_err(|_| format!("unsupported format spec `:{s}` (precision, alignment and fill are not implemented)"))?;
114+ }
115+ Ok(spec)
116+}
117+
118+/// Build the Nim expression for one argument, given its already-lowered value.
119+pub fn render_arg(value: &str, spec: &Spec) -> String {
120+ let core = match spec.radix {
121+ Some(r) => format!(
122+ "rsRadix({}, {}, {})",
123+ value,
124+ match r {
125+ 'x' | 'X' => 16,
126+ 'b' => 2,
127+ _ => 8,
128+ },
129+ r == 'X'
130+ ),
131+ None if spec.debug => format!("rsDebug({value})"),
132+ None => format!("rsDisplay({value})"),
133+ };
134+ if spec.width > 0 {
135+ format!("rsPad({}, {}, {})", core, spec.width, spec.zero)
136+ } else {
137+ core
138+ }
139+}
140+
141+/// Nim string literal with Rust's escaping rules applied to the bytes we emit.
142+pub fn nim_str(s: &str) -> String {
143+ let mut out = String::from("\"");
144+ for c in s.chars() {
145+ match c {
146+ '"' => out.push_str("\\\""),
147+ '\\' => out.push_str("\\\\"),
148+ '\n' => out.push_str("\\n"),
149+ '\t' => out.push_str("\\t"),
150+ '\r' => out.push_str("\\r"),
151+ c => out.push(c),
152+ }
153+ }
154+ out.push('"');
155+ out
156+}
157+
158+#[cfg(test)]
159+mod tests {
160+ use super::*;
161+
162+ #[test]
163+ fn braces_and_specs() {
164+ assert_eq!(parse("a{{b").unwrap(), vec![Piece::Lit("a{b".into())]);
165+ assert_eq!(
166+ parse("{:02x}").unwrap(),
167+ vec![Piece::Arg {
168+ r#ref: Ref::Next,
169+ spec: Spec { debug: false, radix: Some('x'), width: 2, zero: true }
170+ }]
171+ );
172+ assert_eq!(
173+ parse("{n:?}").unwrap(),
174+ vec![Piece::Arg {
175+ r#ref: Ref::Named("n".into()),
176+ spec: Spec { debug: true, ..Spec::default() }
177+ }]
178+ );
179+ }
180+
181+ #[test]
182+ fn unsupported_specs_are_rejected_not_guessed() {
183+ assert!(parse("{:>8}").is_err());
184+ assert!(parse("{:.3}").is_err());
185+ assert!(parse("{").is_err());
186+ }
187+}
added src/lower.rs +1610 -0
new file mode 100644
@@ -0,0 +1,1610 @@
1+//! Rust AST -> Nim source.
2+//!
3+//! The governing rule is in DESIGN.md and it shapes every function here:
4+//! anything whose Rust semantics cannot be reproduced exactly in Nim returns
5+//! `Err` with a reason. Nothing is emitted on a guess. Where a construct maps
6+//! one-to-one (signed `shr`, unsigned wrapping, truncating `div`/`mod`) the
7+//! mapping is direct and there is a comment saying why that is safe.
8+
9+use crate::fmt;
10+use crate::ty::{self, Nim};
11+use std::collections::HashMap;
12+use syn::{
13+ BinOp, Expr, FnArg, Item, Lit, Local, Pat, ReturnType, Stmt, UnOp,
14+};
15+
16+// --------------------------------------------------------------- vocabulary
17+
18+/// Nim keywords. Rust code may legally use any of these as an identifier.
19+const NIM_KEYWORDS: &[&str] = &[
20+ "addr", "and", "as", "asm", "bind", "block", "break", "case", "cast",
21+ "concept", "const", "continue", "converter", "defer", "discard", "distinct",
22+ "div", "do", "elif", "else", "end", "enum", "except", "export", "finally",
23+ "for", "from", "func", "if", "import", "in", "include", "interface", "is",
24+ "isnot", "iterator", "let", "macro", "method", "mixin", "mod", "nil", "not",
25+ "notin", "object", "of", "or", "out", "proc", "ptr", "raise", "ref",
26+ "return", "shl", "shr", "static", "template", "try", "tuple", "type",
27+ "using", "var", "when", "while", "xor", "result", "echo",
28+];
29+
30+fn ident(name: &str) -> String {
31+ if NIM_KEYWORDS.contains(&name) {
32+ format!("{name}_r")
33+ } else {
34+ name.to_string()
35+ }
36+}
37+
38+/// A lowered expression: its Nim text, and its type where we know it.
39+///
40+/// The type is not decoration. Nim needs it to pick `div` over `/`, to size a
41+/// `cast`, and to annotate every binding so that Nim's own type checker
42+/// catches a mistake in this file rather than letting it through as output
43+/// that runs and is wrong.
44+#[derive(Clone, Debug)]
45+struct Val {
46+ code: String,
47+ ty: Option<Nim>,
48+}
49+
50+impl Val {
51+ fn new(code: impl Into<String>, ty: Option<Nim>) -> Self {
52+ Val { code: code.into(), ty }
53+ }
54+ fn untyped(code: impl Into<String>) -> Self {
55+ Val { code: code.into(), ty: None }
56+ }
57+}
58+
59+struct Sig {
60+ params: Vec<Nim>,
61+ ret: Nim,
62+}
63+
64+pub struct Lowerer {
65+ out: String,
66+ indent: usize,
67+ scopes: Vec<HashMap<String, Nim>>,
68+ fns: HashMap<String, Sig>,
69+ /// struct name -> (field, type)
70+ structs: HashMap<String, Vec<(String, Nim)>>,
71+ /// Return type of the proc being lowered, so `return e` and a trailing
72+ /// expression can type their literals the way Rust's inference would.
73+ ret: Option<Nim>,
74+ /// `(name, type)` that the arms of the `if`/`match` being lowered as a
75+ /// statement must assign their value to.
76+ target: Option<(String, Option<Nim>)>,
77+ tmp: usize,
78+}
79+
80+impl Lowerer {
81+ pub fn new() -> Self {
82+ Lowerer {
83+ out: String::new(),
84+ indent: 0,
85+ scopes: vec![HashMap::new()],
86+ fns: HashMap::new(),
87+ structs: HashMap::new(),
88+ ret: None,
89+ target: None,
90+ tmp: 0,
91+ }
92+ }
93+
94+ // ------------------------------------------------------------ emission
95+
96+ fn line(&mut self, s: &str) {
97+ for _ in 0..self.indent {
98+ self.out.push_str(" ");
99+ }
100+ self.out.push_str(s);
101+ self.out.push('\n');
102+ }
103+
104+ fn blank(&mut self) {
105+ self.out.push('\n');
106+ }
107+
108+ fn fresh(&mut self, hint: &str) -> String {
109+ self.tmp += 1;
110+ format!("rsTmp{}{}", hint, self.tmp)
111+ }
112+
113+ // --------------------------------------------------------------- scope
114+
115+ fn push_scope(&mut self) {
116+ self.scopes.push(HashMap::new());
117+ }
118+ fn pop_scope(&mut self) {
119+ self.scopes.pop();
120+ }
121+ fn bind(&mut self, name: &str, t: Nim) {
122+ self.scopes.last_mut().unwrap().insert(name.to_string(), t);
123+ }
124+ fn lookup(&self, name: &str) -> Option<Nim> {
125+ self.scopes.iter().rev().find_map(|s| s.get(name).cloned())
126+ }
127+
128+ // ---------------------------------------------------------------- file
129+
130+ pub fn lower_file(&mut self, file: &syn::File) -> Result<String, String> {
131+ self.out.push_str(include_str!("prelude.nim"));
132+ self.blank();
133+
134+ // Pass 1: signatures and struct shapes, so that a call can be typed
135+ // regardless of declaration order (Rust has no forward declarations).
136+ for item in &file.items {
137+ self.collect(item)?;
138+ }
139+ // Pass 2: bodies.
140+ for item in &file.items {
141+ self.item(item)?;
142+ }
143+
144+ if self.fns.contains_key("main") {
145+ self.blank();
146+ self.line("when isMainModule:");
147+ self.indent += 1;
148+ self.line("try:");
149+ self.line(" main()");
150+ // Rust's panic exits 101 with a message on stderr. Nim's Defects
151+ // exit 1. Mapping them here is what keeps the differential runner's
152+ // exit-status comparison meaningful for panicking programs.
153+ self.line("except RustPanic as e:");
154+ self.line(" stderr.writeLine(\"thread 'main' panicked: \" & e.msg)");
155+ self.line(" quit(101)");
156+ self.line("except Defect as e:");
157+ self.line(" stderr.writeLine(\"thread 'main' panicked: \" & e.msg)");
158+ self.line(" quit(101)");
159+ self.indent -= 1;
160+ }
161+ Ok(std::mem::take(&mut self.out))
162+ }
163+
164+ fn collect(&mut self, item: &Item) -> Result<(), String> {
165+ match item {
166+ Item::Fn(f) => {
167+ let (params, ret) = self.signature(&f.sig)?;
168+ self.fns.insert(f.sig.ident.to_string(), Sig { params, ret });
169+ }
170+ Item::Struct(s) => {
171+ let mut fields = Vec::new();
172+ for (i, f) in s.fields.iter().enumerate() {
173+ let name = match &f.ident {
174+ Some(id) => id.to_string(),
175+ None => format!("f{i}"), // tuple struct
176+ };
177+ fields.push((name, ty::map(&f.ty)?.owned()));
178+ }
179+ self.structs.insert(s.ident.to_string(), fields);
180+ }
181+ Item::Impl(im) => {
182+ let self_ty = ty::map(&im.self_ty)?;
183+ for it in &im.items {
184+ if let syn::ImplItem::Fn(m) = it {
185+ let (mut params, ret) = self.signature(&m.sig)?;
186+ if takes_self(&m.sig) {
187+ params.insert(0, self_ty.clone());
188+ }
189+ self.fns.insert(m.sig.ident.to_string(), Sig { params, ret });
190+ }
191+ }
192+ }
193+ _ => {}
194+ }
195+ Ok(())
196+ }
197+
198+ fn signature(&self, sig: &syn::Signature) -> Result<(Vec<Nim>, Nim), String> {
199+ if sig.asyncness.is_some() {
200+ return Err(format!("`async fn {}`: Nim has no equivalent", sig.ident));
201+ }
202+ if !sig.generics.params.is_empty() {
203+ return Err(format!(
204+ "`fn {}` is generic: generics are not implemented yet",
205+ sig.ident
206+ ));
207+ }
208+ let mut params = Vec::new();
209+ for a in &sig.inputs {
210+ if let FnArg::Typed(t) = a {
211+ params.push(ty::map(&t.ty)?);
212+ }
213+ }
214+ let ret = match &sig.output {
215+ ReturnType::Default => Nim::Unit,
216+ ReturnType::Type(_, t) => ty::map(t)?.owned(),
217+ };
218+ Ok((params, ret))
219+ }
220+
221+ // --------------------------------------------------------------- items
222+
223+ fn item(&mut self, item: &Item) -> Result<(), String> {
224+ match item {
225+ Item::Fn(f) => self.func(&f.sig, &f.block, None),
226+ Item::Struct(s) => {
227+ let name = s.ident.to_string();
228+ let fields = self.structs[&name].clone();
229+ self.line(&format!("type {}* = object", ident(&name)));
230+ self.indent += 1;
231+ if fields.is_empty() {
232+ self.line("discard");
233+ }
234+ for (fname, fty) in &fields {
235+ self.line(&format!("{}*: {}", ident(fname), fty.render()));
236+ }
237+ self.indent -= 1;
238+ self.blank();
239+ Ok(())
240+ }
241+ Item::Const(c) => {
242+ let t = ty::map(&c.ty)?.owned();
243+ let v = self.expr(&c.expr)?;
244+ self.bind(&c.ident.to_string(), t.clone());
245+ let line = format!("const {}*: {} = {}", ident(&c.ident.to_string()), t.render(), v.code);
246+ self.line(&line);
247+ self.blank();
248+ Ok(())
249+ }
250+ Item::Impl(im) => {
251+ let self_ty = ty::map(&im.self_ty)?;
252+ if im.trait_.is_some() {
253+ return Err(format!(
254+ "`impl Trait for {}`: trait impls are not implemented yet",
255+ self_ty.render()
256+ ));
257+ }
258+ for it in &im.items {
259+ match it {
260+ syn::ImplItem::Fn(m) => {
261+ let recv = if takes_self(&m.sig) { Some(self_ty.clone()) } else { None };
262+ self.func(&m.sig, &m.block, recv)?;
263+ }
264+ _ => return Err("only `fn` items are supported inside `impl`".into()),
265+ }
266+ }
267+ Ok(())
268+ }
269+ Item::Use(_) => Ok(()), // `use` has no Nim analogue in a single module
270+ Item::Mod(m) if m.content.is_none() => {
271+ Err(format!("`mod {};` (external file) is not implemented yet", m.ident))
272+ }
273+ other => Err(format!("unsupported item: {}", item_kind(other))),
274+ }
275+ }
276+
277+ fn func(
278+ &mut self,
279+ sig: &syn::Signature,
280+ body: &syn::Block,
281+ recv: Option<Nim>,
282+ ) -> Result<(), String> {
283+ let name = sig.ident.to_string();
284+ let (ptys, ret) = self.signature(sig)?;
285+
286+ self.push_scope();
287+ let mut rendered: Vec<String> = Vec::new();
288+
289+ if let Some(self_ty) = recv {
290+ // `&mut self` and `mut self` both mean the body may mutate the
291+ // receiver; only the former is observable by the caller, and a Nim
292+ // `var` parameter is the faithful spelling of that.
293+ let mutable = matches!(
294+ sig.inputs.first(),
295+ Some(FnArg::Receiver(r))
296+ if matches!(&r.kind, syn::ReceiverKind::Reference(_, _, m) if m.is_some())
297+ );
298+ let t = if mutable { Nim::Var(Box::new(self_ty.clone())) } else { self_ty.clone() };
299+ rendered.push(format!("self: {}", t.render()));
300+ self.bind("self", self_ty);
301+ }
302+
303+ let typed: Vec<&syn::PatType> = sig
304+ .inputs
305+ .iter()
306+ .filter_map(|a| match a {
307+ FnArg::Typed(t) => Some(t),
308+ _ => None,
309+ })
310+ .collect();
311+ for (p, t) in typed.iter().zip(ptys.iter()) {
312+ let pname = match &*p.pat {
313+ Pat::Ident(i) => i.ident.to_string(),
314+ _ => return Err("only plain identifier parameters are supported".into()),
315+ };
316+ rendered.push(format!("{}: {}", ident(&pname), t.render()));
317+ // Inside the body a `var T` parameter is used exactly like a `T`.
318+ self.bind(&pname, t.clone().owned());
319+ }
320+
321+ let head = if ret == Nim::Unit {
322+ format!("proc {}*({}) =", ident(&name), rendered.join(", "))
323+ } else {
324+ format!("proc {}*({}): {} =", ident(&name), rendered.join(", "), ret.render())
325+ };
326+ self.line(&head);
327+ self.indent += 1;
328+ let outer_ret = self.ret.replace(ret.clone());
329+
330+ // A Rust fn's trailing expression is its return value. Naming Nim's
331+ // implicit `result` as the target makes that true whether the tail is
332+ // a plain expression or an `if`/`match` with statement arms.
333+ let outer_target = if ret == Nim::Unit {
334+ self.target.take()
335+ } else {
336+ self.target.replace(("result".to_string(), Some(ret.clone())))
337+ };
338+ let before = self.out.len();
339+ let tail = self.block_body_at(body, Some(&ret))?;
340+ self.target = outer_target;
341+ match tail {
342+ Some(v) if ret != Nim::Unit => {
343+ let code = v.code.clone();
344+ self.line(&format!("result = {code}"));
345+ }
346+ Some(v) => {
347+ // A trailing expression in a `()`-returning fn is evaluated for
348+ // its effect; Nim requires an explicit discard.
349+ let needs_discard = v.ty.as_ref().is_none_or(|t| *t != Nim::Unit);
350+ if needs_discard && !v.code.is_empty() {
351+ let code = v.code.clone();
352+ self.line(&format!("discard {code}"));
353+ }
354+ }
355+ None => {}
356+ }
357+ if self.out.len() == before {
358+ self.line("discard");
359+ }
360+
361+ self.indent -= 1;
362+ self.ret = outer_ret;
363+ self.pop_scope();
364+ self.blank();
365+ Ok(())
366+ }
367+
368+ // ---------------------------------------------------------- statements
369+
370+ /// Lower a block's statements. Returns the block's trailing expression,
371+ /// if it has one, *without* emitting it — the caller decides whether that
372+ /// value is a return value, a binding, or discarded.
373+ fn block_body(&mut self, b: &syn::Block) -> Result<Option<Val>, String> {
374+ self.block_body_at(b, None)
375+ }
376+
377+ fn block_body_at(
378+ &mut self,
379+ b: &syn::Block,
380+ expect: Option<&Nim>,
381+ ) -> Result<Option<Val>, String> {
382+ // An assignment target belongs to *this* block's trailing expression
383+ // only. A non-final `if` is a statement and must not assign anything.
384+ let target = self.target.take();
385+ let n = b.stmts.len();
386+ let mut tail = None;
387+ for (i, st) in b.stmts.iter().enumerate() {
388+ let last = i + 1 == n;
389+ match st {
390+ Stmt::Expr(e, None) if last && expressible(e) => {
391+ tail = Some(self.expr_at(e, expect)?)
392+ }
393+ Stmt::Expr(e, None) if last => {
394+ // A trailing `if`/`match` with statement arms, or a loop.
395+ // Lower it as statements; if this block's value is wanted,
396+ // each arm assigns it.
397+ match &target {
398+ Some((t, ty)) => {
399+ let (t, ty) = (t.clone(), ty.clone());
400+ self.assign_from(e, &t, ty.as_ref())?;
401+ }
402+ None => self.stmt(st)?,
403+ }
404+ }
405+ _ => self.stmt(st)?,
406+ }
407+ }
408+ self.target = target;
409+ Ok(tail)
410+ }
411+
412+ /// Lower a block in statement position (loop bodies, `if` arms).
413+ fn nested_block(&mut self, b: &syn::Block) -> Result<(), String> {
414+ self.push_scope();
415+ self.indent += 1;
416+ let before = self.out.len();
417+ let want = self.target.clone().and_then(|(_, t)| t);
418+ let tail = self.block_body_at(b, want.as_ref())?;
419+ self.emit_tail(tail);
420+ if self.out.len() == before {
421+ self.line("discard");
422+ }
423+ self.indent -= 1;
424+ self.pop_scope();
425+ Ok(())
426+ }
427+
428+ fn stmt(&mut self, s: &Stmt) -> Result<(), String> {
429+ match s {
430+ Stmt::Local(l) => self.local(l),
431+ Stmt::Expr(e, _) => {
432+ let v = self.expr_stmt(e)?;
433+ if let Some(v) = v {
434+ // A bare expression with a value must be discarded in Nim.
435+ let needs = v.ty.as_ref().is_none_or(|t| *t != Nim::Unit);
436+ let code = v.code.clone();
437+ if needs {
438+ self.line(&format!("discard {code}"));
439+ } else if !code.is_empty() {
440+ self.line(&code);
441+ }
442+ }
443+ Ok(())
444+ }
445+ Stmt::Item(i) => self.item(i),
446+ Stmt::Macro(m) => {
447+ let line = self.macro_call(&m.mac)?;
448+ self.line(&line);
449+ Ok(())
450+ }
451+ }
452+ }
453+
454+ fn local(&mut self, l: &Local) -> Result<(), String> {
455+ let (name, mutable, ann): (String, bool, Option<Nim>) = match &l.pat {
456+ Pat::Ident(i) => (i.ident.to_string(), i.mutability.is_some(), None),
457+ Pat::Type(t) => match &*t.pat {
458+ Pat::Ident(i) => (i.ident.to_string(), i.mutability.is_some(), Some(ty::map(&t.ty)?)),
459+ _ => return Err("only `let <ident>` bindings are supported".into()),
460+ },
461+ Pat::Wild(_) => ("_".into(), false, None),
462+ _ => return Err("destructuring `let` is not implemented yet".into()),
463+ };
464+
465+ let Some(init) = &l.init else {
466+ // `let x: T;` — Nim's `var x: T` zero-initialises, which Rust does
467+ // not. Rust's own rules make reading it before assignment illegal,
468+ // so the two agree on every program rustc accepts.
469+ let t = ann.ok_or("`let` without an initialiser needs a type annotation")?;
470+ let t = t.owned();
471+ self.line(&format!("var {}: {}", ident(&name), t.render()));
472+ self.bind(&name, t);
473+ return Ok(());
474+ };
475+ if init.diverge.is_some() {
476+ return Err("`let ... else` is not implemented yet".into());
477+ }
478+
479+ if !expressible(&init.expr) && name != "_" {
480+ // The initialiser is an `if`/`match` whose arms are statements.
481+ // Declare first, then let each arm assign into the binding.
482+ let t = ann
483+ .clone()
484+ .ok_or_else(|| {
485+ format!(
486+ "`let {name} = match/if ...` needs a type annotation: \
487+ its arms are statements, so the binding must be \
488+ declared before they run"
489+ )
490+ })?
491+ .owned();
492+ self.line(&format!("var {}: {}", ident(&name), t.render()));
493+ self.bind(&name, t.clone());
494+ let target = ident(&name);
495+ return self.assign_from(&init.expr, &target, Some(&t));
496+ }
497+
498+ let v = self.expr_at(&init.expr, ann.as_ref())?;
499+ let t = match (ann, &v.ty) {
500+ (Some(a), _) => a.owned(),
501+ (None, Some(t)) => t.clone().owned(),
502+ (None, None) => {
503+ return Err(format!(
504+ "cannot infer the type of `let {name}`; annotate it — \
505+ guessing here would change integer width, and with it the \
506+ meaning of any arithmetic on `{name}`"
507+ ))
508+ }
509+ };
510+
511+ if name == "_" {
512+ let code = v.code.clone();
513+ self.line(&format!("discard {code}"));
514+ return Ok(());
515+ }
516+ // Rust's immutable `let` is Nim's `let`; `let mut` is `var`. Shadowing
517+ // works in both, so a re-`let` of the same name needs no rename.
518+ let kw = if mutable { "var" } else { "let" };
519+ let line = format!("{} {}: {} = {}", kw, ident(&name), t.render(), v.code);
520+ self.line(&line);
521+ self.bind(&name, t);
522+ Ok(())
523+ }
524+
525+ /// Expressions that are statements in Rust and statements in Nim too
526+ /// (control flow). Returns `None` when it emitted lines itself.
527+ fn expr_stmt(&mut self, e: &Expr) -> Result<Option<Val>, String> {
528+ match e {
529+ Expr::If(_) => {
530+ self.if_stmt(e)?;
531+ Ok(None)
532+ }
533+ Expr::While(w) => {
534+ if w.label.is_some() {
535+ return Err("loop labels are not implemented yet".into());
536+ }
537+ let c = self.expr(&w.cond)?;
538+ self.line(&format!("while {}:", c.code));
539+ let saved = self.target.take();
540+ self.nested_block(&w.body)?;
541+ self.target = saved;
542+ Ok(None)
543+ }
544+ Expr::Loop(l) => {
545+ if l.label.is_some() {
546+ return Err("loop labels are not implemented yet".into());
547+ }
548+ self.line("while true:");
549+ let saved = self.target.take();
550+ self.nested_block(&l.body)?;
551+ self.target = saved;
552+ Ok(None)
553+ }
554+ Expr::ForLoop(f) => {
555+ self.for_loop(f)?;
556+ Ok(None)
557+ }
558+ Expr::Block(b) => {
559+ if b.label.is_some() {
560+ return Err("block labels are not implemented yet".into());
561+ }
562+ self.line("block:");
563+ self.nested_block(&b.block)?;
564+ Ok(None)
565+ }
566+ Expr::Match(_) => {
567+ self.match_stmt(e)?;
568+ Ok(None)
569+ }
570+ Expr::Return(r) => {
571+ match &r.expr {
572+ Some(e) => {
573+ let want = self.ret.clone();
574+ let v = self.expr_at(e, want.as_ref())?;
575+ self.line(&format!("return {}", v.code));
576+ }
577+ None => self.line("return"),
578+ }
579+ Ok(None)
580+ }
581+ Expr::Break(b) => {
582+ if b.expr.is_some() || b.label.is_some() {
583+ return Err("`break` with a value or a label is not implemented yet".into());
584+ }
585+ self.line("break");
586+ Ok(None)
587+ }
588+ Expr::Continue(c) => {
589+ if c.label.is_some() {
590+ return Err("labelled `continue` is not implemented yet".into());
591+ }
592+ self.line("continue");
593+ Ok(None)
594+ }
595+ Expr::Assign(a) => {
596+ let lhs = self.expr(&a.left)?;
597+ if !expressible(&a.right) {
598+ let target = lhs.code.clone();
599+ return self.assign_from(&a.right, &target, lhs.ty.as_ref()).map(|_| None);
600+ }
601+ let rhs = self.expr_at(&a.right, lhs.ty.as_ref())?;
602+ self.line(&format!("{} = {}", lhs.code, rhs.code));
603+ Ok(None)
604+ }
605+ Expr::Binary(b) if is_compound(&b.op) => {
606+ let lhs = self.expr(&b.left)?;
607+ // `i += 1` must widen the literal to `i`'s type, not to the
608+ // i32 an unconstrained Rust literal would default to.
609+ let rhs = self.expr_at(&b.right, lhs.ty.as_ref())?;
610+ let op = self.bin_op(&b.op, &lhs, &rhs)?;
611+ // Nim has no `shl=` etc., and `+=` on a `let` is illegal in
612+ // both languages, so the expanded form is always correct.
613+ self.line(&format!("{} = {} {} {}", lhs.code, lhs.code, op, rhs.code));
614+ Ok(None)
615+ }
616+ Expr::Macro(m) => {
617+ let line = self.macro_call(&m.mac)?;
618+ self.line(&line);
619+ Ok(None)
620+ }
621+ _ => Ok(Some(self.expr(e)?)),
622+ }
623+ }
624+
625+ /// Lower `e` in statement position, assigning each arm's value to
626+ /// `target`. This is how Rust's expression-oriented `if`/`match` survive
627+ /// the trip when their arms are too big for a Nim `if`-expression.
628+ fn assign_from(
629+ &mut self,
630+ e: &Expr,
631+ target: &str,
632+ expect: Option<&Nim>,
633+ ) -> Result<(), String> {
634+ let saved = self.target.replace((target.to_string(), expect.cloned()));
635+ let r = match e {
636+ Expr::If(_) => self.if_stmt(e),
637+ Expr::Match(_) => self.match_stmt(e),
638+ other => {
639+ let v = self.expr_at(other, expect)?;
640+ self.line(&format!("{} = {}", target, v.code));
641+ Ok(())
642+ }
643+ };
644+ self.target = saved;
645+ r
646+ }
647+
648+ /// Emit a block's value into the active assignment target, if there is
649+ /// one, or discard it if there is not.
650+ fn emit_tail(&mut self, v: Option<Val>) {
651+ let Some(v) = v else { return };
652+ match self.target.clone() {
653+ Some((t, _)) => {
654+ let code = v.code.clone();
655+ self.line(&format!("{t} = {code}"));
656+ }
657+ None => {
658+ let needs = v.ty.as_ref().is_none_or(|t| *t != Nim::Unit);
659+ let code = v.code.clone();
660+ if needs {
661+ self.line(&format!("discard {code}"));
662+ } else if !code.is_empty() {
663+ self.line(&code);
664+ }
665+ }
666+ }
667+ }
668+
669+ fn if_stmt(&mut self, e: &Expr) -> Result<(), String> {
670+ let Expr::If(i) = e else { unreachable!() };
671+ if let Expr::Let(_) = &*i.cond {
672+ return Err("`if let` is not implemented yet".into());
673+ }
674+ let c = self.expr(&i.cond)?;
675+ self.line(&format!("if {}:", c.code));
676+ self.nested_block(&i.then_branch)?;
677+ match &i.else_branch {
678+ None => {}
679+ Some((_, els)) => match &**els {
680+ Expr::If(_) => {
681+ // Nim needs `elif`; splice the nested `if` in as one.
682+ let mark = self.out.len();
683+ self.if_stmt(els)?;
684+ let tail = self.out.split_off(mark);
685+ let indent = " ".repeat(self.indent);
686+ self.out.push_str(&tail.replacen(&format!("{indent}if "), &format!("{indent}elif "), 1));
687+ }
688+ Expr::Block(b) => {
689+ self.line("else:");
690+ self.nested_block(&b.block)?;
691+ }
692+ _ => return Err("unsupported `else` form".into()),
693+ },
694+ }
695+ Ok(())
696+ }
697+
698+ fn for_loop(&mut self, f: &syn::ExprForLoop) -> Result<(), String> {
699+ if f.label.is_some() {
700+ return Err("loop labels are not implemented yet".into());
701+ }
702+ let name = match &*f.pat {
703+ Pat::Ident(i) => i.ident.to_string(),
704+ Pat::Wild(_) => "_".into(),
705+ _ => return Err("destructuring `for` patterns are not implemented yet".into()),
706+ };
707+
708+ // Strip the iterator adaptors that are no-ops once we are iterating a
709+ // Nim container directly. Anything else (`.map`, `.filter`, `.rev`)
710+ // is a real iterator and is rejected rather than silently dropped.
711+ let mut src = &*f.expr;
712+ loop {
713+ match src {
714+ Expr::MethodCall(m)
715+ if matches!(m.method.to_string().as_str(), "iter" | "into_iter" | "iter_mut")
716+ && m.args.is_empty() =>
717+ {
718+ src = &m.receiver
719+ }
720+ Expr::Reference(r) => src = &r.expr,
721+ _ => break,
722+ }
723+ }
724+
725+ let (header, elem) = match src {
726+ Expr::Range(r) => {
727+ let lo = match &r.start {
728+ Some(e) => self.expr(e)?,
729+ None => return Err("a `for` over `..n` needs a start bound".into()),
730+ };
731+ let hi = match &r.end {
732+ Some(e) => self.expr(e)?,
733+ None => return Err("a `for` over an unbounded range would not terminate".into()),
734+ };
735+ let op = match r.limits {
736+ syn::RangeLimits::HalfOpen(_) => "..<",
737+ syn::RangeLimits::Closed(_) => "..",
738+ };
739+ let t = lo.ty.clone().or(hi.ty.clone());
740+ (format!("{} {} {}", lo.code, op, hi.code), t)
741+ }
742+ other => {
743+ let v = self.expr(other)?;
744+ let elem = match v.ty.clone() {
745+ Some(Nim::Seq(t)) | Some(Nim::OpenArray(t)) | Some(Nim::Array(_, t)) => Some(*t),
746+ Some(Nim::Prim(p)) if p == "string" => Some(Nim::Prim("char".into())),
747+ _ => None,
748+ };
749+ (v.code, elem)
750+ }
751+ };
752+
753+ self.line(&format!("for {} in {}:", ident(&name), header));
754+ self.push_scope();
755+ if let Some(t) = elem {
756+ self.bind(&name, t);
757+ }
758+ self.indent += 1;
759+ let before = self.out.len();
760+ let saved = self.target.take();
761+ if let Some(v) = self.block_body(&f.body)? {
762+ let code = v.code.clone();
763+ self.line(&format!("discard {code}"));
764+ }
765+ self.target = saved;
766+ if self.out.len() == before {
767+ self.line("discard");
768+ }
769+ self.indent -= 1;
770+ self.pop_scope();
771+ Ok(())
772+ }
773+
774+ fn match_stmt(&mut self, e: &Expr) -> Result<(), String> {
775+ let Expr::Match(m) = e else { unreachable!() };
776+ let scrut = self.expr(&m.expr)?;
777+ // A `match` whose arms are all literal or `_` patterns is a Nim `case`,
778+ // which is exhaustiveness-checked the same way. Anything richer is
779+ // rejected rather than flattened into an if-chain that loses the
780+ // check.
781+ let name = self.fresh("Match");
782+ let t = scrut
783+ .ty
784+ .clone()
785+ .ok_or("cannot infer the type of a `match` scrutinee")?;
786+ self.line(&format!("let {}: {} = {}", name, t.render(), scrut.code));
787+ self.line(&format!("case {}", name));
788+
789+ let mut saw_wild = false;
790+ for arm in &m.arms {
791+ match &arm.pat {
792+ Pat::Guard(_) => {
793+ return Err("`match` guards are not implemented yet".into())
794+ }
795+ Pat::Wild(_) => {
796+ saw_wild = true;
797+ self.line("else:");
798+ }
799+ p => {
800+ let labels = self.pat_labels(p, Some(&t))?;
801+ self.line(&format!("of {}:", labels.join(", ")));
802+ }
803+ }
804+ self.indent += 1;
805+ let before = self.out.len();
806+ match &*arm.body {
807+ Expr::Block(b) => {
808+ self.indent -= 1;
809+ self.nested_block(&b.block)?;
810+ self.indent += 1;
811+ }
812+ other => {
813+ let v = self.expr_stmt(other)?;
814+ self.emit_tail(v);
815+ }
816+ }
817+ if self.out.len() == before {
818+ self.line("discard");
819+ }
820+ self.indent -= 1;
821+ }
822+ if !saw_wild {
823+ // Rust checked exhaustiveness already, but Nim cannot always see
824+ // it (an integer `case` needs every value covered), so make the
825+ // unreachable arm explicit rather than leaving a compile error.
826+ self.line("else:");
827+ self.line(" rsPanic(\"unreachable match arm\")");
828+ }
829+ Ok(())
830+ }
831+
832+ fn pat_labels(&mut self, p: &Pat, expect: Option<&Nim>) -> Result<Vec<String>, String> {
833+ match p {
834+ Pat::Lit(l) => Ok(vec![self.lit_at(&l.lit, expect)?.code]),
835+ Pat::Or(o) => {
836+ let mut out = Vec::new();
837+ for p in &o.cases {
838+ out.extend(self.pat_labels(p, expect)?);
839+ }
840+ Ok(out)
841+ }
842+ Pat::Range(r) => {
843+ let lo = r.start.as_ref().ok_or("open-ended range pattern")?;
844+ let hi = r.end.as_ref().ok_or("open-ended range pattern")?;
845+ let (lo, hi) = (self.expr_at(lo, expect)?, self.expr_at(hi, expect)?);
846+ let op = match r.limits {
847+ syn::RangeLimits::HalfOpen(_) => "..<",
848+ syn::RangeLimits::Closed(_) => "..",
849+ };
850+ Ok(vec![format!("{} {} {}", lo.code, op, hi.code)])
851+ }
852+ Pat::Path(p) => Ok(vec![ident(&path_name(&p.path))]),
853+ _ => Err("unsupported `match` pattern; only literals, ranges, `|` \
854+ alternatives and `_` are implemented"
855+ .into()),
856+ }
857+ }
858+
859+ // --------------------------------------------------------- expressions
860+
861+ fn expr(&mut self, e: &Expr) -> Result<Val, String> {
862+ self.expr_at(e, None)
863+ }
864+
865+ /// Lower `e`, with the type the surrounding code expects of it.
866+ ///
867+ /// Rust infers an unsuffixed integer literal's type from its context and
868+ /// falls back to `i32`; Nim falls back to 64-bit `int`. Carrying the
869+ /// expected type down to the literal is what makes `let x: u8 = 255` and
870+ /// `x.wrapping_add(100)` mean the same thing on both sides. Without it the
871+ /// widths silently diverge, which is exactly the class of bug this
872+ /// project refuses to ship.
873+ fn expr_at(&mut self, e: &Expr, expect: Option<&Nim>) -> Result<Val, String> {
874+ match e {
875+ Expr::Lit(l) => self.lit_at(&l.lit, expect),
876+ Expr::Path(p) => {
877+ let name = path_name(&p.path);
878+ match name.as_str() {
879+ "None" => Ok(Val::untyped("rsNone()")),
880+ _ => {
881+ let t = self.lookup(&name);
882+ Ok(Val::new(ident(&name), t))
883+ }
884+ }
885+ }
886+ Expr::Paren(p) => {
887+ let v = self.expr_at(&p.expr, expect)?;
888+ Ok(Val::new(format!("({})", v.code), v.ty))
889+ }
890+ Expr::Group(g) => self.expr_at(&g.expr, expect),
891+ // `&x` is a value in Nim; `&mut x` in an argument position binds to
892+ // a `var` parameter, which is also just `x` at the call site.
893+ Expr::Reference(r) => self.expr_at(&r.expr, expect),
894+ Expr::Unary(u) => self.unary(u, expect),
895+ Expr::Binary(b) => self.binary(b, expect),
896+ Expr::Cast(c) => self.cast(c),
897+ Expr::Index(i) => {
898+ let base = self.expr(&i.expr)?;
899+ let idx = self.expr(&i.index)?;
900+ // Rust indexes with usize; Nim wants an `int`, and a `uint`
901+ // index is a type error there rather than a silent conversion.
902+ let idx_code = match &idx.ty {
903+ Some(t) if t.is_unsigned() => format!("int({})", idx.code),
904+ _ => idx.code.clone(),
905+ };
906+ let elem = match base.ty.clone() {
907+ Some(Nim::Seq(t)) | Some(Nim::OpenArray(t)) | Some(Nim::Array(_, t)) => Some(*t),
908+ Some(Nim::Prim(p)) if p == "string" => Some(Nim::Prim("char".into())),
909+ _ => None,
910+ };
911+ Ok(Val::new(format!("{}[{}]", base.code, idx_code), elem))
912+ }
913+ Expr::Field(f) => {
914+ let base = self.expr(&f.base)?;
915+ let name = match &f.member {
916+ syn::Member::Named(n) => n.to_string(),
917+ syn::Member::Unnamed(i) => format!("f{}", i.index),
918+ };
919+ let t = match &base.ty {
920+ Some(Nim::Named(s, _)) => self
921+ .structs
922+ .get(s)
923+ .and_then(|fs| fs.iter().find(|(f, _)| *f == name))
924+ .map(|(_, t)| t.clone()),
925+ _ => None,
926+ };
927+ Ok(Val::new(format!("{}.{}", base.code, ident(&name)), t))
928+ }
929+ Expr::Call(c) => self.call(c),
930+ Expr::MethodCall(m) => self.method(m),
931+ Expr::Macro(m) => {
932+ let code = self.macro_call(&m.mac)?;
933+ Ok(Val::new(code, None))
934+ }
935+ Expr::Struct(s) => {
936+ let name = path_name(&s.path);
937+ let mut parts = Vec::new();
938+ for f in &s.fields {
939+ let fname = match &f.member {
940+ syn::Member::Named(n) => n.to_string(),
941+ syn::Member::Unnamed(i) => format!("f{}", i.index),
942+ };
943+ let v = self.expr(&f.expr)?;
944+ parts.push(format!("{}: {}", ident(&fname), v.code));
945+ }
946+ if s.rest.is_some() {
947+ return Err("struct update syntax `..rest` is not implemented yet".into());
948+ }
949+ Ok(Val::new(
950+ format!("{}({})", ident(&name), parts.join(", ")),
951+ Some(Nim::Named(name, vec![])),
952+ ))
953+ }
954+ Expr::Array(a) => {
955+ let mut parts = Vec::new();
956+ let mut elem = match expect {
957+ Some(Nim::Array(_, t)) | Some(Nim::Seq(t)) | Some(Nim::OpenArray(t)) => {
958+ Some((**t).clone())
959+ }
960+ _ => None,
961+ };
962+ for e in &a.elems {
963+ let want = elem.clone();
964+ let v = self.expr_at(e, want.as_ref())?;
965+ elem = elem.or(v.ty.clone());
966+ parts.push(v.code);
967+ }
968+ let t = elem.map(|t| Nim::Array(a.elems.len(), Box::new(t)));
969+ Ok(Val::new(format!("[{}]", parts.join(", ")), t))
970+ }
971+ Expr::Repeat(r) => {
972+ let v = self.expr(&r.expr)?;
973+ let n = self.expr(&r.len)?;
974+ let t = v.ty.clone().map(|t| Nim::Seq(Box::new(t)));
975+ Ok(Val::new(format!("newSeqWith(int({}), {})", n.code, v.code), t))
976+ }
977+ Expr::Tuple(t) if t.elems.is_empty() => Ok(Val::new("", Some(Nim::Unit))),
978+ Expr::Tuple(t) => {
979+ let mut parts = Vec::new();
980+ let mut tys = Vec::new();
981+ for e in &t.elems {
982+ let v = self.expr(e)?;
983+ tys.push(v.ty.clone());
984+ parts.push(v.code);
985+ }
986+ let ty = tys
987+ .iter()
988+ .cloned()
989+ .collect::<Option<Vec<_>>>()
990+ .map(Nim::Tuple);
991+ Ok(Val::new(format!("({})", parts.join(", ")), ty))
992+ }
993+ // `if` and `match` are expressions in both languages, but only
994+ // when every arm is itself a single expression.
995+ Expr::If(i) => self.if_expr(i, expect),
996+ Expr::Block(b) if b.block.stmts.len() == 1 => {
997+ if let Some(Stmt::Expr(e, None)) = b.block.stmts.first() {
998+ self.expr_at(e, expect)
999+ } else {
1000+ Err("block expression with statements in value position is not implemented yet".into())
1001+ }
1002+ }
1003+ other => Err(format!(
1004+ "unsupported expression in value position: {}",
1005+ expr_kind(other)
1006+ )),
1007+ }
1008+ }
1009+
1010+ fn if_expr(&mut self, i: &syn::ExprIf, expect: Option<&Nim>) -> Result<Val, String> {
1011+ let (Some(then), Some((_, els))) = (single_expr(&i.then_branch), &i.else_branch) else {
1012+ return Err(
1013+ "an `if` used as a value must have an `else` and single-expression arms".into(),
1014+ );
1015+ };
1016+ let c = self.expr(&i.cond)?;
1017+ let t = self.expr_at(then, expect)?;
1018+ let want = expect.cloned().or_else(|| t.ty.clone());
1019+ let e = match &**els {
1020+ Expr::Block(b) => match single_expr(&b.block) {
1021+ Some(x) => self.expr_at(x, want.as_ref())?,
1022+ None => return Err("an `if` used as a value must have single-expression arms".into()),
1023+ },
1024+ other => self.expr_at(other, want.as_ref())?,
1025+ };
1026+ let ty = t.ty.clone().or(e.ty.clone());
1027+ Ok(Val::new(
1028+ format!("(if {}: {} else: {})", c.code, t.code, e.code),
1029+ ty,
1030+ ))
1031+ }
1032+
1033+ fn lit_at(&mut self, l: &Lit, expect: Option<&Nim>) -> Result<Val, String> {
1034+ match l {
1035+ Lit::Int(i) => {
1036+ let suffix = i.suffix();
1037+ if let Some(why) = ty::rejected(suffix) {
1038+ return Err(format!("integer literal `{}`: {}", i, why));
1039+ }
1040+ let digits = i.base10_digits().to_string();
1041+ // Rust's default for an unconstrained integer literal is i32.
1042+ // Nim's is `int` (64-bit). Making the width explicit is what
1043+ // keeps overflow behaviour the same on both sides.
1044+ let t = if suffix.is_empty() {
1045+ match expect {
1046+ Some(t) if t.is_integer() => t.clone(),
1047+ // Rust's fallback for an otherwise-unconstrained
1048+ // integer literal.
1049+ _ => Nim::Prim("int32".into()),
1050+ }
1051+ } else {
1052+ ty::prim(suffix).ok_or_else(|| format!("unknown literal suffix `{suffix}`"))?
1053+ };
1054+ Ok(Val::new(format!("{}'{}", digits, nim_suffix(&t)?), Some(t)))
1055+ }
1056+ Lit::Float(f) => {
1057+ let t = match f.suffix() {
1058+ "" => match expect {
1059+ Some(Nim::Prim(p)) if p == "float32" => Nim::Prim("float32".into()),
1060+ _ => Nim::Prim("float64".into()),
1061+ },
1062+ "f64" => Nim::Prim("float64".into()),
1063+ "f32" => Nim::Prim("float32".into()),
1064+ s => return Err(format!("unknown float suffix `{s}`")),
1065+ };
1066+ let d = f.base10_digits();
1067+ let d = if d.contains('.') || d.contains('e') { d.to_string() } else { format!("{d}.0") };
1068+ Ok(Val::new(d, Some(t)))
1069+ }
1070+ Lit::Bool(b) => Ok(Val::new(b.value.to_string(), Some(Nim::Prim("bool".into())))),
1071+ Lit::Str(s) => Ok(Val::new(
1072+ fmt::nim_str(&s.value()),
1073+ Some(Nim::Prim("string".into())),
1074+ )),
1075+ Lit::Char(c) => Ok(Val::new(
1076+ format!("Rune({})", c.value() as u32),
1077+ Some(Nim::Prim("Rune".into())),
1078+ )),
1079+ Lit::Byte(b) => Ok(Val::new(
1080+ format!("{}'u8", b.value()),
1081+ Some(Nim::Prim("uint8".into())),
1082+ )),
1083+ Lit::ByteStr(b) => {
1084+ let bytes: Vec<String> = b.value().iter().map(|x| format!("{x}'u8")).collect();
1085+ Ok(Val::new(
1086+ format!("@[{}]", bytes.join(", ")),
1087+ Some(Nim::Seq(Box::new(Nim::Prim("uint8".into())))),
1088+ ))
1089+ }
1090+ other => Err(format!("unsupported literal: {other:?}")),
1091+ }
1092+ }
1093+
1094+ fn unary(&mut self, u: &syn::ExprUnary, expect: Option<&Nim>) -> Result<Val, String> {
1095+ // `-128i8` is a literal in Rust, but `-(128'i8)` in Nim would overflow
1096+ // the positive half of the range before the negation runs. Folding the
1097+ // sign into the literal keeps `i8::MIN` and friends expressible.
1098+ if let (UnOp::Neg(_), Expr::Lit(l)) = (&u.op, &*u.expr) {
1099+ if matches!(l.lit, Lit::Int(_) | Lit::Float(_)) {
1100+ let v = self.lit_at(&l.lit, expect)?;
1101+ return Ok(Val::new(format!("-{}", v.code), v.ty));
1102+ }
1103+ }
1104+ let v = self.expr_at(&u.expr, expect)?;
1105+ match u.op {
1106+ UnOp::Neg(_) => Ok(Val::new(format!("(-{})", v.code), v.ty)),
1107+ // Rust's `!` is logical on bool and bitwise-complement on integers.
1108+ // Nim spells those `not` and `not` as well, so one mapping covers
1109+ // both — but only because Nim overloads `not` the same way.
1110+ UnOp::Not(_) => Ok(Val::new(format!("(not {})", v.code), v.ty)),
1111+ UnOp::Deref(_) => Ok(v),
1112+ _ => Err("unsupported unary operator".into()),
1113+ }
1114+ }
1115+
1116+ fn binary(&mut self, b: &syn::ExprBinary, expect: Option<&Nim>) -> Result<Val, String> {
1117+ // A comparison's operands are unrelated to the `bool` it produces, so
1118+ // the outer expectation is not passed through to them.
1119+ let down = match b.op {
1120+ BinOp::Eq(_) | BinOp::Ne(_) | BinOp::Lt(_) | BinOp::Le(_) | BinOp::Gt(_)
1121+ | BinOp::Ge(_) | BinOp::And(_) | BinOp::Or(_) => None,
1122+ _ => expect,
1123+ };
1124+ let mut l = self.expr_at(&b.left, down)?;
1125+ // Rust unifies the two operand types; propagating whichever side is
1126+ // known to the other reproduces that, and disagreement then surfaces
1127+ // as a Nim type error rather than as a silent width change.
1128+ let mut r = self.expr_at(&b.right, l.ty.as_ref().or(down))?;
1129+ if l.ty.is_none() && r.ty.is_some() {
1130+ l = self.expr_at(&b.left, r.ty.as_ref())?;
1131+ }
1132+ let r = std::mem::replace(&mut r, Val::untyped(""));
1133+ let op = self.bin_op(&b.op, &l, &r)?;
1134+ let ty = match b.op {
1135+ BinOp::Eq(_) | BinOp::Ne(_) | BinOp::Lt(_) | BinOp::Le(_) | BinOp::Gt(_)
1136+ | BinOp::Ge(_) | BinOp::And(_) | BinOp::Or(_) => Some(Nim::Prim("bool".into())),
1137+ // Rust's shift takes its result type from the *left* operand, and
1138+ // the right may be a different width entirely.
1139+ BinOp::Shl(_) | BinOp::Shr(_) => l.ty.clone(),
1140+ _ => l.ty.clone().or(r.ty.clone()),
1141+ };
1142+ Ok(Val::new(format!("({} {} {})", l.code, op, r.code), ty))
1143+ }
1144+
1145+ fn bin_op(&mut self, op: &BinOp, l: &Val, r: &Val) -> Result<&'static str, String> {
1146+ Ok(match op {
1147+ BinOp::Add(_) | BinOp::AddAssign(_) => "+",
1148+ BinOp::Sub(_) | BinOp::SubAssign(_) => "-",
1149+ BinOp::Mul(_) | BinOp::MulAssign(_) => "*",
1150+ BinOp::Div(_) | BinOp::DivAssign(_) => {
1151+ // Nim spells integer division `div`. Both languages truncate
1152+ // toward zero, so once the right operator is chosen the
1153+ // semantics match, including for negative operands.
1154+ let t = l.ty.clone().or(r.ty.clone()).ok_or(
1155+ "cannot tell integer from float division here; annotate the operands",
1156+ )?;
1157+ if t.is_integer() { "div" } else { "/" }
1158+ }
1159+ BinOp::Rem(_) | BinOp::RemAssign(_) => {
1160+ let t = l.ty.clone().or(r.ty.clone()).ok_or(
1161+ "cannot tell integer from float remainder here; annotate the operands",
1162+ )?;
1163+ if t.is_integer() { "mod" } else { return Err("float `%` is not implemented yet".into()) }
1164+ }
1165+ BinOp::And(_) => "and",
1166+ BinOp::Or(_) => "or",
1167+ // Nim's `and`/`or`/`xor` are bitwise on integers and logical on
1168+ // bools, exactly as Rust's `&`/`|`/`^` are.
1169+ BinOp::BitAnd(_) | BinOp::BitAndAssign(_) => "and",
1170+ BinOp::BitOr(_) | BinOp::BitOrAssign(_) => "or",
1171+ BinOp::BitXor(_) | BinOp::BitXorAssign(_) => "xor",
1172+ // Settled empirically: Nim's `shr` on a signed integer is
1173+ // arithmetic, matching Rust. See DESIGN.md.
1174+ BinOp::Shl(_) | BinOp::ShlAssign(_) => "shl",
1175+ BinOp::Shr(_) | BinOp::ShrAssign(_) => "shr",
1176+ BinOp::Eq(_) => "==",
1177+ BinOp::Ne(_) => "!=",
1178+ BinOp::Lt(_) => "<",
1179+ BinOp::Le(_) => "<=",
1180+ BinOp::Gt(_) => ">",
1181+ BinOp::Ge(_) => ">=",
1182+ other => return Err(format!("unsupported binary operator {other:?}")),
1183+ })
1184+ }
1185+
1186+ fn cast(&mut self, c: &syn::ExprCast) -> Result<Val, String> {
1187+ let v = self.expr(&c.expr)?;
1188+ let to = ty::map(&c.ty)?;
1189+ let from = v.ty.clone().ok_or_else(|| {
1190+ format!(
1191+ "cannot lower `as {}`: the source type is unknown, and `as` \
1192+ truncates, so the source width decides the result",
1193+ to.render()
1194+ )
1195+ })?;
1196+
1197+ let code = match (&from, &to) {
1198+ (f, t) if f.is_integer() && t.is_integer() => {
1199+ // Rust's `as` between integers is a pure bit-width truncation
1200+ // or sign-extension — never a range check. Nim's `T(x)` *does*
1201+ // range-check and would raise where Rust wraps, so `cast` is
1202+ // the only faithful spelling. Probed against both compilers.
1203+ format!("cast[{}]({})", t.render(), v.code)
1204+ }
1205+ (f, Nim::Prim(p)) if f.is_integer() && (p == "float64" || p == "float32") => {
1206+ format!("{}({})", p, v.code)
1207+ }
1208+ (Nim::Prim(b), t) if b == "bool" && t.is_integer() => {
1209+ format!("{}(ord({}))", t.render(), v.code)
1210+ }
1211+ (Nim::Prim(r), t) if r == "Rune" && t.is_integer() => {
1212+ format!("cast[{}](int32({}))", t.render(), v.code)
1213+ }
1214+ (f, Nim::Prim(r)) if f.is_integer() && r == "Rune" => {
1215+ format!("Rune(int32({}))", v.code)
1216+ }
1217+ (Nim::Prim(a), Nim::Prim(b)) if a == b => v.code.clone(),
1218+ (f, t) if matches!(f, Nim::Prim(p) if p.starts_with("float")) && t.is_integer() => {
1219+ // Rust saturates float->int casts; Nim rounds and range-errors.
1220+ // Not the same operation, so it is refused rather than mapped.
1221+ return Err(format!(
1222+ "`as {}` from a float: Rust saturates, Nim rounds and range-checks; \
1223+ no faithful mapping is implemented",
1224+ t.render()
1225+ ));
1226+ }
1227+ (f, t) => {
1228+ return Err(format!(
1229+ "unsupported cast from `{}` to `{}`",
1230+ f.render(),
1231+ t.render()
1232+ ))
1233+ }
1234+ };
1235+ Ok(Val::new(code, Some(to)))
1236+ }
1237+
1238+ fn call(&mut self, c: &syn::ExprCall) -> Result<Val, String> {
1239+ let Expr::Path(p) = &*c.func else {
1240+ return Err("only calls to named functions are supported".into());
1241+ };
1242+ let name = path_name(&p.path);
1243+ let ptys: Vec<Nim> = self
1244+ .fns
1245+ .get(&name)
1246+ .map(|s| s.params.clone())
1247+ .unwrap_or_default();
1248+ let mut args = Vec::new();
1249+ for (i, a) in c.args.iter().enumerate() {
1250+ let want = ptys.get(i).cloned();
1251+ args.push(self.expr_at(a, want.as_ref())?);
1252+ }
1253+ let codes: Vec<String> = args.iter().map(|a| a.code.clone()).collect();
1254+
1255+ // Constructors from the prelude.
1256+ // Constructors that live in the prelude rather than in the input file.
1257+ if let Some(ctor) = match name.as_str() {
1258+ "Some" => Some("rsSome"),
1259+ "Ok" => Some("rsOk"),
1260+ "Err" => Some("rsErr"),
1261+ _ => None,
1262+ } {
1263+ return Ok(Val::new(format!("{}({})", ctor, codes.join(", ")), None));
1264+ }
1265+
1266+ // A bare path that names a primitive type is Rust's tuple-struct-like
1267+ // conversion, e.g. `String::from(..)`; handled by the method path.
1268+ let ret = self.fns.get(&name).map(|s| s.ret.clone());
1269+ if ret.is_none() && !self.structs.contains_key(&name) {
1270+ return Err(format!(
1271+ "call to unknown function `{name}`; only functions defined in \
1272+ this file and the supported standard-library subset can be lowered"
1273+ ));
1274+ }
1275+ Ok(Val::new(
1276+ format!("{}({})", ident(&name), codes.join(", ")),
1277+ ret,
1278+ ))
1279+ }
1280+
1281+ fn method(&mut self, m: &syn::ExprMethodCall) -> Result<Val, String> {
1282+ let recv = self.expr(&m.receiver)?;
1283+ let name = m.method.to_string();
1284+ // `x.wrapping_add(1)` and `x.min(3)` take an argument of the receiver's
1285+ // own type; `v.push(e)` takes the element type.
1286+ let arg_want = match (name.as_str(), &recv.ty) {
1287+ ("push", Some(Nim::Seq(t))) | ("push", Some(Nim::OpenArray(t))) => Some((**t).clone()),
1288+ (_, t) => t.clone(),
1289+ };
1290+ let mut args = Vec::new();
1291+ for a in &m.args {
1292+ args.push(self.expr_at(a, arg_want.as_ref())?);
1293+ }
1294+ let a0 = args.first().map(|a| a.code.clone());
1295+ let rt = recv.ty.clone();
1296+
1297+ let (code, ty) = match name.as_str() {
1298+ // Rust's `len()` is `usize`; Nim's is `int`. The conversion is
1299+ // explicit so that a `usize` binding type-checks on the Nim side.
1300+ "len" => (format!("uint({}.len)", recv.code), Some(Nim::Prim("uint".into()))),
1301+ "is_empty" => (format!("({}.len == 0)", recv.code), Some(Nim::Prim("bool".into()))),
1302+ "push" => (format!("{}.add({})", recv.code, a0.unwrap_or_default()), Some(Nim::Unit)),
1303+ "clone" | "to_vec" | "to_owned" | "as_slice" | "as_ref" | "as_mut" | "iter"
1304+ | "into_iter" => (recv.code.clone(), rt.clone()),
1305+ "unwrap" | "expect" => {
1306+ let inner = match &rt {
1307+ Some(Nim::Named(n, a)) if (n == "Option" && a.len() == 1) || (n == "Result" && a.len() == 2) => {
1308+ Some(a[0].clone())
1309+ }
1310+ _ => None,
1311+ };
1312+ (format!("unwrap({})", recv.code), inner)
1313+ }
1314+ "is_some" => (format!("{}.has", recv.code), Some(Nim::Prim("bool".into()))),
1315+ "is_none" => (format!("(not {}.has)", recv.code), Some(Nim::Prim("bool".into()))),
1316+ "is_ok" => (format!("{}.ok", recv.code), Some(Nim::Prim("bool".into()))),
1317+ "is_err" => (format!("(not {}.ok)", recv.code), Some(Nim::Prim("bool".into()))),
1318+
1319+ // Settled empirically: Nim's fixed-width *unsigned* arithmetic
1320+ // wraps silently, matching Rust's `wrapping_*`. For *signed* types
1321+ // Nim raises OverflowDefect, so the operation is routed through
1322+ // the unsigned view of the same width, which is what Rust's
1323+ // wrapping_* is defined to compute.
1324+ "wrapping_add" | "wrapping_sub" | "wrapping_mul" => {
1325+ let op = match name.as_str() {
1326+ "wrapping_add" => "+",
1327+ "wrapping_sub" => "-",
1328+ _ => "*",
1329+ };
1330+ let t = rt.clone().ok_or_else(|| {
1331+ format!("`{name}` needs a known receiver type to pick the wrapping width")
1332+ })?;
1333+ if !t.is_integer() {
1334+ return Err(format!("`{name}` on a non-integer type"));
1335+ }
1336+ let arg = a0.ok_or_else(|| format!("`{name}` takes one argument"))?;
1337+ if t.is_unsigned() {
1338+ (format!("({} {} {})", recv.code, op, arg), Some(t))
1339+ } else {
1340+ let u = unsigned_peer(&t)?;
1341+ (
1342+ format!(
1343+ "cast[{}](cast[{}]({}) {} cast[{}]({}))",
1344+ t.render(), u, recv.code, op, u, arg
1345+ ),
1346+ Some(t),
1347+ )
1348+ }
1349+ }
1350+ "abs" => (format!("abs({})", recv.code), rt.clone()),
1351+ "min" => (format!("min({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),
1352+ "max" => (format!("max({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),
1353+ "to_string" => (format!("rsDisplay({})", recv.code), Some(Nim::Prim("string".into()))),
1354+ "as_bytes" | "into_bytes" => (
1355+ format!("rsBytes({})", recv.code),
1356+ Some(Nim::Seq(Box::new(Nim::Prim("uint8".into())))),
1357+ ),
1358+
1359+ _ => {
1360+ // A method defined in this file via `impl`. Nim's UFCS makes
1361+ // the call site spelling identical.
1362+ if let Some(sig) = self.fns.get(&name) {
1363+ let ret = sig.ret.clone();
1364+ let mut all = vec![recv.code.clone()];
1365+ all.extend(args.iter().map(|a| a.code.clone()));
1366+ (format!("{}({})", ident(&name), all.join(", ")), Some(ret))
1367+ } else {
1368+ return Err(format!(
1369+ "unsupported method `.{name}()`; it is neither defined in \
1370+ this file nor part of the standard-library subset that \
1371+ has a verified Nim equivalent"
1372+ ));
1373+ }
1374+ }
1375+ };
1376+ Ok(Val::new(code, ty))
1377+ }
1378+
1379+ // -------------------------------------------------------------- macros
1380+
1381+ fn macro_call(&mut self, mac: &syn::Macro) -> Result<String, String> {
1382+ let name = path_name(&mac.path);
1383+ match name.as_str() {
1384+ "println" | "print" | "eprintln" | "eprint" => {
1385+ let s = self.format_args(mac)?;
1386+ let nl = name.ends_with("ln");
1387+ Ok(match (name.starts_with('e'), nl) {
1388+ (false, true) => format!("echo {s}"),
1389+ (false, false) => format!("stdout.write({s})"),
1390+ (true, true) => format!("stderr.writeLine({s})"),
1391+ (true, false) => format!("stderr.write({s})"),
1392+ })
1393+ }
1394+ "format" => self.format_args(mac),
1395+ "panic" => {
1396+ let s = self.format_args(mac)?;
1397+ Ok(format!("rsPanic({s})"))
1398+ }
1399+ "assert" => {
1400+ let e: Expr = mac.parse_body().map_err(|e| format!("assert!: {e}"))?;
1401+ let v = self.expr(&e)?;
1402+ Ok(format!(
1403+ "(if not ({}): rsPanic(\"assertion failed\"))",
1404+ v.code
1405+ ))
1406+ }
1407+ "vec" => {
1408+ let body = mac.tokens.to_string();
1409+ if body.trim().is_empty() {
1410+ return Ok("@[]".into());
1411+ }
1412+ let elems: syn::punctuated::Punctuated<Expr, syn::Token![,]> = mac
1413+ .parse_body_with(syn::punctuated::Punctuated::parse_terminated)
1414+ .map_err(|e| format!("vec!: {e}"))?;
1415+ let mut parts = Vec::new();
1416+ for e in &elems {
1417+ parts.push(self.expr(e)?.code);
1418+ }
1419+ Ok(format!("@[{}]", parts.join(", ")))
1420+ }
1421+ other => Err(format!(
1422+ "unsupported macro `{other}!`; a macro whose expansion is not \
1423+ known cannot be lowered faithfully"
1424+ )),
1425+ }
1426+ }
1427+
1428+ /// `println!("{} {}", a, b)` -> a Nim string-concatenation expression.
1429+ fn format_args(&mut self, mac: &syn::Macro) -> Result<String, String> {
1430+ let args: syn::punctuated::Punctuated<Expr, syn::Token![,]> = mac
1431+ .parse_body_with(syn::punctuated::Punctuated::parse_terminated)
1432+ .map_err(|e| format!("format arguments: {e}"))?;
1433+ let mut it = args.iter();
1434+ let Some(Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. })) = it.next() else {
1435+ if args.is_empty() {
1436+ return Ok("\"\"".into());
1437+ }
1438+ return Err("the first argument must be a literal format string".into());
1439+ };
1440+ let rest: Vec<&Expr> = it.collect();
1441+
1442+ let pieces = fmt::parse(&s.value())?;
1443+ let mut parts: Vec<String> = Vec::new();
1444+ let mut next = 0usize;
1445+ let mut used = vec![false; rest.len()];
1446+ for p in &pieces {
1447+ match p {
1448+ fmt::Piece::Lit(l) => parts.push(fmt::nim_str(l)),
1449+ fmt::Piece::Arg { r#ref, spec } => {
1450+ let v = match r#ref {
1451+ fmt::Ref::Next => {
1452+ let e = rest.get(next).ok_or("too few arguments for format string")?;
1453+ used[next] = true;
1454+ next += 1;
1455+ self.expr(e)?
1456+ }
1457+ fmt::Ref::Index(i) => {
1458+ let e = rest.get(*i).ok_or("format index out of range")?;
1459+ used[*i] = true;
1460+ self.expr(e)?
1461+ }
1462+ fmt::Ref::Named(n) => {
1463+ let t = self.lookup(n).ok_or_else(|| {
1464+ format!("`{{{n}}}` captures `{n}`, which is not in scope")
1465+ })?;
1466+ Val::new(ident(n), Some(t))
1467+ }
1468+ };
1469+ parts.push(fmt::render_arg(&v.code, spec));
1470+ }
1471+ }
1472+ }
1473+ // Rust rejects an argument that no `{}` consumes; so do we, rather
1474+ // than dropping it from the output.
1475+ if let Some(i) = used.iter().position(|u| !u) {
1476+ return Err(format!(
1477+ "argument {} is never used by the format string",
1478+ i + 1
1479+ ));
1480+ }
1481+ Ok(if parts.is_empty() { "\"\"".into() } else { parts.join(" & ") })
1482+ }
1483+}
1484+
1485+/// Whether an expression has a direct Nim expression form.
1486+///
1487+/// Nim's `if` is an expression only when every arm is a single expression, and
1488+/// its `case` is never one here. Anything else has to be lowered as statements
1489+/// that assign into a target.
1490+fn expressible(e: &Expr) -> bool {
1491+ match e {
1492+ Expr::If(i) => {
1493+ let Some(then) = single_expr(&i.then_branch) else { return false };
1494+ if !expressible(then) {
1495+ return false;
1496+ }
1497+ match &i.else_branch {
1498+ None => false,
1499+ Some((_, els)) => match &**els {
1500+ Expr::Block(b) => single_expr(&b.block).is_some_and(expressible),
1501+ other => expressible(other),
1502+ },
1503+ }
1504+ }
1505+ Expr::Match(_) | Expr::Block(_) | Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => false,
1506+ _ => true,
1507+ }
1508+}
1509+
1510+/// The single expression a block consists of, if that is all it is. An `if`
1511+/// can only be lowered as a Nim `if`-expression when both arms are this shape.
1512+fn single_expr(b: &syn::Block) -> Option<&Expr> {
1513+ match (b.stmts.len(), b.stmts.first()) {
1514+ (1, Some(Stmt::Expr(e, None))) => Some(e),
1515+ _ => None,
1516+ }
1517+}
1518+
1519+// --------------------------------------------------------------- utilities
1520+
1521+fn takes_self(sig: &syn::Signature) -> bool {
1522+ matches!(sig.inputs.first(), Some(FnArg::Receiver(_)))
1523+}
1524+
1525+fn path_name(p: &syn::Path) -> String {
1526+ p.segments
1527+ .last()
1528+ .map(|s| s.ident.to_string())
1529+ .unwrap_or_default()
1530+}
1531+
1532+fn is_compound(op: &BinOp) -> bool {
1533+ matches!(
1534+ op,
1535+ BinOp::AddAssign(_)
1536+ | BinOp::SubAssign(_)
1537+ | BinOp::MulAssign(_)
1538+ | BinOp::DivAssign(_)
1539+ | BinOp::RemAssign(_)
1540+ | BinOp::BitAndAssign(_)
1541+ | BinOp::BitOrAssign(_)
1542+ | BinOp::BitXorAssign(_)
1543+ | BinOp::ShlAssign(_)
1544+ | BinOp::ShrAssign(_)
1545+ )
1546+}
1547+
1548+/// The Nim literal suffix for an integer type (`5'i32`).
1549+fn nim_suffix(t: &Nim) -> Result<&'static str, String> {
1550+ let Nim::Prim(p) = t else {
1551+ return Err("not a primitive integer".into());
1552+ };
1553+ Ok(match p.as_str() {
1554+ "int8" => "i8",
1555+ "int16" => "i16",
1556+ "int32" => "i32",
1557+ "int64" => "i64",
1558+ "int" => "i",
1559+ "uint8" => "u8",
1560+ "uint16" => "u16",
1561+ "uint32" => "u32",
1562+ "uint64" => "u64",
1563+ "uint" => "u",
1564+ other => return Err(format!("no Nim literal suffix for `{other}`")),
1565+ })
1566+}
1567+
1568+/// The unsigned integer type of the same width, used to spell `wrapping_*`.
1569+fn unsigned_peer(t: &Nim) -> Result<&'static str, String> {
1570+ let Nim::Prim(p) = t else {
1571+ return Err("not a primitive integer".into());
1572+ };
1573+ Ok(match p.as_str() {
1574+ "int8" => "uint8",
1575+ "int16" => "uint16",
1576+ "int32" => "uint32",
1577+ "int64" => "uint64",
1578+ "int" => "uint",
1579+ other => return Err(format!("`{other}` has no unsigned peer")),
1580+ })
1581+}
1582+
1583+fn item_kind(i: &Item) -> &'static str {
1584+ match i {
1585+ Item::Trait(_) => "`trait`",
1586+ Item::Enum(_) => "`enum`",
1587+ Item::Type(_) => "`type` alias",
1588+ Item::Static(_) => "`static`",
1589+ Item::Macro(_) => "macro definition",
1590+ Item::Union(_) => "`union`",
1591+ Item::ExternCrate(_) => "`extern crate`",
1592+ Item::ForeignMod(_) => "`extern` block",
1593+ _ => "item",
1594+ }
1595+}
1596+
1597+fn expr_kind(e: &Expr) -> &'static str {
1598+ match e {
1599+ Expr::Closure(_) => "closure",
1600+ Expr::Async(_) => "`async` block",
1601+ Expr::Await(_) => "`.await`",
1602+ Expr::Try(_) => "`?`",
1603+ Expr::Range(_) => "range",
1604+ Expr::Match(_) => "`match` (only statement position is implemented)",
1605+ Expr::Let(_) => "`let` expression",
1606+ Expr::Unsafe(_) => "`unsafe` block",
1607+ Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => "loop (has no value in Nim)",
1608+ _ => "expression",
1609+ }
1610+}
new file mode 100644
@@ -0,0 +1,1610 @@
1+//! Rust AST -> Nim source.
2+//!
3+//! The governing rule is in DESIGN.md and it shapes every function here:
4+//! anything whose Rust semantics cannot be reproduced exactly in Nim returns
5+//! `Err` with a reason. Nothing is emitted on a guess. Where a construct maps
6+//! one-to-one (signed `shr`, unsigned wrapping, truncating `div`/`mod`) the
7+//! mapping is direct and there is a comment saying why that is safe.
8+
9+use crate::fmt;
10+use crate::ty::{self, Nim};
11+use std::collections::HashMap;
12+use syn::{
13+ BinOp, Expr, FnArg, Item, Lit, Local, Pat, ReturnType, Stmt, UnOp,
14+};
15+
16+// --------------------------------------------------------------- vocabulary
17+
18+/// Nim keywords. Rust code may legally use any of these as an identifier.
19+const NIM_KEYWORDS: &[&str] = &[
20+ "addr", "and", "as", "asm", "bind", "block", "break", "case", "cast",
21+ "concept", "const", "continue", "converter", "defer", "discard", "distinct",
22+ "div", "do", "elif", "else", "end", "enum", "except", "export", "finally",
23+ "for", "from", "func", "if", "import", "in", "include", "interface", "is",
24+ "isnot", "iterator", "let", "macro", "method", "mixin", "mod", "nil", "not",
25+ "notin", "object", "of", "or", "out", "proc", "ptr", "raise", "ref",
26+ "return", "shl", "shr", "static", "template", "try", "tuple", "type",
27+ "using", "var", "when", "while", "xor", "result", "echo",
28+];
29+
30+fn ident(name: &str) -> String {
31+ if NIM_KEYWORDS.contains(&name) {
32+ format!("{name}_r")
33+ } else {
34+ name.to_string()
35+ }
36+}
37+
38+/// A lowered expression: its Nim text, and its type where we know it.
39+///
40+/// The type is not decoration. Nim needs it to pick `div` over `/`, to size a
41+/// `cast`, and to annotate every binding so that Nim's own type checker
42+/// catches a mistake in this file rather than letting it through as output
43+/// that runs and is wrong.
44+#[derive(Clone, Debug)]
45+struct Val {
46+ code: String,
47+ ty: Option<Nim>,
48+}
49+
50+impl Val {
51+ fn new(code: impl Into<String>, ty: Option<Nim>) -> Self {
52+ Val { code: code.into(), ty }
53+ }
54+ fn untyped(code: impl Into<String>) -> Self {
55+ Val { code: code.into(), ty: None }
56+ }
57+}
58+
59+struct Sig {
60+ params: Vec<Nim>,
61+ ret: Nim,
62+}
63+
64+pub struct Lowerer {
65+ out: String,
66+ indent: usize,
67+ scopes: Vec<HashMap<String, Nim>>,
68+ fns: HashMap<String, Sig>,
69+ /// struct name -> (field, type)
70+ structs: HashMap<String, Vec<(String, Nim)>>,
71+ /// Return type of the proc being lowered, so `return e` and a trailing
72+ /// expression can type their literals the way Rust's inference would.
73+ ret: Option<Nim>,
74+ /// `(name, type)` that the arms of the `if`/`match` being lowered as a
75+ /// statement must assign their value to.
76+ target: Option<(String, Option<Nim>)>,
77+ tmp: usize,
78+}
79+
80+impl Lowerer {
81+ pub fn new() -> Self {
82+ Lowerer {
83+ out: String::new(),
84+ indent: 0,
85+ scopes: vec![HashMap::new()],
86+ fns: HashMap::new(),
87+ structs: HashMap::new(),
88+ ret: None,
89+ target: None,
90+ tmp: 0,
91+ }
92+ }
93+
94+ // ------------------------------------------------------------ emission
95+
96+ fn line(&mut self, s: &str) {
97+ for _ in 0..self.indent {
98+ self.out.push_str(" ");
99+ }
100+ self.out.push_str(s);
101+ self.out.push('\n');
102+ }
103+
104+ fn blank(&mut self) {
105+ self.out.push('\n');
106+ }
107+
108+ fn fresh(&mut self, hint: &str) -> String {
109+ self.tmp += 1;
110+ format!("rsTmp{}{}", hint, self.tmp)
111+ }
112+
113+ // --------------------------------------------------------------- scope
114+
115+ fn push_scope(&mut self) {
116+ self.scopes.push(HashMap::new());
117+ }
118+ fn pop_scope(&mut self) {
119+ self.scopes.pop();
120+ }
121+ fn bind(&mut self, name: &str, t: Nim) {
122+ self.scopes.last_mut().unwrap().insert(name.to_string(), t);
123+ }
124+ fn lookup(&self, name: &str) -> Option<Nim> {
125+ self.scopes.iter().rev().find_map(|s| s.get(name).cloned())
126+ }
127+
128+ // ---------------------------------------------------------------- file
129+
130+ pub fn lower_file(&mut self, file: &syn::File) -> Result<String, String> {
131+ self.out.push_str(include_str!("prelude.nim"));
132+ self.blank();
133+
134+ // Pass 1: signatures and struct shapes, so that a call can be typed
135+ // regardless of declaration order (Rust has no forward declarations).
136+ for item in &file.items {
137+ self.collect(item)?;
138+ }
139+ // Pass 2: bodies.
140+ for item in &file.items {
141+ self.item(item)?;
142+ }
143+
144+ if self.fns.contains_key("main") {
145+ self.blank();
146+ self.line("when isMainModule:");
147+ self.indent += 1;
148+ self.line("try:");
149+ self.line(" main()");
150+ // Rust's panic exits 101 with a message on stderr. Nim's Defects
151+ // exit 1. Mapping them here is what keeps the differential runner's
152+ // exit-status comparison meaningful for panicking programs.
153+ self.line("except RustPanic as e:");
154+ self.line(" stderr.writeLine(\"thread 'main' panicked: \" & e.msg)");
155+ self.line(" quit(101)");
156+ self.line("except Defect as e:");
157+ self.line(" stderr.writeLine(\"thread 'main' panicked: \" & e.msg)");
158+ self.line(" quit(101)");
159+ self.indent -= 1;
160+ }
161+ Ok(std::mem::take(&mut self.out))
162+ }
163+
164+ fn collect(&mut self, item: &Item) -> Result<(), String> {
165+ match item {
166+ Item::Fn(f) => {
167+ let (params, ret) = self.signature(&f.sig)?;
168+ self.fns.insert(f.sig.ident.to_string(), Sig { params, ret });
169+ }
170+ Item::Struct(s) => {
171+ let mut fields = Vec::new();
172+ for (i, f) in s.fields.iter().enumerate() {
173+ let name = match &f.ident {
174+ Some(id) => id.to_string(),
175+ None => format!("f{i}"), // tuple struct
176+ };
177+ fields.push((name, ty::map(&f.ty)?.owned()));
178+ }
179+ self.structs.insert(s.ident.to_string(), fields);
180+ }
181+ Item::Impl(im) => {
182+ let self_ty = ty::map(&im.self_ty)?;
183+ for it in &im.items {
184+ if let syn::ImplItem::Fn(m) = it {
185+ let (mut params, ret) = self.signature(&m.sig)?;
186+ if takes_self(&m.sig) {
187+ params.insert(0, self_ty.clone());
188+ }
189+ self.fns.insert(m.sig.ident.to_string(), Sig { params, ret });
190+ }
191+ }
192+ }
193+ _ => {}
194+ }
195+ Ok(())
196+ }
197+
198+ fn signature(&self, sig: &syn::Signature) -> Result<(Vec<Nim>, Nim), String> {
199+ if sig.asyncness.is_some() {
200+ return Err(format!("`async fn {}`: Nim has no equivalent", sig.ident));
201+ }
202+ if !sig.generics.params.is_empty() {
203+ return Err(format!(
204+ "`fn {}` is generic: generics are not implemented yet",
205+ sig.ident
206+ ));
207+ }
208+ let mut params = Vec::new();
209+ for a in &sig.inputs {
210+ if let FnArg::Typed(t) = a {
211+ params.push(ty::map(&t.ty)?);
212+ }
213+ }
214+ let ret = match &sig.output {
215+ ReturnType::Default => Nim::Unit,
216+ ReturnType::Type(_, t) => ty::map(t)?.owned(),
217+ };
218+ Ok((params, ret))
219+ }
220+
221+ // --------------------------------------------------------------- items
222+
223+ fn item(&mut self, item: &Item) -> Result<(), String> {
224+ match item {
225+ Item::Fn(f) => self.func(&f.sig, &f.block, None),
226+ Item::Struct(s) => {
227+ let name = s.ident.to_string();
228+ let fields = self.structs[&name].clone();
229+ self.line(&format!("type {}* = object", ident(&name)));
230+ self.indent += 1;
231+ if fields.is_empty() {
232+ self.line("discard");
233+ }
234+ for (fname, fty) in &fields {
235+ self.line(&format!("{}*: {}", ident(fname), fty.render()));
236+ }
237+ self.indent -= 1;
238+ self.blank();
239+ Ok(())
240+ }
241+ Item::Const(c) => {
242+ let t = ty::map(&c.ty)?.owned();
243+ let v = self.expr(&c.expr)?;
244+ self.bind(&c.ident.to_string(), t.clone());
245+ let line = format!("const {}*: {} = {}", ident(&c.ident.to_string()), t.render(), v.code);
246+ self.line(&line);
247+ self.blank();
248+ Ok(())
249+ }
250+ Item::Impl(im) => {
251+ let self_ty = ty::map(&im.self_ty)?;
252+ if im.trait_.is_some() {
253+ return Err(format!(
254+ "`impl Trait for {}`: trait impls are not implemented yet",
255+ self_ty.render()
256+ ));
257+ }
258+ for it in &im.items {
259+ match it {
260+ syn::ImplItem::Fn(m) => {
261+ let recv = if takes_self(&m.sig) { Some(self_ty.clone()) } else { None };
262+ self.func(&m.sig, &m.block, recv)?;
263+ }
264+ _ => return Err("only `fn` items are supported inside `impl`".into()),
265+ }
266+ }
267+ Ok(())
268+ }
269+ Item::Use(_) => Ok(()), // `use` has no Nim analogue in a single module
270+ Item::Mod(m) if m.content.is_none() => {
271+ Err(format!("`mod {};` (external file) is not implemented yet", m.ident))
272+ }
273+ other => Err(format!("unsupported item: {}", item_kind(other))),
274+ }
275+ }
276+
277+ fn func(
278+ &mut self,
279+ sig: &syn::Signature,
280+ body: &syn::Block,
281+ recv: Option<Nim>,
282+ ) -> Result<(), String> {
283+ let name = sig.ident.to_string();
284+ let (ptys, ret) = self.signature(sig)?;
285+
286+ self.push_scope();
287+ let mut rendered: Vec<String> = Vec::new();
288+
289+ if let Some(self_ty) = recv {
290+ // `&mut self` and `mut self` both mean the body may mutate the
291+ // receiver; only the former is observable by the caller, and a Nim
292+ // `var` parameter is the faithful spelling of that.
293+ let mutable = matches!(
294+ sig.inputs.first(),
295+ Some(FnArg::Receiver(r))
296+ if matches!(&r.kind, syn::ReceiverKind::Reference(_, _, m) if m.is_some())
297+ );
298+ let t = if mutable { Nim::Var(Box::new(self_ty.clone())) } else { self_ty.clone() };
299+ rendered.push(format!("self: {}", t.render()));
300+ self.bind("self", self_ty);
301+ }
302+
303+ let typed: Vec<&syn::PatType> = sig
304+ .inputs
305+ .iter()
306+ .filter_map(|a| match a {
307+ FnArg::Typed(t) => Some(t),
308+ _ => None,
309+ })
310+ .collect();
311+ for (p, t) in typed.iter().zip(ptys.iter()) {
312+ let pname = match &*p.pat {
313+ Pat::Ident(i) => i.ident.to_string(),
314+ _ => return Err("only plain identifier parameters are supported".into()),
315+ };
316+ rendered.push(format!("{}: {}", ident(&pname), t.render()));
317+ // Inside the body a `var T` parameter is used exactly like a `T`.
318+ self.bind(&pname, t.clone().owned());
319+ }
320+
321+ let head = if ret == Nim::Unit {
322+ format!("proc {}*({}) =", ident(&name), rendered.join(", "))
323+ } else {
324+ format!("proc {}*({}): {} =", ident(&name), rendered.join(", "), ret.render())
325+ };
326+ self.line(&head);
327+ self.indent += 1;
328+ let outer_ret = self.ret.replace(ret.clone());
329+
330+ // A Rust fn's trailing expression is its return value. Naming Nim's
331+ // implicit `result` as the target makes that true whether the tail is
332+ // a plain expression or an `if`/`match` with statement arms.
333+ let outer_target = if ret == Nim::Unit {
334+ self.target.take()
335+ } else {
336+ self.target.replace(("result".to_string(), Some(ret.clone())))
337+ };
338+ let before = self.out.len();
339+ let tail = self.block_body_at(body, Some(&ret))?;
340+ self.target = outer_target;
341+ match tail {
342+ Some(v) if ret != Nim::Unit => {
343+ let code = v.code.clone();
344+ self.line(&format!("result = {code}"));
345+ }
346+ Some(v) => {
347+ // A trailing expression in a `()`-returning fn is evaluated for
348+ // its effect; Nim requires an explicit discard.
349+ let needs_discard = v.ty.as_ref().is_none_or(|t| *t != Nim::Unit);
350+ if needs_discard && !v.code.is_empty() {
351+ let code = v.code.clone();
352+ self.line(&format!("discard {code}"));
353+ }
354+ }
355+ None => {}
356+ }
357+ if self.out.len() == before {
358+ self.line("discard");
359+ }
360+
361+ self.indent -= 1;
362+ self.ret = outer_ret;
363+ self.pop_scope();
364+ self.blank();
365+ Ok(())
366+ }
367+
368+ // ---------------------------------------------------------- statements
369+
370+ /// Lower a block's statements. Returns the block's trailing expression,
371+ /// if it has one, *without* emitting it — the caller decides whether that
372+ /// value is a return value, a binding, or discarded.
373+ fn block_body(&mut self, b: &syn::Block) -> Result<Option<Val>, String> {
374+ self.block_body_at(b, None)
375+ }
376+
377+ fn block_body_at(
378+ &mut self,
379+ b: &syn::Block,
380+ expect: Option<&Nim>,
381+ ) -> Result<Option<Val>, String> {
382+ // An assignment target belongs to *this* block's trailing expression
383+ // only. A non-final `if` is a statement and must not assign anything.
384+ let target = self.target.take();
385+ let n = b.stmts.len();
386+ let mut tail = None;
387+ for (i, st) in b.stmts.iter().enumerate() {
388+ let last = i + 1 == n;
389+ match st {
390+ Stmt::Expr(e, None) if last && expressible(e) => {
391+ tail = Some(self.expr_at(e, expect)?)
392+ }
393+ Stmt::Expr(e, None) if last => {
394+ // A trailing `if`/`match` with statement arms, or a loop.
395+ // Lower it as statements; if this block's value is wanted,
396+ // each arm assigns it.
397+ match &target {
398+ Some((t, ty)) => {
399+ let (t, ty) = (t.clone(), ty.clone());
400+ self.assign_from(e, &t, ty.as_ref())?;
401+ }
402+ None => self.stmt(st)?,
403+ }
404+ }
405+ _ => self.stmt(st)?,
406+ }
407+ }
408+ self.target = target;
409+ Ok(tail)
410+ }
411+
412+ /// Lower a block in statement position (loop bodies, `if` arms).
413+ fn nested_block(&mut self, b: &syn::Block) -> Result<(), String> {
414+ self.push_scope();
415+ self.indent += 1;
416+ let before = self.out.len();
417+ let want = self.target.clone().and_then(|(_, t)| t);
418+ let tail = self.block_body_at(b, want.as_ref())?;
419+ self.emit_tail(tail);
420+ if self.out.len() == before {
421+ self.line("discard");
422+ }
423+ self.indent -= 1;
424+ self.pop_scope();
425+ Ok(())
426+ }
427+
428+ fn stmt(&mut self, s: &Stmt) -> Result<(), String> {
429+ match s {
430+ Stmt::Local(l) => self.local(l),
431+ Stmt::Expr(e, _) => {
432+ let v = self.expr_stmt(e)?;
433+ if let Some(v) = v {
434+ // A bare expression with a value must be discarded in Nim.
435+ let needs = v.ty.as_ref().is_none_or(|t| *t != Nim::Unit);
436+ let code = v.code.clone();
437+ if needs {
438+ self.line(&format!("discard {code}"));
439+ } else if !code.is_empty() {
440+ self.line(&code);
441+ }
442+ }
443+ Ok(())
444+ }
445+ Stmt::Item(i) => self.item(i),
446+ Stmt::Macro(m) => {
447+ let line = self.macro_call(&m.mac)?;
448+ self.line(&line);
449+ Ok(())
450+ }
451+ }
452+ }
453+
454+ fn local(&mut self, l: &Local) -> Result<(), String> {
455+ let (name, mutable, ann): (String, bool, Option<Nim>) = match &l.pat {
456+ Pat::Ident(i) => (i.ident.to_string(), i.mutability.is_some(), None),
457+ Pat::Type(t) => match &*t.pat {
458+ Pat::Ident(i) => (i.ident.to_string(), i.mutability.is_some(), Some(ty::map(&t.ty)?)),
459+ _ => return Err("only `let <ident>` bindings are supported".into()),
460+ },
461+ Pat::Wild(_) => ("_".into(), false, None),
462+ _ => return Err("destructuring `let` is not implemented yet".into()),
463+ };
464+
465+ let Some(init) = &l.init else {
466+ // `let x: T;` — Nim's `var x: T` zero-initialises, which Rust does
467+ // not. Rust's own rules make reading it before assignment illegal,
468+ // so the two agree on every program rustc accepts.
469+ let t = ann.ok_or("`let` without an initialiser needs a type annotation")?;
470+ let t = t.owned();
471+ self.line(&format!("var {}: {}", ident(&name), t.render()));
472+ self.bind(&name, t);
473+ return Ok(());
474+ };
475+ if init.diverge.is_some() {
476+ return Err("`let ... else` is not implemented yet".into());
477+ }
478+
479+ if !expressible(&init.expr) && name != "_" {
480+ // The initialiser is an `if`/`match` whose arms are statements.
481+ // Declare first, then let each arm assign into the binding.
482+ let t = ann
483+ .clone()
484+ .ok_or_else(|| {
485+ format!(
486+ "`let {name} = match/if ...` needs a type annotation: \
487+ its arms are statements, so the binding must be \
488+ declared before they run"
489+ )
490+ })?
491+ .owned();
492+ self.line(&format!("var {}: {}", ident(&name), t.render()));
493+ self.bind(&name, t.clone());
494+ let target = ident(&name);
495+ return self.assign_from(&init.expr, &target, Some(&t));
496+ }
497+
498+ let v = self.expr_at(&init.expr, ann.as_ref())?;
499+ let t = match (ann, &v.ty) {
500+ (Some(a), _) => a.owned(),
501+ (None, Some(t)) => t.clone().owned(),
502+ (None, None) => {
503+ return Err(format!(
504+ "cannot infer the type of `let {name}`; annotate it — \
505+ guessing here would change integer width, and with it the \
506+ meaning of any arithmetic on `{name}`"
507+ ))
508+ }
509+ };
510+
511+ if name == "_" {
512+ let code = v.code.clone();
513+ self.line(&format!("discard {code}"));
514+ return Ok(());
515+ }
516+ // Rust's immutable `let` is Nim's `let`; `let mut` is `var`. Shadowing
517+ // works in both, so a re-`let` of the same name needs no rename.
518+ let kw = if mutable { "var" } else { "let" };
519+ let line = format!("{} {}: {} = {}", kw, ident(&name), t.render(), v.code);
520+ self.line(&line);
521+ self.bind(&name, t);
522+ Ok(())
523+ }
524+
525+ /// Expressions that are statements in Rust and statements in Nim too
526+ /// (control flow). Returns `None` when it emitted lines itself.
527+ fn expr_stmt(&mut self, e: &Expr) -> Result<Option<Val>, String> {
528+ match e {
529+ Expr::If(_) => {
530+ self.if_stmt(e)?;
531+ Ok(None)
532+ }
533+ Expr::While(w) => {
534+ if w.label.is_some() {
535+ return Err("loop labels are not implemented yet".into());
536+ }
537+ let c = self.expr(&w.cond)?;
538+ self.line(&format!("while {}:", c.code));
539+ let saved = self.target.take();
540+ self.nested_block(&w.body)?;
541+ self.target = saved;
542+ Ok(None)
543+ }
544+ Expr::Loop(l) => {
545+ if l.label.is_some() {
546+ return Err("loop labels are not implemented yet".into());
547+ }
548+ self.line("while true:");
549+ let saved = self.target.take();
550+ self.nested_block(&l.body)?;
551+ self.target = saved;
552+ Ok(None)
553+ }
554+ Expr::ForLoop(f) => {
555+ self.for_loop(f)?;
556+ Ok(None)
557+ }
558+ Expr::Block(b) => {
559+ if b.label.is_some() {
560+ return Err("block labels are not implemented yet".into());
561+ }
562+ self.line("block:");
563+ self.nested_block(&b.block)?;
564+ Ok(None)
565+ }
566+ Expr::Match(_) => {
567+ self.match_stmt(e)?;
568+ Ok(None)
569+ }
570+ Expr::Return(r) => {
571+ match &r.expr {
572+ Some(e) => {
573+ let want = self.ret.clone();
574+ let v = self.expr_at(e, want.as_ref())?;
575+ self.line(&format!("return {}", v.code));
576+ }
577+ None => self.line("return"),
578+ }
579+ Ok(None)
580+ }
581+ Expr::Break(b) => {
582+ if b.expr.is_some() || b.label.is_some() {
583+ return Err("`break` with a value or a label is not implemented yet".into());
584+ }
585+ self.line("break");
586+ Ok(None)
587+ }
588+ Expr::Continue(c) => {
589+ if c.label.is_some() {
590+ return Err("labelled `continue` is not implemented yet".into());
591+ }
592+ self.line("continue");
593+ Ok(None)
594+ }
595+ Expr::Assign(a) => {
596+ let lhs = self.expr(&a.left)?;
597+ if !expressible(&a.right) {
598+ let target = lhs.code.clone();
599+ return self.assign_from(&a.right, &target, lhs.ty.as_ref()).map(|_| None);
600+ }
601+ let rhs = self.expr_at(&a.right, lhs.ty.as_ref())?;
602+ self.line(&format!("{} = {}", lhs.code, rhs.code));
603+ Ok(None)
604+ }
605+ Expr::Binary(b) if is_compound(&b.op) => {
606+ let lhs = self.expr(&b.left)?;
607+ // `i += 1` must widen the literal to `i`'s type, not to the
608+ // i32 an unconstrained Rust literal would default to.
609+ let rhs = self.expr_at(&b.right, lhs.ty.as_ref())?;
610+ let op = self.bin_op(&b.op, &lhs, &rhs)?;
611+ // Nim has no `shl=` etc., and `+=` on a `let` is illegal in
612+ // both languages, so the expanded form is always correct.
613+ self.line(&format!("{} = {} {} {}", lhs.code, lhs.code, op, rhs.code));
614+ Ok(None)
615+ }
616+ Expr::Macro(m) => {
617+ let line = self.macro_call(&m.mac)?;
618+ self.line(&line);
619+ Ok(None)
620+ }
621+ _ => Ok(Some(self.expr(e)?)),
622+ }
623+ }
624+
625+ /// Lower `e` in statement position, assigning each arm's value to
626+ /// `target`. This is how Rust's expression-oriented `if`/`match` survive
627+ /// the trip when their arms are too big for a Nim `if`-expression.
628+ fn assign_from(
629+ &mut self,
630+ e: &Expr,
631+ target: &str,
632+ expect: Option<&Nim>,
633+ ) -> Result<(), String> {
634+ let saved = self.target.replace((target.to_string(), expect.cloned()));
635+ let r = match e {
636+ Expr::If(_) => self.if_stmt(e),
637+ Expr::Match(_) => self.match_stmt(e),
638+ other => {
639+ let v = self.expr_at(other, expect)?;
640+ self.line(&format!("{} = {}", target, v.code));
641+ Ok(())
642+ }
643+ };
644+ self.target = saved;
645+ r
646+ }
647+
648+ /// Emit a block's value into the active assignment target, if there is
649+ /// one, or discard it if there is not.
650+ fn emit_tail(&mut self, v: Option<Val>) {
651+ let Some(v) = v else { return };
652+ match self.target.clone() {
653+ Some((t, _)) => {
654+ let code = v.code.clone();
655+ self.line(&format!("{t} = {code}"));
656+ }
657+ None => {
658+ let needs = v.ty.as_ref().is_none_or(|t| *t != Nim::Unit);
659+ let code = v.code.clone();
660+ if needs {
661+ self.line(&format!("discard {code}"));
662+ } else if !code.is_empty() {
663+ self.line(&code);
664+ }
665+ }
666+ }
667+ }
668+
669+ fn if_stmt(&mut self, e: &Expr) -> Result<(), String> {
670+ let Expr::If(i) = e else { unreachable!() };
671+ if let Expr::Let(_) = &*i.cond {
672+ return Err("`if let` is not implemented yet".into());
673+ }
674+ let c = self.expr(&i.cond)?;
675+ self.line(&format!("if {}:", c.code));
676+ self.nested_block(&i.then_branch)?;
677+ match &i.else_branch {
678+ None => {}
679+ Some((_, els)) => match &**els {
680+ Expr::If(_) => {
681+ // Nim needs `elif`; splice the nested `if` in as one.
682+ let mark = self.out.len();
683+ self.if_stmt(els)?;
684+ let tail = self.out.split_off(mark);
685+ let indent = " ".repeat(self.indent);
686+ self.out.push_str(&tail.replacen(&format!("{indent}if "), &format!("{indent}elif "), 1));
687+ }
688+ Expr::Block(b) => {
689+ self.line("else:");
690+ self.nested_block(&b.block)?;
691+ }
692+ _ => return Err("unsupported `else` form".into()),
693+ },
694+ }
695+ Ok(())
696+ }
697+
698+ fn for_loop(&mut self, f: &syn::ExprForLoop) -> Result<(), String> {
699+ if f.label.is_some() {
700+ return Err("loop labels are not implemented yet".into());
701+ }
702+ let name = match &*f.pat {
703+ Pat::Ident(i) => i.ident.to_string(),
704+ Pat::Wild(_) => "_".into(),
705+ _ => return Err("destructuring `for` patterns are not implemented yet".into()),
706+ };
707+
708+ // Strip the iterator adaptors that are no-ops once we are iterating a
709+ // Nim container directly. Anything else (`.map`, `.filter`, `.rev`)
710+ // is a real iterator and is rejected rather than silently dropped.
711+ let mut src = &*f.expr;
712+ loop {
713+ match src {
714+ Expr::MethodCall(m)
715+ if matches!(m.method.to_string().as_str(), "iter" | "into_iter" | "iter_mut")
716+ && m.args.is_empty() =>
717+ {
718+ src = &m.receiver
719+ }
720+ Expr::Reference(r) => src = &r.expr,
721+ _ => break,
722+ }
723+ }
724+
725+ let (header, elem) = match src {
726+ Expr::Range(r) => {
727+ let lo = match &r.start {
728+ Some(e) => self.expr(e)?,
729+ None => return Err("a `for` over `..n` needs a start bound".into()),
730+ };
731+ let hi = match &r.end {
732+ Some(e) => self.expr(e)?,
733+ None => return Err("a `for` over an unbounded range would not terminate".into()),
734+ };
735+ let op = match r.limits {
736+ syn::RangeLimits::HalfOpen(_) => "..<",
737+ syn::RangeLimits::Closed(_) => "..",
738+ };
739+ let t = lo.ty.clone().or(hi.ty.clone());
740+ (format!("{} {} {}", lo.code, op, hi.code), t)
741+ }
742+ other => {
743+ let v = self.expr(other)?;
744+ let elem = match v.ty.clone() {
745+ Some(Nim::Seq(t)) | Some(Nim::OpenArray(t)) | Some(Nim::Array(_, t)) => Some(*t),
746+ Some(Nim::Prim(p)) if p == "string" => Some(Nim::Prim("char".into())),
747+ _ => None,
748+ };
749+ (v.code, elem)
750+ }
751+ };
752+
753+ self.line(&format!("for {} in {}:", ident(&name), header));
754+ self.push_scope();
755+ if let Some(t) = elem {
756+ self.bind(&name, t);
757+ }
758+ self.indent += 1;
759+ let before = self.out.len();
760+ let saved = self.target.take();
761+ if let Some(v) = self.block_body(&f.body)? {
762+ let code = v.code.clone();
763+ self.line(&format!("discard {code}"));
764+ }
765+ self.target = saved;
766+ if self.out.len() == before {
767+ self.line("discard");
768+ }
769+ self.indent -= 1;
770+ self.pop_scope();
771+ Ok(())
772+ }
773+
774+ fn match_stmt(&mut self, e: &Expr) -> Result<(), String> {
775+ let Expr::Match(m) = e else { unreachable!() };
776+ let scrut = self.expr(&m.expr)?;
777+ // A `match` whose arms are all literal or `_` patterns is a Nim `case`,
778+ // which is exhaustiveness-checked the same way. Anything richer is
779+ // rejected rather than flattened into an if-chain that loses the
780+ // check.
781+ let name = self.fresh("Match");
782+ let t = scrut
783+ .ty
784+ .clone()
785+ .ok_or("cannot infer the type of a `match` scrutinee")?;
786+ self.line(&format!("let {}: {} = {}", name, t.render(), scrut.code));
787+ self.line(&format!("case {}", name));
788+
789+ let mut saw_wild = false;
790+ for arm in &m.arms {
791+ match &arm.pat {
792+ Pat::Guard(_) => {
793+ return Err("`match` guards are not implemented yet".into())
794+ }
795+ Pat::Wild(_) => {
796+ saw_wild = true;
797+ self.line("else:");
798+ }
799+ p => {
800+ let labels = self.pat_labels(p, Some(&t))?;
801+ self.line(&format!("of {}:", labels.join(", ")));
802+ }
803+ }
804+ self.indent += 1;
805+ let before = self.out.len();
806+ match &*arm.body {
807+ Expr::Block(b) => {
808+ self.indent -= 1;
809+ self.nested_block(&b.block)?;
810+ self.indent += 1;
811+ }
812+ other => {
813+ let v = self.expr_stmt(other)?;
814+ self.emit_tail(v);
815+ }
816+ }
817+ if self.out.len() == before {
818+ self.line("discard");
819+ }
820+ self.indent -= 1;
821+ }
822+ if !saw_wild {
823+ // Rust checked exhaustiveness already, but Nim cannot always see
824+ // it (an integer `case` needs every value covered), so make the
825+ // unreachable arm explicit rather than leaving a compile error.
826+ self.line("else:");
827+ self.line(" rsPanic(\"unreachable match arm\")");
828+ }
829+ Ok(())
830+ }
831+
832+ fn pat_labels(&mut self, p: &Pat, expect: Option<&Nim>) -> Result<Vec<String>, String> {
833+ match p {
834+ Pat::Lit(l) => Ok(vec![self.lit_at(&l.lit, expect)?.code]),
835+ Pat::Or(o) => {
836+ let mut out = Vec::new();
837+ for p in &o.cases {
838+ out.extend(self.pat_labels(p, expect)?);
839+ }
840+ Ok(out)
841+ }
842+ Pat::Range(r) => {
843+ let lo = r.start.as_ref().ok_or("open-ended range pattern")?;
844+ let hi = r.end.as_ref().ok_or("open-ended range pattern")?;
845+ let (lo, hi) = (self.expr_at(lo, expect)?, self.expr_at(hi, expect)?);
846+ let op = match r.limits {
847+ syn::RangeLimits::HalfOpen(_) => "..<",
848+ syn::RangeLimits::Closed(_) => "..",
849+ };
850+ Ok(vec![format!("{} {} {}", lo.code, op, hi.code)])
851+ }
852+ Pat::Path(p) => Ok(vec![ident(&path_name(&p.path))]),
853+ _ => Err("unsupported `match` pattern; only literals, ranges, `|` \
854+ alternatives and `_` are implemented"
855+ .into()),
856+ }
857+ }
858+
859+ // --------------------------------------------------------- expressions
860+
861+ fn expr(&mut self, e: &Expr) -> Result<Val, String> {
862+ self.expr_at(e, None)
863+ }
864+
865+ /// Lower `e`, with the type the surrounding code expects of it.
866+ ///
867+ /// Rust infers an unsuffixed integer literal's type from its context and
868+ /// falls back to `i32`; Nim falls back to 64-bit `int`. Carrying the
869+ /// expected type down to the literal is what makes `let x: u8 = 255` and
870+ /// `x.wrapping_add(100)` mean the same thing on both sides. Without it the
871+ /// widths silently diverge, which is exactly the class of bug this
872+ /// project refuses to ship.
873+ fn expr_at(&mut self, e: &Expr, expect: Option<&Nim>) -> Result<Val, String> {
874+ match e {
875+ Expr::Lit(l) => self.lit_at(&l.lit, expect),
876+ Expr::Path(p) => {
877+ let name = path_name(&p.path);
878+ match name.as_str() {
879+ "None" => Ok(Val::untyped("rsNone()")),
880+ _ => {
881+ let t = self.lookup(&name);
882+ Ok(Val::new(ident(&name), t))
883+ }
884+ }
885+ }
886+ Expr::Paren(p) => {
887+ let v = self.expr_at(&p.expr, expect)?;
888+ Ok(Val::new(format!("({})", v.code), v.ty))
889+ }
890+ Expr::Group(g) => self.expr_at(&g.expr, expect),
891+ // `&x` is a value in Nim; `&mut x` in an argument position binds to
892+ // a `var` parameter, which is also just `x` at the call site.
893+ Expr::Reference(r) => self.expr_at(&r.expr, expect),
894+ Expr::Unary(u) => self.unary(u, expect),
895+ Expr::Binary(b) => self.binary(b, expect),
896+ Expr::Cast(c) => self.cast(c),
897+ Expr::Index(i) => {
898+ let base = self.expr(&i.expr)?;
899+ let idx = self.expr(&i.index)?;
900+ // Rust indexes with usize; Nim wants an `int`, and a `uint`
901+ // index is a type error there rather than a silent conversion.
902+ let idx_code = match &idx.ty {
903+ Some(t) if t.is_unsigned() => format!("int({})", idx.code),
904+ _ => idx.code.clone(),
905+ };
906+ let elem = match base.ty.clone() {
907+ Some(Nim::Seq(t)) | Some(Nim::OpenArray(t)) | Some(Nim::Array(_, t)) => Some(*t),
908+ Some(Nim::Prim(p)) if p == "string" => Some(Nim::Prim("char".into())),
909+ _ => None,
910+ };
911+ Ok(Val::new(format!("{}[{}]", base.code, idx_code), elem))
912+ }
913+ Expr::Field(f) => {
914+ let base = self.expr(&f.base)?;
915+ let name = match &f.member {
916+ syn::Member::Named(n) => n.to_string(),
917+ syn::Member::Unnamed(i) => format!("f{}", i.index),
918+ };
919+ let t = match &base.ty {
920+ Some(Nim::Named(s, _)) => self
921+ .structs
922+ .get(s)
923+ .and_then(|fs| fs.iter().find(|(f, _)| *f == name))
924+ .map(|(_, t)| t.clone()),
925+ _ => None,
926+ };
927+ Ok(Val::new(format!("{}.{}", base.code, ident(&name)), t))
928+ }
929+ Expr::Call(c) => self.call(c),
930+ Expr::MethodCall(m) => self.method(m),
931+ Expr::Macro(m) => {
932+ let code = self.macro_call(&m.mac)?;
933+ Ok(Val::new(code, None))
934+ }
935+ Expr::Struct(s) => {
936+ let name = path_name(&s.path);
937+ let mut parts = Vec::new();
938+ for f in &s.fields {
939+ let fname = match &f.member {
940+ syn::Member::Named(n) => n.to_string(),
941+ syn::Member::Unnamed(i) => format!("f{}", i.index),
942+ };
943+ let v = self.expr(&f.expr)?;
944+ parts.push(format!("{}: {}", ident(&fname), v.code));
945+ }
946+ if s.rest.is_some() {
947+ return Err("struct update syntax `..rest` is not implemented yet".into());
948+ }
949+ Ok(Val::new(
950+ format!("{}({})", ident(&name), parts.join(", ")),
951+ Some(Nim::Named(name, vec![])),
952+ ))
953+ }
954+ Expr::Array(a) => {
955+ let mut parts = Vec::new();
956+ let mut elem = match expect {
957+ Some(Nim::Array(_, t)) | Some(Nim::Seq(t)) | Some(Nim::OpenArray(t)) => {
958+ Some((**t).clone())
959+ }
960+ _ => None,
961+ };
962+ for e in &a.elems {
963+ let want = elem.clone();
964+ let v = self.expr_at(e, want.as_ref())?;
965+ elem = elem.or(v.ty.clone());
966+ parts.push(v.code);
967+ }
968+ let t = elem.map(|t| Nim::Array(a.elems.len(), Box::new(t)));
969+ Ok(Val::new(format!("[{}]", parts.join(", ")), t))
970+ }
971+ Expr::Repeat(r) => {
972+ let v = self.expr(&r.expr)?;
973+ let n = self.expr(&r.len)?;
974+ let t = v.ty.clone().map(|t| Nim::Seq(Box::new(t)));
975+ Ok(Val::new(format!("newSeqWith(int({}), {})", n.code, v.code), t))
976+ }
977+ Expr::Tuple(t) if t.elems.is_empty() => Ok(Val::new("", Some(Nim::Unit))),
978+ Expr::Tuple(t) => {
979+ let mut parts = Vec::new();
980+ let mut tys = Vec::new();
981+ for e in &t.elems {
982+ let v = self.expr(e)?;
983+ tys.push(v.ty.clone());
984+ parts.push(v.code);
985+ }
986+ let ty = tys
987+ .iter()
988+ .cloned()
989+ .collect::<Option<Vec<_>>>()
990+ .map(Nim::Tuple);
991+ Ok(Val::new(format!("({})", parts.join(", ")), ty))
992+ }
993+ // `if` and `match` are expressions in both languages, but only
994+ // when every arm is itself a single expression.
995+ Expr::If(i) => self.if_expr(i, expect),
996+ Expr::Block(b) if b.block.stmts.len() == 1 => {
997+ if let Some(Stmt::Expr(e, None)) = b.block.stmts.first() {
998+ self.expr_at(e, expect)
999+ } else {
1000+ Err("block expression with statements in value position is not implemented yet".into())
1001+ }
1002+ }
1003+ other => Err(format!(
1004+ "unsupported expression in value position: {}",
1005+ expr_kind(other)
1006+ )),
1007+ }
1008+ }
1009+
1010+ fn if_expr(&mut self, i: &syn::ExprIf, expect: Option<&Nim>) -> Result<Val, String> {
1011+ let (Some(then), Some((_, els))) = (single_expr(&i.then_branch), &i.else_branch) else {
1012+ return Err(
1013+ "an `if` used as a value must have an `else` and single-expression arms".into(),
1014+ );
1015+ };
1016+ let c = self.expr(&i.cond)?;
1017+ let t = self.expr_at(then, expect)?;
1018+ let want = expect.cloned().or_else(|| t.ty.clone());
1019+ let e = match &**els {
1020+ Expr::Block(b) => match single_expr(&b.block) {
1021+ Some(x) => self.expr_at(x, want.as_ref())?,
1022+ None => return Err("an `if` used as a value must have single-expression arms".into()),
1023+ },
1024+ other => self.expr_at(other, want.as_ref())?,
1025+ };
1026+ let ty = t.ty.clone().or(e.ty.clone());
1027+ Ok(Val::new(
1028+ format!("(if {}: {} else: {})", c.code, t.code, e.code),
1029+ ty,
1030+ ))
1031+ }
1032+
1033+ fn lit_at(&mut self, l: &Lit, expect: Option<&Nim>) -> Result<Val, String> {
1034+ match l {
1035+ Lit::Int(i) => {
1036+ let suffix = i.suffix();
1037+ if let Some(why) = ty::rejected(suffix) {
1038+ return Err(format!("integer literal `{}`: {}", i, why));
1039+ }
1040+ let digits = i.base10_digits().to_string();
1041+ // Rust's default for an unconstrained integer literal is i32.
1042+ // Nim's is `int` (64-bit). Making the width explicit is what
1043+ // keeps overflow behaviour the same on both sides.
1044+ let t = if suffix.is_empty() {
1045+ match expect {
1046+ Some(t) if t.is_integer() => t.clone(),
1047+ // Rust's fallback for an otherwise-unconstrained
1048+ // integer literal.
1049+ _ => Nim::Prim("int32".into()),
1050+ }
1051+ } else {
1052+ ty::prim(suffix).ok_or_else(|| format!("unknown literal suffix `{suffix}`"))?
1053+ };
1054+ Ok(Val::new(format!("{}'{}", digits, nim_suffix(&t)?), Some(t)))
1055+ }
1056+ Lit::Float(f) => {
1057+ let t = match f.suffix() {
1058+ "" => match expect {
1059+ Some(Nim::Prim(p)) if p == "float32" => Nim::Prim("float32".into()),
1060+ _ => Nim::Prim("float64".into()),
1061+ },
1062+ "f64" => Nim::Prim("float64".into()),
1063+ "f32" => Nim::Prim("float32".into()),
1064+ s => return Err(format!("unknown float suffix `{s}`")),
1065+ };
1066+ let d = f.base10_digits();
1067+ let d = if d.contains('.') || d.contains('e') { d.to_string() } else { format!("{d}.0") };
1068+ Ok(Val::new(d, Some(t)))
1069+ }
1070+ Lit::Bool(b) => Ok(Val::new(b.value.to_string(), Some(Nim::Prim("bool".into())))),
1071+ Lit::Str(s) => Ok(Val::new(
1072+ fmt::nim_str(&s.value()),
1073+ Some(Nim::Prim("string".into())),
1074+ )),
1075+ Lit::Char(c) => Ok(Val::new(
1076+ format!("Rune({})", c.value() as u32),
1077+ Some(Nim::Prim("Rune".into())),
1078+ )),
1079+ Lit::Byte(b) => Ok(Val::new(
1080+ format!("{}'u8", b.value()),
1081+ Some(Nim::Prim("uint8".into())),
1082+ )),
1083+ Lit::ByteStr(b) => {
1084+ let bytes: Vec<String> = b.value().iter().map(|x| format!("{x}'u8")).collect();
1085+ Ok(Val::new(
1086+ format!("@[{}]", bytes.join(", ")),
1087+ Some(Nim::Seq(Box::new(Nim::Prim("uint8".into())))),
1088+ ))
1089+ }
1090+ other => Err(format!("unsupported literal: {other:?}")),
1091+ }
1092+ }
1093+
1094+ fn unary(&mut self, u: &syn::ExprUnary, expect: Option<&Nim>) -> Result<Val, String> {
1095+ // `-128i8` is a literal in Rust, but `-(128'i8)` in Nim would overflow
1096+ // the positive half of the range before the negation runs. Folding the
1097+ // sign into the literal keeps `i8::MIN` and friends expressible.
1098+ if let (UnOp::Neg(_), Expr::Lit(l)) = (&u.op, &*u.expr) {
1099+ if matches!(l.lit, Lit::Int(_) | Lit::Float(_)) {
1100+ let v = self.lit_at(&l.lit, expect)?;
1101+ return Ok(Val::new(format!("-{}", v.code), v.ty));
1102+ }
1103+ }
1104+ let v = self.expr_at(&u.expr, expect)?;
1105+ match u.op {
1106+ UnOp::Neg(_) => Ok(Val::new(format!("(-{})", v.code), v.ty)),
1107+ // Rust's `!` is logical on bool and bitwise-complement on integers.
1108+ // Nim spells those `not` and `not` as well, so one mapping covers
1109+ // both — but only because Nim overloads `not` the same way.
1110+ UnOp::Not(_) => Ok(Val::new(format!("(not {})", v.code), v.ty)),
1111+ UnOp::Deref(_) => Ok(v),
1112+ _ => Err("unsupported unary operator".into()),
1113+ }
1114+ }
1115+
1116+ fn binary(&mut self, b: &syn::ExprBinary, expect: Option<&Nim>) -> Result<Val, String> {
1117+ // A comparison's operands are unrelated to the `bool` it produces, so
1118+ // the outer expectation is not passed through to them.
1119+ let down = match b.op {
1120+ BinOp::Eq(_) | BinOp::Ne(_) | BinOp::Lt(_) | BinOp::Le(_) | BinOp::Gt(_)
1121+ | BinOp::Ge(_) | BinOp::And(_) | BinOp::Or(_) => None,
1122+ _ => expect,
1123+ };
1124+ let mut l = self.expr_at(&b.left, down)?;
1125+ // Rust unifies the two operand types; propagating whichever side is
1126+ // known to the other reproduces that, and disagreement then surfaces
1127+ // as a Nim type error rather than as a silent width change.
1128+ let mut r = self.expr_at(&b.right, l.ty.as_ref().or(down))?;
1129+ if l.ty.is_none() && r.ty.is_some() {
1130+ l = self.expr_at(&b.left, r.ty.as_ref())?;
1131+ }
1132+ let r = std::mem::replace(&mut r, Val::untyped(""));
1133+ let op = self.bin_op(&b.op, &l, &r)?;
1134+ let ty = match b.op {
1135+ BinOp::Eq(_) | BinOp::Ne(_) | BinOp::Lt(_) | BinOp::Le(_) | BinOp::Gt(_)
1136+ | BinOp::Ge(_) | BinOp::And(_) | BinOp::Or(_) => Some(Nim::Prim("bool".into())),
1137+ // Rust's shift takes its result type from the *left* operand, and
1138+ // the right may be a different width entirely.
1139+ BinOp::Shl(_) | BinOp::Shr(_) => l.ty.clone(),
1140+ _ => l.ty.clone().or(r.ty.clone()),
1141+ };
1142+ Ok(Val::new(format!("({} {} {})", l.code, op, r.code), ty))
1143+ }
1144+
1145+ fn bin_op(&mut self, op: &BinOp, l: &Val, r: &Val) -> Result<&'static str, String> {
1146+ Ok(match op {
1147+ BinOp::Add(_) | BinOp::AddAssign(_) => "+",
1148+ BinOp::Sub(_) | BinOp::SubAssign(_) => "-",
1149+ BinOp::Mul(_) | BinOp::MulAssign(_) => "*",
1150+ BinOp::Div(_) | BinOp::DivAssign(_) => {
1151+ // Nim spells integer division `div`. Both languages truncate
1152+ // toward zero, so once the right operator is chosen the
1153+ // semantics match, including for negative operands.
1154+ let t = l.ty.clone().or(r.ty.clone()).ok_or(
1155+ "cannot tell integer from float division here; annotate the operands",
1156+ )?;
1157+ if t.is_integer() { "div" } else { "/" }
1158+ }
1159+ BinOp::Rem(_) | BinOp::RemAssign(_) => {
1160+ let t = l.ty.clone().or(r.ty.clone()).ok_or(
1161+ "cannot tell integer from float remainder here; annotate the operands",
1162+ )?;
1163+ if t.is_integer() { "mod" } else { return Err("float `%` is not implemented yet".into()) }
1164+ }
1165+ BinOp::And(_) => "and",
1166+ BinOp::Or(_) => "or",
1167+ // Nim's `and`/`or`/`xor` are bitwise on integers and logical on
1168+ // bools, exactly as Rust's `&`/`|`/`^` are.
1169+ BinOp::BitAnd(_) | BinOp::BitAndAssign(_) => "and",
1170+ BinOp::BitOr(_) | BinOp::BitOrAssign(_) => "or",
1171+ BinOp::BitXor(_) | BinOp::BitXorAssign(_) => "xor",
1172+ // Settled empirically: Nim's `shr` on a signed integer is
1173+ // arithmetic, matching Rust. See DESIGN.md.
1174+ BinOp::Shl(_) | BinOp::ShlAssign(_) => "shl",
1175+ BinOp::Shr(_) | BinOp::ShrAssign(_) => "shr",
1176+ BinOp::Eq(_) => "==",
1177+ BinOp::Ne(_) => "!=",
1178+ BinOp::Lt(_) => "<",
1179+ BinOp::Le(_) => "<=",
1180+ BinOp::Gt(_) => ">",
1181+ BinOp::Ge(_) => ">=",
1182+ other => return Err(format!("unsupported binary operator {other:?}")),
1183+ })
1184+ }
1185+
1186+ fn cast(&mut self, c: &syn::ExprCast) -> Result<Val, String> {
1187+ let v = self.expr(&c.expr)?;
1188+ let to = ty::map(&c.ty)?;
1189+ let from = v.ty.clone().ok_or_else(|| {
1190+ format!(
1191+ "cannot lower `as {}`: the source type is unknown, and `as` \
1192+ truncates, so the source width decides the result",
1193+ to.render()
1194+ )
1195+ })?;
1196+
1197+ let code = match (&from, &to) {
1198+ (f, t) if f.is_integer() && t.is_integer() => {
1199+ // Rust's `as` between integers is a pure bit-width truncation
1200+ // or sign-extension — never a range check. Nim's `T(x)` *does*
1201+ // range-check and would raise where Rust wraps, so `cast` is
1202+ // the only faithful spelling. Probed against both compilers.
1203+ format!("cast[{}]({})", t.render(), v.code)
1204+ }
1205+ (f, Nim::Prim(p)) if f.is_integer() && (p == "float64" || p == "float32") => {
1206+ format!("{}({})", p, v.code)
1207+ }
1208+ (Nim::Prim(b), t) if b == "bool" && t.is_integer() => {
1209+ format!("{}(ord({}))", t.render(), v.code)
1210+ }
1211+ (Nim::Prim(r), t) if r == "Rune" && t.is_integer() => {
1212+ format!("cast[{}](int32({}))", t.render(), v.code)
1213+ }
1214+ (f, Nim::Prim(r)) if f.is_integer() && r == "Rune" => {
1215+ format!("Rune(int32({}))", v.code)
1216+ }
1217+ (Nim::Prim(a), Nim::Prim(b)) if a == b => v.code.clone(),
1218+ (f, t) if matches!(f, Nim::Prim(p) if p.starts_with("float")) && t.is_integer() => {
1219+ // Rust saturates float->int casts; Nim rounds and range-errors.
1220+ // Not the same operation, so it is refused rather than mapped.
1221+ return Err(format!(
1222+ "`as {}` from a float: Rust saturates, Nim rounds and range-checks; \
1223+ no faithful mapping is implemented",
1224+ t.render()
1225+ ));
1226+ }
1227+ (f, t) => {
1228+ return Err(format!(
1229+ "unsupported cast from `{}` to `{}`",
1230+ f.render(),
1231+ t.render()
1232+ ))
1233+ }
1234+ };
1235+ Ok(Val::new(code, Some(to)))
1236+ }
1237+
1238+ fn call(&mut self, c: &syn::ExprCall) -> Result<Val, String> {
1239+ let Expr::Path(p) = &*c.func else {
1240+ return Err("only calls to named functions are supported".into());
1241+ };
1242+ let name = path_name(&p.path);
1243+ let ptys: Vec<Nim> = self
1244+ .fns
1245+ .get(&name)
1246+ .map(|s| s.params.clone())
1247+ .unwrap_or_default();
1248+ let mut args = Vec::new();
1249+ for (i, a) in c.args.iter().enumerate() {
1250+ let want = ptys.get(i).cloned();
1251+ args.push(self.expr_at(a, want.as_ref())?);
1252+ }
1253+ let codes: Vec<String> = args.iter().map(|a| a.code.clone()).collect();
1254+
1255+ // Constructors from the prelude.
1256+ // Constructors that live in the prelude rather than in the input file.
1257+ if let Some(ctor) = match name.as_str() {
1258+ "Some" => Some("rsSome"),
1259+ "Ok" => Some("rsOk"),
1260+ "Err" => Some("rsErr"),
1261+ _ => None,
1262+ } {
1263+ return Ok(Val::new(format!("{}({})", ctor, codes.join(", ")), None));
1264+ }
1265+
1266+ // A bare path that names a primitive type is Rust's tuple-struct-like
1267+ // conversion, e.g. `String::from(..)`; handled by the method path.
1268+ let ret = self.fns.get(&name).map(|s| s.ret.clone());
1269+ if ret.is_none() && !self.structs.contains_key(&name) {
1270+ return Err(format!(
1271+ "call to unknown function `{name}`; only functions defined in \
1272+ this file and the supported standard-library subset can be lowered"
1273+ ));
1274+ }
1275+ Ok(Val::new(
1276+ format!("{}({})", ident(&name), codes.join(", ")),
1277+ ret,
1278+ ))
1279+ }
1280+
1281+ fn method(&mut self, m: &syn::ExprMethodCall) -> Result<Val, String> {
1282+ let recv = self.expr(&m.receiver)?;
1283+ let name = m.method.to_string();
1284+ // `x.wrapping_add(1)` and `x.min(3)` take an argument of the receiver's
1285+ // own type; `v.push(e)` takes the element type.
1286+ let arg_want = match (name.as_str(), &recv.ty) {
1287+ ("push", Some(Nim::Seq(t))) | ("push", Some(Nim::OpenArray(t))) => Some((**t).clone()),
1288+ (_, t) => t.clone(),
1289+ };
1290+ let mut args = Vec::new();
1291+ for a in &m.args {
1292+ args.push(self.expr_at(a, arg_want.as_ref())?);
1293+ }
1294+ let a0 = args.first().map(|a| a.code.clone());
1295+ let rt = recv.ty.clone();
1296+
1297+ let (code, ty) = match name.as_str() {
1298+ // Rust's `len()` is `usize`; Nim's is `int`. The conversion is
1299+ // explicit so that a `usize` binding type-checks on the Nim side.
1300+ "len" => (format!("uint({}.len)", recv.code), Some(Nim::Prim("uint".into()))),
1301+ "is_empty" => (format!("({}.len == 0)", recv.code), Some(Nim::Prim("bool".into()))),
1302+ "push" => (format!("{}.add({})", recv.code, a0.unwrap_or_default()), Some(Nim::Unit)),
1303+ "clone" | "to_vec" | "to_owned" | "as_slice" | "as_ref" | "as_mut" | "iter"
1304+ | "into_iter" => (recv.code.clone(), rt.clone()),
1305+ "unwrap" | "expect" => {
1306+ let inner = match &rt {
1307+ Some(Nim::Named(n, a)) if (n == "Option" && a.len() == 1) || (n == "Result" && a.len() == 2) => {
1308+ Some(a[0].clone())
1309+ }
1310+ _ => None,
1311+ };
1312+ (format!("unwrap({})", recv.code), inner)
1313+ }
1314+ "is_some" => (format!("{}.has", recv.code), Some(Nim::Prim("bool".into()))),
1315+ "is_none" => (format!("(not {}.has)", recv.code), Some(Nim::Prim("bool".into()))),
1316+ "is_ok" => (format!("{}.ok", recv.code), Some(Nim::Prim("bool".into()))),
1317+ "is_err" => (format!("(not {}.ok)", recv.code), Some(Nim::Prim("bool".into()))),
1318+
1319+ // Settled empirically: Nim's fixed-width *unsigned* arithmetic
1320+ // wraps silently, matching Rust's `wrapping_*`. For *signed* types
1321+ // Nim raises OverflowDefect, so the operation is routed through
1322+ // the unsigned view of the same width, which is what Rust's
1323+ // wrapping_* is defined to compute.
1324+ "wrapping_add" | "wrapping_sub" | "wrapping_mul" => {
1325+ let op = match name.as_str() {
1326+ "wrapping_add" => "+",
1327+ "wrapping_sub" => "-",
1328+ _ => "*",
1329+ };
1330+ let t = rt.clone().ok_or_else(|| {
1331+ format!("`{name}` needs a known receiver type to pick the wrapping width")
1332+ })?;
1333+ if !t.is_integer() {
1334+ return Err(format!("`{name}` on a non-integer type"));
1335+ }
1336+ let arg = a0.ok_or_else(|| format!("`{name}` takes one argument"))?;
1337+ if t.is_unsigned() {
1338+ (format!("({} {} {})", recv.code, op, arg), Some(t))
1339+ } else {
1340+ let u = unsigned_peer(&t)?;
1341+ (
1342+ format!(
1343+ "cast[{}](cast[{}]({}) {} cast[{}]({}))",
1344+ t.render(), u, recv.code, op, u, arg
1345+ ),
1346+ Some(t),
1347+ )
1348+ }
1349+ }
1350+ "abs" => (format!("abs({})", recv.code), rt.clone()),
1351+ "min" => (format!("min({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),
1352+ "max" => (format!("max({}, {})", recv.code, a0.unwrap_or_default()), rt.clone()),
1353+ "to_string" => (format!("rsDisplay({})", recv.code), Some(Nim::Prim("string".into()))),
1354+ "as_bytes" | "into_bytes" => (
1355+ format!("rsBytes({})", recv.code),
1356+ Some(Nim::Seq(Box::new(Nim::Prim("uint8".into())))),
1357+ ),
1358+
1359+ _ => {
1360+ // A method defined in this file via `impl`. Nim's UFCS makes
1361+ // the call site spelling identical.
1362+ if let Some(sig) = self.fns.get(&name) {
1363+ let ret = sig.ret.clone();
1364+ let mut all = vec![recv.code.clone()];
1365+ all.extend(args.iter().map(|a| a.code.clone()));
1366+ (format!("{}({})", ident(&name), all.join(", ")), Some(ret))
1367+ } else {
1368+ return Err(format!(
1369+ "unsupported method `.{name}()`; it is neither defined in \
1370+ this file nor part of the standard-library subset that \
1371+ has a verified Nim equivalent"
1372+ ));
1373+ }
1374+ }
1375+ };
1376+ Ok(Val::new(code, ty))
1377+ }
1378+
1379+ // -------------------------------------------------------------- macros
1380+
1381+ fn macro_call(&mut self, mac: &syn::Macro) -> Result<String, String> {
1382+ let name = path_name(&mac.path);
1383+ match name.as_str() {
1384+ "println" | "print" | "eprintln" | "eprint" => {
1385+ let s = self.format_args(mac)?;
1386+ let nl = name.ends_with("ln");
1387+ Ok(match (name.starts_with('e'), nl) {
1388+ (false, true) => format!("echo {s}"),
1389+ (false, false) => format!("stdout.write({s})"),
1390+ (true, true) => format!("stderr.writeLine({s})"),
1391+ (true, false) => format!("stderr.write({s})"),
1392+ })
1393+ }
1394+ "format" => self.format_args(mac),
1395+ "panic" => {
1396+ let s = self.format_args(mac)?;
1397+ Ok(format!("rsPanic({s})"))
1398+ }
1399+ "assert" => {
1400+ let e: Expr = mac.parse_body().map_err(|e| format!("assert!: {e}"))?;
1401+ let v = self.expr(&e)?;
1402+ Ok(format!(
1403+ "(if not ({}): rsPanic(\"assertion failed\"))",
1404+ v.code
1405+ ))
1406+ }
1407+ "vec" => {
1408+ let body = mac.tokens.to_string();
1409+ if body.trim().is_empty() {
1410+ return Ok("@[]".into());
1411+ }
1412+ let elems: syn::punctuated::Punctuated<Expr, syn::Token![,]> = mac
1413+ .parse_body_with(syn::punctuated::Punctuated::parse_terminated)
1414+ .map_err(|e| format!("vec!: {e}"))?;
1415+ let mut parts = Vec::new();
1416+ for e in &elems {
1417+ parts.push(self.expr(e)?.code);
1418+ }
1419+ Ok(format!("@[{}]", parts.join(", ")))
1420+ }
1421+ other => Err(format!(
1422+ "unsupported macro `{other}!`; a macro whose expansion is not \
1423+ known cannot be lowered faithfully"
1424+ )),
1425+ }
1426+ }
1427+
1428+ /// `println!("{} {}", a, b)` -> a Nim string-concatenation expression.
1429+ fn format_args(&mut self, mac: &syn::Macro) -> Result<String, String> {
1430+ let args: syn::punctuated::Punctuated<Expr, syn::Token![,]> = mac
1431+ .parse_body_with(syn::punctuated::Punctuated::parse_terminated)
1432+ .map_err(|e| format!("format arguments: {e}"))?;
1433+ let mut it = args.iter();
1434+ let Some(Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. })) = it.next() else {
1435+ if args.is_empty() {
1436+ return Ok("\"\"".into());
1437+ }
1438+ return Err("the first argument must be a literal format string".into());
1439+ };
1440+ let rest: Vec<&Expr> = it.collect();
1441+
1442+ let pieces = fmt::parse(&s.value())?;
1443+ let mut parts: Vec<String> = Vec::new();
1444+ let mut next = 0usize;
1445+ let mut used = vec![false; rest.len()];
1446+ for p in &pieces {
1447+ match p {
1448+ fmt::Piece::Lit(l) => parts.push(fmt::nim_str(l)),
1449+ fmt::Piece::Arg { r#ref, spec } => {
1450+ let v = match r#ref {
1451+ fmt::Ref::Next => {
1452+ let e = rest.get(next).ok_or("too few arguments for format string")?;
1453+ used[next] = true;
1454+ next += 1;
1455+ self.expr(e)?
1456+ }
1457+ fmt::Ref::Index(i) => {
1458+ let e = rest.get(*i).ok_or("format index out of range")?;
1459+ used[*i] = true;
1460+ self.expr(e)?
1461+ }
1462+ fmt::Ref::Named(n) => {
1463+ let t = self.lookup(n).ok_or_else(|| {
1464+ format!("`{{{n}}}` captures `{n}`, which is not in scope")
1465+ })?;
1466+ Val::new(ident(n), Some(t))
1467+ }
1468+ };
1469+ parts.push(fmt::render_arg(&v.code, spec));
1470+ }
1471+ }
1472+ }
1473+ // Rust rejects an argument that no `{}` consumes; so do we, rather
1474+ // than dropping it from the output.
1475+ if let Some(i) = used.iter().position(|u| !u) {
1476+ return Err(format!(
1477+ "argument {} is never used by the format string",
1478+ i + 1
1479+ ));
1480+ }
1481+ Ok(if parts.is_empty() { "\"\"".into() } else { parts.join(" & ") })
1482+ }
1483+}
1484+
1485+/// Whether an expression has a direct Nim expression form.
1486+///
1487+/// Nim's `if` is an expression only when every arm is a single expression, and
1488+/// its `case` is never one here. Anything else has to be lowered as statements
1489+/// that assign into a target.
1490+fn expressible(e: &Expr) -> bool {
1491+ match e {
1492+ Expr::If(i) => {
1493+ let Some(then) = single_expr(&i.then_branch) else { return false };
1494+ if !expressible(then) {
1495+ return false;
1496+ }
1497+ match &i.else_branch {
1498+ None => false,
1499+ Some((_, els)) => match &**els {
1500+ Expr::Block(b) => single_expr(&b.block).is_some_and(expressible),
1501+ other => expressible(other),
1502+ },
1503+ }
1504+ }
1505+ Expr::Match(_) | Expr::Block(_) | Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => false,
1506+ _ => true,
1507+ }
1508+}
1509+
1510+/// The single expression a block consists of, if that is all it is. An `if`
1511+/// can only be lowered as a Nim `if`-expression when both arms are this shape.
1512+fn single_expr(b: &syn::Block) -> Option<&Expr> {
1513+ match (b.stmts.len(), b.stmts.first()) {
1514+ (1, Some(Stmt::Expr(e, None))) => Some(e),
1515+ _ => None,
1516+ }
1517+}
1518+
1519+// --------------------------------------------------------------- utilities
1520+
1521+fn takes_self(sig: &syn::Signature) -> bool {
1522+ matches!(sig.inputs.first(), Some(FnArg::Receiver(_)))
1523+}
1524+
1525+fn path_name(p: &syn::Path) -> String {
1526+ p.segments
1527+ .last()
1528+ .map(|s| s.ident.to_string())
1529+ .unwrap_or_default()
1530+}
1531+
1532+fn is_compound(op: &BinOp) -> bool {
1533+ matches!(
1534+ op,
1535+ BinOp::AddAssign(_)
1536+ | BinOp::SubAssign(_)
1537+ | BinOp::MulAssign(_)
1538+ | BinOp::DivAssign(_)
1539+ | BinOp::RemAssign(_)
1540+ | BinOp::BitAndAssign(_)
1541+ | BinOp::BitOrAssign(_)
1542+ | BinOp::BitXorAssign(_)
1543+ | BinOp::ShlAssign(_)
1544+ | BinOp::ShrAssign(_)
1545+ )
1546+}
1547+
1548+/// The Nim literal suffix for an integer type (`5'i32`).
1549+fn nim_suffix(t: &Nim) -> Result<&'static str, String> {
1550+ let Nim::Prim(p) = t else {
1551+ return Err("not a primitive integer".into());
1552+ };
1553+ Ok(match p.as_str() {
1554+ "int8" => "i8",
1555+ "int16" => "i16",
1556+ "int32" => "i32",
1557+ "int64" => "i64",
1558+ "int" => "i",
1559+ "uint8" => "u8",
1560+ "uint16" => "u16",
1561+ "uint32" => "u32",
1562+ "uint64" => "u64",
1563+ "uint" => "u",
1564+ other => return Err(format!("no Nim literal suffix for `{other}`")),
1565+ })
1566+}
1567+
1568+/// The unsigned integer type of the same width, used to spell `wrapping_*`.
1569+fn unsigned_peer(t: &Nim) -> Result<&'static str, String> {
1570+ let Nim::Prim(p) = t else {
1571+ return Err("not a primitive integer".into());
1572+ };
1573+ Ok(match p.as_str() {
1574+ "int8" => "uint8",
1575+ "int16" => "uint16",
1576+ "int32" => "uint32",
1577+ "int64" => "uint64",
1578+ "int" => "uint",
1579+ other => return Err(format!("`{other}` has no unsigned peer")),
1580+ })
1581+}
1582+
1583+fn item_kind(i: &Item) -> &'static str {
1584+ match i {
1585+ Item::Trait(_) => "`trait`",
1586+ Item::Enum(_) => "`enum`",
1587+ Item::Type(_) => "`type` alias",
1588+ Item::Static(_) => "`static`",
1589+ Item::Macro(_) => "macro definition",
1590+ Item::Union(_) => "`union`",
1591+ Item::ExternCrate(_) => "`extern crate`",
1592+ Item::ForeignMod(_) => "`extern` block",
1593+ _ => "item",
1594+ }
1595+}
1596+
1597+fn expr_kind(e: &Expr) -> &'static str {
1598+ match e {
1599+ Expr::Closure(_) => "closure",
1600+ Expr::Async(_) => "`async` block",
1601+ Expr::Await(_) => "`.await`",
1602+ Expr::Try(_) => "`?`",
1603+ Expr::Range(_) => "range",
1604+ Expr::Match(_) => "`match` (only statement position is implemented)",
1605+ Expr::Let(_) => "`let` expression",
1606+ Expr::Unsafe(_) => "`unsafe` block",
1607+ Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => "loop (has no value in Nim)",
1608+ _ => "expression",
1609+ }
1610+}
modified src/main.rs +67 -1
@@ -1,2 +1,68 @@
1+//! rustnim — Rust -> Nim transpiler.
2+//!
3+//! Usage: rustnim <input.rs> [-o <output.nim>]
4+
5+mod fmt;
6+mod lower;
17 mod ty;
2-fn main() { println!("rustnim: scaffold"); }
8+
9+use std::path::PathBuf;
10+use std::process::ExitCode;
11+
12+fn main() -> ExitCode {
13+ match run() {
14+ Ok(()) => ExitCode::SUCCESS,
15+ Err(e) => {
16+ eprintln!("rustnim: {e}");
17+ // Exiting non-zero on any failure is load-bearing: the entire
18+ // point of this project is that a transpiler must never report
19+ // success for work it did not do.
20+ ExitCode::FAILURE
21+ }
22+ }
23+}
24+
25+fn run() -> Result<(), String> {
26+ let mut args = std::env::args_os().skip(1);
27+ let mut input: Option<PathBuf> = None;
28+ let mut output: Option<PathBuf> = None;
29+
30+ while let Some(a) = args.next() {
31+ match a.to_string_lossy().as_ref() {
32+ "-o" | "--output" => {
33+ output = Some(
34+ args.next()
35+ .ok_or("`-o` needs a path")?
36+ .into(),
37+ );
38+ }
39+ "-h" | "--help" => {
40+ println!("usage: rustnim <input.rs> [-o <output.nim>]");
41+ return Ok(());
42+ }
43+ s if s.starts_with('-') => return Err(format!("unknown flag `{s}`")),
44+ _ => {
45+ if input.is_some() {
46+ return Err("more than one input file given".into());
47+ }
48+ input = Some(a.into());
49+ }
50+ }
51+ }
52+
53+ let input = input.ok_or("no input file; usage: rustnim <input.rs> [-o <output.nim>]")?;
54+ let src = std::fs::read_to_string(&input)
55+ .map_err(|e| format!("cannot read {}: {e}", input.display()))?;
56+
57+ let file: syn::File = syn::parse_file(&src)
58+ .map_err(|e| format!("{}: parse error: {e}", input.display()))?;
59+
60+ let nim = lower::Lowerer::new().lower_file(&file)?;
61+
62+ match output {
63+ Some(p) => std::fs::write(&p, nim)
64+ .map_err(|e| format!("cannot write {}: {e}", p.display()))?,
65+ None => print!("{nim}"),
66+ }
67+ Ok(())
68+}
@@ -1,2 +1,68 @@
1+//! rustnim — Rust -> Nim transpiler.
2+//!
3+//! Usage: rustnim <input.rs> [-o <output.nim>]
4+
5+mod fmt;
6+mod lower;
1 mod ty;7 mod ty;
2-fn main() { println!("rustnim: scaffold"); }8+
9+use std::path::PathBuf;
10+use std::process::ExitCode;
11+
12+fn main() -> ExitCode {
13+ match run() {
14+ Ok(()) => ExitCode::SUCCESS,
15+ Err(e) => {
16+ eprintln!("rustnim: {e}");
17+ // Exiting non-zero on any failure is load-bearing: the entire
18+ // point of this project is that a transpiler must never report
19+ // success for work it did not do.
20+ ExitCode::FAILURE
21+ }
22+ }
23+}
24+
25+fn run() -> Result<(), String> {
26+ let mut args = std::env::args_os().skip(1);
27+ let mut input: Option<PathBuf> = None;
28+ let mut output: Option<PathBuf> = None;
29+
30+ while let Some(a) = args.next() {
31+ match a.to_string_lossy().as_ref() {
32+ "-o" | "--output" => {
33+ output = Some(
34+ args.next()
35+ .ok_or("`-o` needs a path")?
36+ .into(),
37+ );
38+ }
39+ "-h" | "--help" => {
40+ println!("usage: rustnim <input.rs> [-o <output.nim>]");
41+ return Ok(());
42+ }
43+ s if s.starts_with('-') => return Err(format!("unknown flag `{s}`")),
44+ _ => {
45+ if input.is_some() {
46+ return Err("more than one input file given".into());
47+ }
48+ input = Some(a.into());
49+ }
50+ }
51+ }
52+
53+ let input = input.ok_or("no input file; usage: rustnim <input.rs> [-o <output.nim>]")?;
54+ let src = std::fs::read_to_string(&input)
55+ .map_err(|e| format!("cannot read {}: {e}", input.display()))?;
56+
57+ let file: syn::File = syn::parse_file(&src)
58+ .map_err(|e| format!("{}: parse error: {e}", input.display()))?;
59+
60+ let nim = lower::Lowerer::new().lower_file(&file)?;
61+
62+ match output {
63+ Some(p) => std::fs::write(&p, nim)
64+ .map_err(|e| format!("cannot write {}: {e}", p.display()))?,
65+ None => print!("{nim}"),
66+ }
67+ Ok(())
68+}
added src/prelude.nim +142 -0
new file mode 100644
@@ -0,0 +1,142 @@
1+## rustnim prelude — emitted at the top of every generated module.
2+##
3+## Everything here exists to make Nim's observable behaviour match Rust's
4+## exactly. Where the two languages already agree (signed `shr` is arithmetic
5+## in both; fixed-width unsigned arithmetic wraps in both; integer `div`/`mod`
6+## truncate toward zero in both) there is deliberately nothing here: the
7+## operator is mapped directly and no helper is involved.
8+
9+import std/[unicode, strutils]
10+
11+type
12+ RustPanic* = object of CatchableError
13+
14+ Option*[T] = object
15+ case has*: bool
16+ of true: val*: T
17+ of false: discard
18+
19+ Result*[T, E] = object
20+ case ok*: bool
21+ of true: val*: T
22+ of false: err*: E
23+
24+proc rsPanic*(msg: string) {.noreturn.} =
25+ raise newException(RustPanic, msg)
26+
27+proc rsSome*[T](v: T): Option[T] = Option[T](has: true, val: v)
28+proc rsNone*[T](): Option[T] = Option[T](has: false)
29+proc rsOk*[T, E](v: T): Result[T, E] = Result[T, E](ok: true, val: v)
30+proc rsErr*[T, E](e: E): Result[T, E] = Result[T, E](ok: false, err: e)
31+
32+proc unwrap*[T](o: Option[T]): T =
33+ if not o.has: rsPanic("called `Option::unwrap()` on a `None` value")
34+ o.val
35+proc unwrap*[T, E](r: Result[T, E]): T =
36+ if not r.ok: rsPanic("called `Result::unwrap()` on an `Err` value")
37+ r.val
38+
39+# ---------------------------------------------------------------------------
40+# Display / Debug.
41+#
42+# Rust's `{}` and `{:?}` are two distinct formats and they differ for floats,
43+# strings, chars and sequences. Nim's `$` matches neither consistently, so both
44+# are implemented here rather than approximated with `$`.
45+# ---------------------------------------------------------------------------
46+
47+proc rsDisplay*(x: SomeInteger): string = $x
48+proc rsDebug*(x: SomeInteger): string = $x
49+proc rsDisplay*(x: bool): string = $x
50+proc rsDebug*(x: bool): string = $x
51+
52+proc rsFloatStr(x: float64, debug: bool): string =
53+ ## Nim and Rust both print the shortest round-tripping decimal, but they
54+ ## spell the result differently in three places.
55+ if x != x: return "NaN"
56+ if x == Inf: return "inf"
57+ if x == -Inf: return "-inf"
58+ result = $x
59+ result = result.replace("e+", "e") # Nim `1e+21`, Rust `1e21`
60+ if not debug and result.endsWith(".0"): # Rust Display drops a bare `.0`
61+ result.setLen(result.len - 2)
62+
63+proc rsDisplay*(x: float32 | float64): string = rsFloatStr(float64(x), false)
64+proc rsDebug*(x: float32 | float64): string = rsFloatStr(float64(x), true)
65+
66+proc rsDisplay*(x: string): string = x
67+proc rsDebug*(x: string): string =
68+ result = "\""
69+ for c in x:
70+ case c
71+ of '"': result.add("\\\"")
72+ of '\\': result.add("\\\\")
73+ of '\n': result.add("\\n")
74+ of '\t': result.add("\\t")
75+ of '\r': result.add("\\r")
76+ else: result.add(c)
77+ result.add("\"")
78+
79+proc rsDisplay*(x: Rune): string = $x
80+proc rsDebug*(x: Rune): string =
81+ case $x
82+ of "'": "'\\''"
83+ of "\\": "'\\\\'"
84+ of "\n": "'\\n'"
85+ of "\t": "'\\t'"
86+ of "\r": "'\\r'"
87+ else: "'" & $x & "'"
88+
89+proc rsDebug*[T](x: seq[T] | openArray[T]): string =
90+ result = "["
91+ for i in 0 ..< x.len:
92+ if i > 0: result.add(", ")
93+ result.add(rsDebug(x[i]))
94+ result.add("]")
95+
96+proc rsDebug*[T](o: Option[T]): string =
97+ if o.has: "Some(" & rsDebug(o.val) & ")" else: "None"
98+proc rsDebug*[T, E](r: Result[T, E]): string =
99+ if r.ok: "Ok(" & rsDebug(r.val) & ")" else: "Err(" & rsDebug(r.err) & ")"
100+
101+# ---------------------------------------------------------------------------
102+# Radix formats: `{:x}`, `{:X}`, `{:b}`, `{:o}`, with Rust's width/zero-fill.
103+# Rust formats the *two's-complement bit pattern*, so a negative i8 prints as
104+# `ff`, not `-1`. `toHex` on the unsigned view of the same width reproduces it.
105+# ---------------------------------------------------------------------------
106+
107+proc rsRadix*[T: SomeInteger](x: T, base: int, upper: bool): string =
108+ var v: uint64 =
109+ when T is SomeSignedInt:
110+ # Sign-extend then mask to the type's own width, so the printed bit
111+ # pattern is the Rust one for this exact integer type.
112+ cast[uint64](int64(x)) and (if sizeof(T) == 8: high(uint64) else: (1'u64 shl (sizeof(T) * 8)) - 1)
113+ else:
114+ uint64(x)
115+ if v == 0: return "0"
116+ const digits = "0123456789abcdef"
117+ while v > 0'u64:
118+ result.add(digits[int(v mod uint64(base))])
119+ v = v div uint64(base)
120+ for i in 0 ..< result.len div 2:
121+ swap(result[i], result[result.len - 1 - i])
122+ if upper: result = result.toUpperAscii()
123+
124+proc rsPad*(s: string, width: int, zero: bool): string =
125+ if s.len >= width: return s
126+ let fill = width - s.len
127+ if zero and s.len > 0 and s[0] == '-':
128+ "-" & repeat('0', fill) & s[1 .. ^1]
129+ elif zero:
130+ repeat('0', fill) & s
131+ else:
132+ repeat(' ', fill) & s
133+
134+proc rsBytes*(s: string): seq[uint8] =
135+ ## Rust's `str::as_bytes` is a view of the UTF-8 encoding; Nim's `string` is
136+ ## already those bytes, so this is a reinterpretation, not a conversion.
137+ result = newSeq[uint8](s.len)
138+ for i in 0 ..< s.len: result[i] = uint8(s[i])
139+
140+proc newSeqWith*[T](n: int, v: T): seq[T] =
141+ result = newSeq[T](n)
142+ for i in 0 ..< n: result[i] = v
new file mode 100644
@@ -0,0 +1,142 @@
1+## rustnim prelude — emitted at the top of every generated module.
2+##
3+## Everything here exists to make Nim's observable behaviour match Rust's
4+## exactly. Where the two languages already agree (signed `shr` is arithmetic
5+## in both; fixed-width unsigned arithmetic wraps in both; integer `div`/`mod`
6+## truncate toward zero in both) there is deliberately nothing here: the
7+## operator is mapped directly and no helper is involved.
8+
9+import std/[unicode, strutils]
10+
11+type
12+ RustPanic* = object of CatchableError
13+
14+ Option*[T] = object
15+ case has*: bool
16+ of true: val*: T
17+ of false: discard
18+
19+ Result*[T, E] = object
20+ case ok*: bool
21+ of true: val*: T
22+ of false: err*: E
23+
24+proc rsPanic*(msg: string) {.noreturn.} =
25+ raise newException(RustPanic, msg)
26+
27+proc rsSome*[T](v: T): Option[T] = Option[T](has: true, val: v)
28+proc rsNone*[T](): Option[T] = Option[T](has: false)
29+proc rsOk*[T, E](v: T): Result[T, E] = Result[T, E](ok: true, val: v)
30+proc rsErr*[T, E](e: E): Result[T, E] = Result[T, E](ok: false, err: e)
31+
32+proc unwrap*[T](o: Option[T]): T =
33+ if not o.has: rsPanic("called `Option::unwrap()` on a `None` value")
34+ o.val
35+proc unwrap*[T, E](r: Result[T, E]): T =
36+ if not r.ok: rsPanic("called `Result::unwrap()` on an `Err` value")
37+ r.val
38+
39+# ---------------------------------------------------------------------------
40+# Display / Debug.
41+#
42+# Rust's `{}` and `{:?}` are two distinct formats and they differ for floats,
43+# strings, chars and sequences. Nim's `$` matches neither consistently, so both
44+# are implemented here rather than approximated with `$`.
45+# ---------------------------------------------------------------------------
46+
47+proc rsDisplay*(x: SomeInteger): string = $x
48+proc rsDebug*(x: SomeInteger): string = $x
49+proc rsDisplay*(x: bool): string = $x
50+proc rsDebug*(x: bool): string = $x
51+
52+proc rsFloatStr(x: float64, debug: bool): string =
53+ ## Nim and Rust both print the shortest round-tripping decimal, but they
54+ ## spell the result differently in three places.
55+ if x != x: return "NaN"
56+ if x == Inf: return "inf"
57+ if x == -Inf: return "-inf"
58+ result = $x
59+ result = result.replace("e+", "e") # Nim `1e+21`, Rust `1e21`
60+ if not debug and result.endsWith(".0"): # Rust Display drops a bare `.0`
61+ result.setLen(result.len - 2)
62+
63+proc rsDisplay*(x: float32 | float64): string = rsFloatStr(float64(x), false)
64+proc rsDebug*(x: float32 | float64): string = rsFloatStr(float64(x), true)
65+
66+proc rsDisplay*(x: string): string = x
67+proc rsDebug*(x: string): string =
68+ result = "\""
69+ for c in x:
70+ case c
71+ of '"': result.add("\\\"")
72+ of '\\': result.add("\\\\")
73+ of '\n': result.add("\\n")
74+ of '\t': result.add("\\t")
75+ of '\r': result.add("\\r")
76+ else: result.add(c)
77+ result.add("\"")
78+
79+proc rsDisplay*(x: Rune): string = $x
80+proc rsDebug*(x: Rune): string =
81+ case $x
82+ of "'": "'\\''"
83+ of "\\": "'\\\\'"
84+ of "\n": "'\\n'"
85+ of "\t": "'\\t'"
86+ of "\r": "'\\r'"
87+ else: "'" & $x & "'"
88+
89+proc rsDebug*[T](x: seq[T] | openArray[T]): string =
90+ result = "["
91+ for i in 0 ..< x.len:
92+ if i > 0: result.add(", ")
93+ result.add(rsDebug(x[i]))
94+ result.add("]")
95+
96+proc rsDebug*[T](o: Option[T]): string =
97+ if o.has: "Some(" & rsDebug(o.val) & ")" else: "None"
98+proc rsDebug*[T, E](r: Result[T, E]): string =
99+ if r.ok: "Ok(" & rsDebug(r.val) & ")" else: "Err(" & rsDebug(r.err) & ")"
100+
101+# ---------------------------------------------------------------------------
102+# Radix formats: `{:x}`, `{:X}`, `{:b}`, `{:o}`, with Rust's width/zero-fill.
103+# Rust formats the *two's-complement bit pattern*, so a negative i8 prints as
104+# `ff`, not `-1`. `toHex` on the unsigned view of the same width reproduces it.
105+# ---------------------------------------------------------------------------
106+
107+proc rsRadix*[T: SomeInteger](x: T, base: int, upper: bool): string =
108+ var v: uint64 =
109+ when T is SomeSignedInt:
110+ # Sign-extend then mask to the type's own width, so the printed bit
111+ # pattern is the Rust one for this exact integer type.
112+ cast[uint64](int64(x)) and (if sizeof(T) == 8: high(uint64) else: (1'u64 shl (sizeof(T) * 8)) - 1)
113+ else:
114+ uint64(x)
115+ if v == 0: return "0"
116+ const digits = "0123456789abcdef"
117+ while v > 0'u64:
118+ result.add(digits[int(v mod uint64(base))])
119+ v = v div uint64(base)
120+ for i in 0 ..< result.len div 2:
121+ swap(result[i], result[result.len - 1 - i])
122+ if upper: result = result.toUpperAscii()
123+
124+proc rsPad*(s: string, width: int, zero: bool): string =
125+ if s.len >= width: return s
126+ let fill = width - s.len
127+ if zero and s.len > 0 and s[0] == '-':
128+ "-" & repeat('0', fill) & s[1 .. ^1]
129+ elif zero:
130+ repeat('0', fill) & s
131+ else:
132+ repeat(' ', fill) & s
133+
134+proc rsBytes*(s: string): seq[uint8] =
135+ ## Rust's `str::as_bytes` is a view of the UTF-8 encoding; Nim's `string` is
136+ ## already those bytes, so this is a reinterpretation, not a conversion.
137+ result = newSeq[uint8](s.len)
138+ for i in 0 ..< s.len: result[i] = uint8(s[i])
139+
140+proc newSeqWith*[T](n: int, v: T): seq[T] =
141+ result = newSeq[T](n)
142+ for i in 0 ..< n: result[i] = v
added tests/cases/001-hello.rs +3 -0
new file mode 100644
@@ -0,0 +1,3 @@
1+fn main() {
2+ println!("hello, world");
3+}
new file mode 100644
@@ -0,0 +1,3 @@
1+fn main() {
2+ println!("hello, world");
3+}
added tests/cases/002-integer-widths.rs +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+// Exact widths must survive the trip. A transpiler that widened these would
2+// still print the right answer here, so the wrapping cases below are the
3+// ones that actually discriminate.
4+fn main() {
5+ let a: i8 = -128;
6+ let b: u8 = 255;
7+ let c: i16 = -32768;
8+ let d: u16 = 65535;
9+ let e: i32 = -2147483648;
10+ let f: u32 = 4294967295;
11+ let g: i64 = -9223372036854775808;
12+ let h: u64 = 18446744073709551615;
13+ println!("{} {} {} {} {} {} {} {}", a, b, c, d, e, f, g, h);
14+}
new file mode 100644
@@ -0,0 +1,14 @@
1+// Exact widths must survive the trip. A transpiler that widened these would
2+// still print the right answer here, so the wrapping cases below are the
3+// ones that actually discriminate.
4+fn main() {
5+ let a: i8 = -128;
6+ let b: u8 = 255;
7+ let c: i16 = -32768;
8+ let d: u16 = 65535;
9+ let e: i32 = -2147483648;
10+ let f: u32 = 4294967295;
11+ let g: i64 = -9223372036854775808;
12+ let h: u64 = 18446744073709551615;
13+ println!("{} {} {} {} {} {} {} {}", a, b, c, d, e, f, g, h);
14+}
added tests/cases/003-unsigned-wrapping.rs +10 -0
new file mode 100644
@@ -0,0 +1,10 @@
1+// DESIGN.md item 2: Nim's fixed-width unsigned arithmetic wraps silently,
2+// matching Rust's wrapping_*, so these map to bare operators.
3+fn main() {
4+ let a: u8 = 200;
5+ println!("{}", a.wrapping_add(100));
6+ println!("{}", a.wrapping_sub(250));
7+ println!("{}", a.wrapping_mul(3));
8+ let b: u32 = 4294967295;
9+ println!("{}", b.wrapping_add(2));
10+}
new file mode 100644
@@ -0,0 +1,10 @@
1+// DESIGN.md item 2: Nim's fixed-width unsigned arithmetic wraps silently,
2+// matching Rust's wrapping_*, so these map to bare operators.
3+fn main() {
4+ let a: u8 = 200;
5+ println!("{}", a.wrapping_add(100));
6+ println!("{}", a.wrapping_sub(250));
7+ println!("{}", a.wrapping_mul(3));
8+ let b: u32 = 4294967295;
9+ println!("{}", b.wrapping_add(2));
10+}
added tests/cases/004-signed-shr.rs +10 -0
new file mode 100644
@@ -0,0 +1,10 @@
1+// DESIGN.md item 1: Nim's `shr` on a signed integer is arithmetic, like Rust's
2+// `>>`. This is the operation base16ct's constant-time decoder depends on.
3+fn main() {
4+ let x: i16 = -256;
5+ println!("{}", x >> 8);
6+ println!("{}", (-1i32) >> 16);
7+ let y: u16 = 65280;
8+ println!("{}", y >> 8);
9+ println!("{}", 1i32 << 10);
10+}
new file mode 100644
@@ -0,0 +1,10 @@
1+// DESIGN.md item 1: Nim's `shr` on a signed integer is arithmetic, like Rust's
2+// `>>`. This is the operation base16ct's constant-time decoder depends on.
3+fn main() {
4+ let x: i16 = -256;
5+ println!("{}", x >> 8);
6+ println!("{}", (-1i32) >> 16);
7+ let y: u16 = 65280;
8+ println!("{}", y >> 8);
9+ println!("{}", 1i32 << 10);
10+}
added tests/cases/005-base16ct-decode-core.rs +18 -0
new file mode 100644
@@ -0,0 +1,18 @@
1+// The exact expression from base16ct 1.0.0 that the other transpiler's
2+// float64 universal AST cannot represent. Milestone 1 in miniature.
3+fn decode_nibble(src: u8) -> i16 {
4+ let byte = src as i16;
5+ let mut ret: i16 = -1;
6+ ret += (((0x2fi16 - byte) & (byte - 0x3a)) >> 8) & (byte - 47);
7+ ret += (((0x60i16 - byte) & (byte - 0x67)) >> 8) & (byte - 86);
8+ ret
9+}
10+
11+fn main() {
12+ let mut i: u8 = 0;
13+ while i < 128 {
14+ print!("{} ", decode_nibble(i));
15+ i += 1;
16+ }
17+ println!("");
18+}
new file mode 100644
@@ -0,0 +1,18 @@
1+// The exact expression from base16ct 1.0.0 that the other transpiler's
2+// float64 universal AST cannot represent. Milestone 1 in miniature.
3+fn decode_nibble(src: u8) -> i16 {
4+ let byte = src as i16;
5+ let mut ret: i16 = -1;
6+ ret += (((0x2fi16 - byte) & (byte - 0x3a)) >> 8) & (byte - 47);
7+ ret += (((0x60i16 - byte) & (byte - 0x67)) >> 8) & (byte - 86);
8+ ret
9+}
10+
11+fn main() {
12+ let mut i: u8 = 0;
13+ while i < 128 {
14+ print!("{} ", decode_nibble(i));
15+ i += 1;
16+ }
17+ println!("");
18+}
added tests/cases/006-control-flow.rs +28 -0
new file mode 100644
@@ -0,0 +1,28 @@
1+fn classify(n: i32) -> i32 {
2+ if n < 0 {
3+ -1
4+ } else if n == 0 {
5+ 0
6+ } else {
7+ 1
8+ }
9+}
10+
11+fn main() {
12+ let mut total: i32 = 0;
13+ for i in -3..4 {
14+ total += classify(i);
15+ println!("{} -> {}", i, classify(i));
16+ }
17+ let mut k: i32 = 0;
18+ while k < 3 {
19+ k += 1;
20+ }
21+ loop {
22+ k += 1;
23+ if k > 5 {
24+ break;
25+ }
26+ }
27+ println!("total={} k={}", total, k);
28+}
new file mode 100644
@@ -0,0 +1,28 @@
1+fn classify(n: i32) -> i32 {
2+ if n < 0 {
3+ -1
4+ } else if n == 0 {
5+ 0
6+ } else {
7+ 1
8+ }
9+}
10+
11+fn main() {
12+ let mut total: i32 = 0;
13+ for i in -3..4 {
14+ total += classify(i);
15+ println!("{} -> {}", i, classify(i));
16+ }
17+ let mut k: i32 = 0;
18+ while k < 3 {
19+ k += 1;
20+ }
21+ loop {
22+ k += 1;
23+ if k > 5 {
24+ break;
25+ }
26+ }
27+ println!("total={} k={}", total, k);
28+}
added tests/cases/007-format-specs.rs +11 -0
new file mode 100644
@@ -0,0 +1,11 @@
1+fn main() {
2+ println!("{:x} {:X} {:b} {:o}", 255u32, 255u32, 5u8, 64u16);
3+ println!("{:02x}{:02x}", 1u8, 254u8);
4+ println!("{:08b}", 5u8);
5+ println!("[{:5}]", 42i32);
6+ println!("{0} {1} {0}", 1i32, 2i32);
7+ let name: i32 = 7;
8+ println!("inline {name}");
9+ println!("{{literal}}");
10+ println!("{:?} {:?}", "quoted\n", true);
11+}
new file mode 100644
@@ -0,0 +1,11 @@
1+fn main() {
2+ println!("{:x} {:X} {:b} {:o}", 255u32, 255u32, 5u8, 64u16);
3+ println!("{:02x}{:02x}", 1u8, 254u8);
4+ println!("{:08b}", 5u8);
5+ println!("[{:5}]", 42i32);
6+ println!("{0} {1} {0}", 1i32, 2i32);
7+ let name: i32 = 7;
8+ println!("inline {name}");
9+ println!("{{literal}}");
10+ println!("{:?} {:?}", "quoted\n", true);
11+}
added tests/cases/008-truncating-cast.rs +11 -0
new file mode 100644
@@ -0,0 +1,11 @@
1+// Rust's `as` truncates; Nim's `T(x)` range-checks. The lowering uses `cast`,
2+// which was probed against both compilers.
3+fn main() {
4+ println!("{}", 300i32 as u8);
5+ println!("{}", -1i32 as u32);
6+ println!("{}", 200u8 as i8);
7+ println!("{}", 65535u16 as i16);
8+ println!("{}", -1i64 as u8);
9+ println!("{}", 65i32 as f64);
10+ println!("{}", true as i32);
11+}
new file mode 100644
@@ -0,0 +1,11 @@
1+// Rust's `as` truncates; Nim's `T(x)` range-checks. The lowering uses `cast`,
2+// which was probed against both compilers.
3+fn main() {
4+ println!("{}", 300i32 as u8);
5+ println!("{}", -1i32 as u32);
6+ println!("{}", 200u8 as i8);
7+ println!("{}", 65535u16 as i16);
8+ println!("{}", -1i64 as u8);
9+ println!("{}", 65i32 as f64);
10+ println!("{}", true as i32);
11+}
added tests/cases/009-div-mod-negative.rs +6 -0
new file mode 100644
@@ -0,0 +1,6 @@
1+// Both languages truncate toward zero. `/` must become `div`, not `/`.
2+fn main() {
3+ println!("{} {}", -7i32 / 2, -7i32 % 2);
4+ println!("{} {}", 7i32 / -2, 7i32 % -2);
5+ println!("{}", 7.0f64 / 2.0f64);
6+}
new file mode 100644
@@ -0,0 +1,6 @@
1+// Both languages truncate toward zero. `/` must become `div`, not `/`.
2+fn main() {
3+ println!("{} {}", -7i32 / 2, -7i32 % 2);
4+ println!("{} {}", 7i32 / -2, 7i32 % -2);
5+ println!("{}", 7.0f64 / 2.0f64);
6+}
added tests/cases/010-structs-and-methods.rs +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+struct Counter {
2+ value: i32,
3+ step: i32,
4+}
5+
6+impl Counter {
7+ fn bump(&mut self) {
8+ self.value += self.step;
9+ }
10+ fn get(&self) -> i32 {
11+ self.value
12+ }
13+}
14+
15+fn main() {
16+ let mut c = Counter { value: 0, step: 3 };
17+ c.bump();
18+ c.bump();
19+ println!("{}", c.get());
20+}
new file mode 100644
@@ -0,0 +1,20 @@
1+struct Counter {
2+ value: i32,
3+ step: i32,
4+}
5+
6+impl Counter {
7+ fn bump(&mut self) {
8+ self.value += self.step;
9+ }
10+ fn get(&self) -> i32 {
11+ self.value
12+ }
13+}
14+
15+fn main() {
16+ let mut c = Counter { value: 0, step: 3 };
17+ c.bump();
18+ c.bump();
19+ println!("{}", c.get());
20+}
added tests/cases/011-slices-and-vec.rs +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+fn sum(xs: &[i32]) -> i32 {
2+ let mut t: i32 = 0;
3+ for x in xs {
4+ t += x;
5+ }
6+ t
7+}
8+
9+fn main() {
10+ let v: Vec<i32> = vec![1, 2, 3, 4];
11+ println!("{}", sum(&v));
12+ println!("{}", v.len());
13+ println!("{}", v[2]);
14+ println!("{:?}", v);
15+ let mut w: Vec<i32> = vec![];
16+ w.push(9);
17+ w.push(8);
18+ println!("{:?} {}", w, w.len());
19+}
new file mode 100644
@@ -0,0 +1,19 @@
1+fn sum(xs: &[i32]) -> i32 {
2+ let mut t: i32 = 0;
3+ for x in xs {
4+ t += x;
5+ }
6+ t
7+}
8+
9+fn main() {
10+ let v: Vec<i32> = vec![1, 2, 3, 4];
11+ println!("{}", sum(&v));
12+ println!("{}", v.len());
13+ println!("{}", v[2]);
14+ println!("{:?}", v);
15+ let mut w: Vec<i32> = vec![];
16+ w.push(9);
17+ w.push(8);
18+ println!("{:?} {}", w, w.len());
19+}
added tests/cases/012-match.rs +16 -0
new file mode 100644
@@ -0,0 +1,16 @@
1+fn name(n: u8) -> i32 {
2+ match n {
3+ 0 => 100,
4+ 1 | 2 => 200,
5+ 3..=5 => 300,
6+ _ => 400,
7+ }
8+}
9+
10+fn main() {
11+ let mut i: u8 = 0;
12+ while i < 8 {
13+ println!("{} {}", i, name(i));
14+ i += 1;
15+ }
16+}
new file mode 100644
@@ -0,0 +1,16 @@
1+fn name(n: u8) -> i32 {
2+ match n {
3+ 0 => 100,
4+ 1 | 2 => 200,
5+ 3..=5 => 300,
6+ _ => 400,
7+ }
8+}
9+
10+fn main() {
11+ let mut i: u8 = 0;
12+ while i < 8 {
13+ println!("{} {}", i, name(i));
14+ i += 1;
15+ }
16+}
added tests/cases/013-floats.rs +8 -0
new file mode 100644
@@ -0,0 +1,8 @@
1+// Rust's Display drops a bare `.0`; Debug keeps it. Nim's `$` does neither
2+// consistently, so the prelude implements both.
3+fn main() {
4+ println!("{} {:?}", 1.0f64, 1.0f64);
5+ println!("{} {:?}", 1.5f64, 1.5f64);
6+ println!("{}", 0.1f64 + 0.2f64);
7+ println!("{} {}", -0.5f64, 100.0f64);
8+}
new file mode 100644
@@ -0,0 +1,8 @@
1+// Rust's Display drops a bare `.0`; Debug keeps it. Nim's `$` does neither
2+// consistently, so the prelude implements both.
3+fn main() {
4+ println!("{} {:?}", 1.0f64, 1.0f64);
5+ println!("{} {:?}", 1.5f64, 1.5f64);
6+ println!("{}", 0.1f64 + 0.2f64);
7+ println!("{} {}", -0.5f64, 100.0f64);
8+}
added tests/cases/014-chars.rs +10 -0
new file mode 100644
@@ -0,0 +1,10 @@
1+// DESIGN.md open question 4: Rust `char` is a Unicode scalar, mapped to Nim's
2+// `Rune`. This checks the round trip, including a non-ASCII scalar.
3+fn main() {
4+ let a: char = 'a';
5+ let z: char = 'ζ';
6+ println!("{} {}", a, z);
7+ println!("{:?} {:?}", a, z);
8+ println!("{} {}", a as u32, z as u32);
9+ println!("{}", 955u32 as u8);
10+}
new file mode 100644
@@ -0,0 +1,10 @@
1+// DESIGN.md open question 4: Rust `char` is a Unicode scalar, mapped to Nim's
2+// `Rune`. This checks the round trip, including a non-ASCII scalar.
3+fn main() {
4+ let a: char = 'a';
5+ let z: char = 'ζ';
6+ println!("{} {}", a, z);
7+ println!("{:?} {:?}", a, z);
8+ println!("{} {}", a as u32, z as u32);
9+ println!("{}", 955u32 as u8);
10+}
added tests/cases/015-panic-exit-code.rs +6 -0
new file mode 100644
@@ -0,0 +1,6 @@
1+// A Rust panic exits 101. Nim's Defects exit 1, so the generated module maps
2+// them; without that the runner's exit-status comparison would be vacuous.
3+fn main() {
4+ println!("before");
5+ panic!("boom");
6+}
new file mode 100644
@@ -0,0 +1,6 @@
1+// A Rust panic exits 101. Nim's Defects exit 1, so the generated module maps
2+// them; without that the runner's exit-status comparison would be vacuous.
3+fn main() {
4+ println!("before");
5+ panic!("boom");
6+}
added tests/cases/016-signed-overflow-traps.rs +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+// DESIGN.md item 3, settled: this project models rustc's *debug* profile,
2+// where signed overflow panics. Nim's default build traps it too, so the two
3+// agree — including on the exit code.
4+fn main() {
5+ let mut x: i32 = 2147483647;
6+ println!("{}", x);
7+ x += 1;
8+ println!("unreachable {}", x);
9+}
new file mode 100644
@@ -0,0 +1,9 @@
1+// DESIGN.md item 3, settled: this project models rustc's *debug* profile,
2+// where signed overflow panics. Nim's default build traps it too, so the two
3+// agree — including on the exit code.
4+fn main() {
5+ let mut x: i32 = 2147483647;
6+ println!("{}", x);
7+ x += 1;
8+ println!("unreachable {}", x);
9+}
added tests/cases/900-reject-i128.rs +6 -0
new file mode 100644
@@ -0,0 +1,6 @@
1+//@ reject: 128-bit integers have no faithful Nim equivalent
2+// The rule that this project exists to enforce: no silent approximation.
3+fn main() {
4+ let x: i128 = 1;
5+ println!("{}", x);
6+}
new file mode 100644
@@ -0,0 +1,6 @@
1+//@ reject: 128-bit integers have no faithful Nim equivalent
2+// The rule that this project exists to enforce: no silent approximation.
3+fn main() {
4+ let x: i128 = 1;
5+ println!("{}", x);
6+}
added tests/cases/901-reject-unknown-method.rs +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+//@ reject: unsupported method `.sort()`
2+// A standard-library method with no verified Nim equivalent must be rejected,
3+// not guessed at. Rust's `sort` is stable and Nim's `sort` is not, so an
4+// unthinking `.sort` mapping would differ on equal keys.
5+fn main() {
6+ let mut v: Vec<i32> = vec![3, 1, 2];
7+ v.sort();
8+ println!("{:?}", v);
9+}
new file mode 100644
@@ -0,0 +1,9 @@
1+//@ reject: unsupported method `.sort()`
2+// A standard-library method with no verified Nim equivalent must be rejected,
3+// not guessed at. Rust's `sort` is stable and Nim's `sort` is not, so an
4+// unthinking `.sort` mapping would differ on equal keys.
5+fn main() {
6+ let mut v: Vec<i32> = vec![3, 1, 2];
7+ v.sort();
8+ println!("{:?}", v);
9+}
added tests/cases/902-reject-float-to-int-cast.rs +6 -0
new file mode 100644
@@ -0,0 +1,6 @@
1+//@ reject: Rust saturates, Nim rounds
2+// Rust saturates float->int casts, Nim rounds and range-checks. Different
3+// operations, so no mapping is emitted.
4+fn main() {
5+ println!("{}", 1e10f64 as i32);
6+}
new file mode 100644
@@ -0,0 +1,6 @@
1+//@ reject: Rust saturates, Nim rounds
2+// Rust saturates float->int casts, Nim rounds and range-checks. Different
3+// operations, so no mapping is emitted.
4+fn main() {
5+ println!("{}", 1e10f64 as i32);
6+}
added tests/cases/903-reject-unsupported-format.rs +4 -0
new file mode 100644
@@ -0,0 +1,4 @@
1+//@ reject: unsupported format spec
2+fn main() {
3+ println!("{:>8}", 1i32);
4+}
new file mode 100644
@@ -0,0 +1,4 @@
1+//@ reject: unsupported format spec
2+fn main() {
3+ println!("{:>8}", 1i32);
4+}
added tests/cases/904-reject-closure.rs +5 -0
new file mode 100644
@@ -0,0 +1,5 @@
1+//@ reject: unsupported expression in value position: closure
2+fn main() {
3+ let f = |x: i32| x + 1;
4+ println!("{}", f(1));
5+}
new file mode 100644
@@ -0,0 +1,5 @@
1+//@ reject: unsupported expression in value position: closure
2+fn main() {
3+ let f = |x: i32| x + 1;
4+ println!("{}", f(1));
5+}
added tests/differential.rs +395 -0
new file mode 100644
@@ -0,0 +1,395 @@
1+//! Differential test runner: rustc is the oracle.
2+//!
3+//! For each `tests/cases/*.rs`:
4+//!
5+//! ```text
6+//! rustc case.rs -o a && ./a -> (stdout_rs, status_rs)
7+//! rustnim case.rs -o case.nim
8+//! nim c case.nim -o b && ./b -> (stdout_nim, status_nim)
9+//! assert stdout_rs == stdout_nim && status_rs == status_nim
10+//! ```
11+//!
12+//! A case passes only when *both* binaries build and produce identical output.
13+//! Every intermediate stage is checked explicitly, because the failure mode
14+//! this project exists to avoid is a transpiler reporting success while
15+//! emitting nothing: `rustnim` exiting 0 with an empty or Nim-unparseable
16+//! output file is a hard failure here, not a silent pass.
17+//!
18+//! Profile: rustc is invoked *without* `-O`, so debug-profile integer overflow
19+//! checks are on. Nim's default `nim c` also has overflow checks on. That is
20+//! the matching pair, and it is the profile this project models.
21+//!
22+//! Directives, recognised in `//@ ...` comments at the top of a case:
23+//!
24+//! //@ reject: <substring> rustnim must fail, with <substring> in stderr.
25+//! (The "fail loudly" rule, tested.)
26+//! //@ skip: <reason> Not run; reported as skipped.
27+//! //@ args: <argv> Passed to both binaries.
28+//! //@ stdin: <line> Fed to both binaries on stdin.
29+
30+use std::collections::BTreeMap;
31+use std::fmt::Write as _;
32+use std::fs;
33+use std::io::Write as _;
34+use std::path::{Path, PathBuf};
35+use std::process::{Command, Stdio};
36+
37+const RUSTNIM: &str = env!("CARGO_BIN_EXE_rustnim");
38+
39+// ---------------------------------------------------------------- outcomes
40+
41+#[derive(Debug)]
42+enum Outcome {
43+ Pass,
44+ Skip(String),
45+ /// A stage failed. `stage` is where, `detail` is the evidence.
46+ Fail { stage: &'static str, detail: String },
47+}
48+
49+impl Outcome {
50+ fn fail(stage: &'static str, detail: impl Into<String>) -> Self {
51+ Outcome::Fail { stage, detail: detail.into() }
52+ }
53+}
54+
55+struct Run {
56+ status: Option<i32>,
57+ stdout: Vec<u8>,
58+ stderr: String,
59+}
60+
61+impl Run {
62+ fn ok(&self) -> bool {
63+ self.status == Some(0)
64+ }
65+}
66+
67+// ------------------------------------------------------------------ helpers
68+
69+fn run(cmd: &mut Command, stdin: Option<&str>) -> Result<Run, String> {
70+ cmd.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() })
71+ .stdout(Stdio::piped())
72+ .stderr(Stdio::piped());
73+ let mut child = cmd.spawn().map_err(|e| format!("spawn {:?}: {e}", cmd.get_program()))?;
74+ if let Some(s) = stdin {
75+ child.stdin.as_mut().unwrap().write_all(s.as_bytes()).map_err(|e| e.to_string())?;
76+ }
77+ let out = child.wait_with_output().map_err(|e| e.to_string())?;
78+ Ok(Run {
79+ status: out.status.code(),
80+ stdout: out.stdout,
81+ stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
82+ })
83+}
84+
85+/// Locate the vendored Nim. It lives at the *repository* root, which is not
86+/// the manifest dir when running inside a git worktree, so walk upwards.
87+fn find_nim() -> Result<PathBuf, String> {
88+ if let Ok(p) = std::env::var("RUSTNIM_NIM") {
89+ let p = PathBuf::from(p);
90+ if p.is_file() {
91+ return Ok(p);
92+ }
93+ return Err(format!("RUSTNIM_NIM={} is not a file", p.display()));
94+ }
95+ let mut dir: Option<&Path> = Some(Path::new(env!("CARGO_MANIFEST_DIR")));
96+ while let Some(d) = dir {
97+ let cand = d.join(".nim-toolchain/bin/nim");
98+ if cand.is_file() {
99+ return Ok(cand);
100+ }
101+ dir = d.parent();
102+ }
103+ Err("no .nim-toolchain/bin/nim found in this directory or any parent; \
104+ set RUSTNIM_NIM to the nim binary"
105+ .into())
106+}
107+
108+#[derive(Default)]
109+struct Directives {
110+ reject: Option<String>,
111+ skip: Option<String>,
112+ args: Vec<String>,
113+ stdin: Option<String>,
114+}
115+
116+fn directives(src: &str) -> Directives {
117+ let mut d = Directives::default();
118+ for line in src.lines() {
119+ let line = line.trim();
120+ let Some(rest) = line.strip_prefix("//@") else {
121+ // Directives must be in the leading comment block; stop at the
122+ // first line of real code so a `//@` inside a string can't count.
123+ if line.is_empty() || line.starts_with("//") || line.starts_with("#!") {
124+ continue;
125+ }
126+ break;
127+ };
128+ let rest = rest.trim();
129+ let (key, val) = match rest.split_once(':') {
130+ Some((k, v)) => (k.trim(), v.trim().to_string()),
131+ None => (rest, String::new()),
132+ };
133+ match key {
134+ "reject" => d.reject = Some(val),
135+ "skip" => d.skip = Some(val),
136+ "args" => d.args = val.split_whitespace().map(str::to_string).collect(),
137+ "stdin" => d.stdin = Some(format!("{val}\n")),
138+ _ => {}
139+ }
140+ }
141+ d
142+}
143+
144+/// Byte-for-byte diff, rendered readably: show the first differing line with
145+/// escapes, so a trailing-newline or whitespace difference is visible.
146+fn diff_report(want: &[u8], got: &[u8]) -> String {
147+ let w = String::from_utf8_lossy(want);
148+ let g = String::from_utf8_lossy(got);
149+ let (wl, gl): (Vec<_>, Vec<_>) = (w.lines().collect(), g.lines().collect());
150+ let mut out = String::new();
151+ for i in 0..wl.len().max(gl.len()) {
152+ let (a, b) = (wl.get(i), gl.get(i));
153+ if a != b {
154+ let _ = writeln!(out, " first difference at line {}:", i + 1);
155+ let _ = writeln!(out, " rustc: {}", a.map(|s| format!("{s:?}")).unwrap_or("<eof>".into()));
156+ let _ = writeln!(out, " nim : {}", b.map(|s| format!("{s:?}")).unwrap_or("<eof>".into()));
157+ break;
158+ }
159+ }
160+ if out.is_empty() {
161+ // Lines all matched, so the difference is in trailing bytes.
162+ let _ = writeln!(out, " lines match; raw bytes differ (trailing newline?)");
163+ let _ = writeln!(out, " rustc: {:?}", w);
164+ let _ = writeln!(out, " nim : {:?}", g);
165+ }
166+ let _ = writeln!(out, " ({} bytes from rustc, {} from nim)", want.len(), got.len());
167+ out
168+}
169+
170+fn tail(s: &str, lines: usize) -> String {
171+ let v: Vec<&str> = s.trim_end().lines().collect();
172+ let start = v.len().saturating_sub(lines);
173+ v[start..]
174+ .iter()
175+ .map(|l| format!(" {l}"))
176+ .collect::<Vec<_>>()
177+ .join("\n")
178+}
179+
180+// --------------------------------------------------------------- one case
181+
182+fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome {
183+ let name = case.file_stem().unwrap().to_string_lossy().into_owned();
184+ let src = match fs::read_to_string(case) {
185+ Ok(s) => s,
186+ Err(e) => return Outcome::fail("read", e.to_string()),
187+ };
188+ let d = directives(&src);
189+ if let Some(why) = d.skip {
190+ return Outcome::Skip(why);
191+ }
192+
193+ let dir = work.join(&name);
194+ let _ = fs::remove_dir_all(&dir);
195+ if let Err(e) = fs::create_dir_all(&dir) {
196+ return Outcome::fail("setup", e.to_string());
197+ }
198+
199+ // -- stage: transpile. Checked first, and checked strictly: exit status,
200+ // stderr on failure, and that the file actually has content in it.
201+ // Nim module names must be valid Nim identifiers, which case names
202+ // (`005-base16ct-decode-core`) are not.
203+ let mod_name: String = format!(
204+ "c_{}",
205+ name.chars().map(|c| if c.is_alphanumeric() { c } else { '_' }).collect::<String>()
206+ );
207+ let nim_src = dir.join(format!("{mod_name}.nim"));
208+ let transpile = match run(
209+ Command::new(RUSTNIM).arg(case).arg("-o").arg(&nim_src).env("TMPDIR", &dir),
210+ None,
211+ ) {
212+ Ok(r) => r,
213+ Err(e) => return Outcome::fail("rustnim", e),
214+ };
215+
216+ if let Some(want) = &d.reject {
217+ // A `reject` case asserts the "fail loudly" rule.
218+ if transpile.ok() {
219+ return Outcome::fail(
220+ "rustnim",
221+ format!("expected rejection containing {want:?}, but transpile succeeded"),
222+ );
223+ }
224+ if !transpile.stderr.contains(want.as_str()) {
225+ return Outcome::fail(
226+ "rustnim",
227+ format!("rejected, but message lacks {want:?}:\n{}", tail(&transpile.stderr, 10)),
228+ );
229+ }
230+ return Outcome::Pass;
231+ }
232+
233+ if !transpile.ok() {
234+ return Outcome::fail(
235+ "rustnim",
236+ format!("exit {:?}\n{}", transpile.status, tail(&transpile.stderr, 20)),
237+ );
238+ }
239+ match fs::metadata(&nim_src) {
240+ Err(_) => {
241+ return Outcome::fail("rustnim", "exited 0 but wrote no output file".to_string());
242+ }
243+ Ok(m) if m.len() == 0 => {
244+ return Outcome::fail("rustnim", "exited 0 but wrote an empty output file".to_string());
245+ }
246+ Ok(_) => {}
247+ }
248+
249+ // -- stage: rustc. No -O: debug profile, overflow checks on.
250+ let rs_bin = dir.join("rs.bin");
251+ let rc = match run(
252+ Command::new("rustc")
253+ .arg("--edition=2021")
254+ .arg("-A").arg("warnings")
255+ .arg(case)
256+ .arg("-o").arg(&rs_bin)
257+ .env("TMPDIR", &dir),
258+ None,
259+ ) {
260+ Ok(r) => r,
261+ Err(e) => return Outcome::fail("rustc", e),
262+ };
263+ if !rc.ok() {
264+ return Outcome::fail("rustc", format!("case does not compile as Rust:\n{}", tail(&rc.stderr, 20)));
265+ }
266+
267+ // -- stage: nim c
268+ let nim_bin = dir.join("nim.bin");
269+ let nc = match run(
270+ Command::new(nim)
271+ .arg("c")
272+ .arg("--hints:off")
273+ .arg("--warnings:off")
274+ .arg("--colors:off")
275+ .arg(format!("--nimcache:{}", dir.join("nimcache").display()))
276+ .arg(format!("-o:{}", nim_bin.display()))
277+ .arg(&nim_src)
278+ .env("TMPDIR", &dir),
279+ None,
280+ ) {
281+ Ok(r) => r,
282+ Err(e) => return Outcome::fail("nim", e),
283+ };
284+ if !nc.ok() {
285+ // Nim reports compile errors on stdout.
286+ let msg = if nc.stderr.trim().is_empty() {
287+ String::from_utf8_lossy(&nc.stdout).into_owned()
288+ } else {
289+ nc.stderr.clone()
290+ };
291+ return Outcome::fail(
292+ "nim",
293+ format!("generated Nim does not compile:\n{}\n --- generated ---\n{}", tail(&msg, 20), numbered(&nim_src)),
294+ );
295+ }
296+
297+ // -- stage: execute both
298+ let exec = |bin: &Path| {
299+ let mut c = Command::new(bin);
300+ c.args(&d.args).env("TMPDIR", &dir);
301+ run(&mut c, d.stdin.as_deref())
302+ };
303+ let (a, b) = match (exec(&rs_bin), exec(&nim_bin)) {
304+ (Ok(a), Ok(b)) => (a, b),
305+ (Err(e), _) | (_, Err(e)) => return Outcome::fail("run", e),
306+ };
307+
308+ if a.stdout != b.stdout {
309+ return Outcome::fail("diff", diff_report(&a.stdout, &b.stdout));
310+ }
311+ if a.status != b.status {
312+ return Outcome::fail(
313+ "diff",
314+ format!(
315+ "stdout matches but exit status differs: rustc {:?}, nim {:?}\n nim stderr:\n{}",
316+ a.status,
317+ b.status,
318+ tail(&b.stderr, 10)
319+ ),
320+ );
321+ }
322+ Outcome::Pass
323+}
324+
325+fn numbered(p: &Path) -> String {
326+ fs::read_to_string(p)
327+ .unwrap_or_default()
328+ .lines()
329+ .enumerate()
330+ .map(|(i, l)| format!(" {:>3} | {l}", i + 1))
331+ .collect::<Vec<_>>()
332+ .join("\n")
333+}
334+
335+// ------------------------------------------------------------------ driver
336+
337+#[test]
338+fn differential() {
339+ let root = Path::new(env!("CARGO_MANIFEST_DIR"));
340+ let cases_dir = root.join("tests/cases");
341+ let work = root.join("tests/.work");
342+ let _ = fs::create_dir_all(&work);
343+
344+ let nim = match find_nim() {
345+ Ok(n) => n,
346+ Err(e) => panic!("cannot locate Nim: {e}"),
347+ };
348+
349+ let mut cases: Vec<PathBuf> = fs::read_dir(&cases_dir)
350+ .unwrap_or_else(|e| panic!("no {}: {e}", cases_dir.display()))
351+ .filter_map(|e| e.ok().map(|e| e.path()))
352+ .filter(|p| p.extension().is_some_and(|x| x == "rs"))
353+ .collect();
354+ cases.sort();
355+
356+ // An empty corpus must not read as success. That is the exact failure this
357+ // runner exists to catch, and it applies to the runner itself.
358+ assert!(!cases.is_empty(), "no cases in {}", cases_dir.display());
359+
360+ let filter = std::env::var("RUSTNIM_CASE").ok();
361+ let mut results: BTreeMap<String, Outcome> = BTreeMap::new();
362+ for case in &cases {
363+ let name = case.file_stem().unwrap().to_string_lossy().into_owned();
364+ if let Some(f) = &filter {
365+ if !name.contains(f.as_str()) {
366+ continue;
367+ }
368+ }
369+ let outcome = run_case(case, &work, &nim);
370+ match &outcome {
371+ Outcome::Pass => eprintln!("ok {name}"),
372+ Outcome::Skip(why) => eprintln!("skip {name} ({why})"),
373+ Outcome::Fail { .. } => eprintln!("FAIL {name}"),
374+ }
375+ results.insert(name, outcome);
376+ }
377+
378+ let mut failed = Vec::new();
379+ let (mut pass, mut skip) = (0, 0);
380+ for (name, o) in &results {
381+ match o {
382+ Outcome::Pass => pass += 1,
383+ Outcome::Skip(_) => skip += 1,
384+ Outcome::Fail { stage, detail } => failed.push(format!(
385+ "\n--- {name}: failed at stage `{stage}`\n{}",
386+ detail.trim_end()
387+ )),
388+ }
389+ }
390+
391+ eprintln!("\n{pass} passed, {} failed, {skip} skipped", failed.len());
392+ if !failed.is_empty() {
393+ panic!("{}", failed.join("\n"));
394+ }
395+}
new file mode 100644
@@ -0,0 +1,395 @@
1+//! Differential test runner: rustc is the oracle.
2+//!
3+//! For each `tests/cases/*.rs`:
4+//!
5+//! ```text
6+//! rustc case.rs -o a && ./a -> (stdout_rs, status_rs)
7+//! rustnim case.rs -o case.nim
8+//! nim c case.nim -o b && ./b -> (stdout_nim, status_nim)
9+//! assert stdout_rs == stdout_nim && status_rs == status_nim
10+//! ```
11+//!
12+//! A case passes only when *both* binaries build and produce identical output.
13+//! Every intermediate stage is checked explicitly, because the failure mode
14+//! this project exists to avoid is a transpiler reporting success while
15+//! emitting nothing: `rustnim` exiting 0 with an empty or Nim-unparseable
16+//! output file is a hard failure here, not a silent pass.
17+//!
18+//! Profile: rustc is invoked *without* `-O`, so debug-profile integer overflow
19+//! checks are on. Nim's default `nim c` also has overflow checks on. That is
20+//! the matching pair, and it is the profile this project models.
21+//!
22+//! Directives, recognised in `//@ ...` comments at the top of a case:
23+//!
24+//! //@ reject: <substring> rustnim must fail, with <substring> in stderr.
25+//! (The "fail loudly" rule, tested.)
26+//! //@ skip: <reason> Not run; reported as skipped.
27+//! //@ args: <argv> Passed to both binaries.
28+//! //@ stdin: <line> Fed to both binaries on stdin.
29+
30+use std::collections::BTreeMap;
31+use std::fmt::Write as _;
32+use std::fs;
33+use std::io::Write as _;
34+use std::path::{Path, PathBuf};
35+use std::process::{Command, Stdio};
36+
37+const RUSTNIM: &str = env!("CARGO_BIN_EXE_rustnim");
38+
39+// ---------------------------------------------------------------- outcomes
40+
41+#[derive(Debug)]
42+enum Outcome {
43+ Pass,
44+ Skip(String),
45+ /// A stage failed. `stage` is where, `detail` is the evidence.
46+ Fail { stage: &'static str, detail: String },
47+}
48+
49+impl Outcome {
50+ fn fail(stage: &'static str, detail: impl Into<String>) -> Self {
51+ Outcome::Fail { stage, detail: detail.into() }
52+ }
53+}
54+
55+struct Run {
56+ status: Option<i32>,
57+ stdout: Vec<u8>,
58+ stderr: String,
59+}
60+
61+impl Run {
62+ fn ok(&self) -> bool {
63+ self.status == Some(0)
64+ }
65+}
66+
67+// ------------------------------------------------------------------ helpers
68+
69+fn run(cmd: &mut Command, stdin: Option<&str>) -> Result<Run, String> {
70+ cmd.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() })
71+ .stdout(Stdio::piped())
72+ .stderr(Stdio::piped());
73+ let mut child = cmd.spawn().map_err(|e| format!("spawn {:?}: {e}", cmd.get_program()))?;
74+ if let Some(s) = stdin {
75+ child.stdin.as_mut().unwrap().write_all(s.as_bytes()).map_err(|e| e.to_string())?;
76+ }
77+ let out = child.wait_with_output().map_err(|e| e.to_string())?;
78+ Ok(Run {
79+ status: out.status.code(),
80+ stdout: out.stdout,
81+ stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
82+ })
83+}
84+
85+/// Locate the vendored Nim. It lives at the *repository* root, which is not
86+/// the manifest dir when running inside a git worktree, so walk upwards.
87+fn find_nim() -> Result<PathBuf, String> {
88+ if let Ok(p) = std::env::var("RUSTNIM_NIM") {
89+ let p = PathBuf::from(p);
90+ if p.is_file() {
91+ return Ok(p);
92+ }
93+ return Err(format!("RUSTNIM_NIM={} is not a file", p.display()));
94+ }
95+ let mut dir: Option<&Path> = Some(Path::new(env!("CARGO_MANIFEST_DIR")));
96+ while let Some(d) = dir {
97+ let cand = d.join(".nim-toolchain/bin/nim");
98+ if cand.is_file() {
99+ return Ok(cand);
100+ }
101+ dir = d.parent();
102+ }
103+ Err("no .nim-toolchain/bin/nim found in this directory or any parent; \
104+ set RUSTNIM_NIM to the nim binary"
105+ .into())
106+}
107+
108+#[derive(Default)]
109+struct Directives {
110+ reject: Option<String>,
111+ skip: Option<String>,
112+ args: Vec<String>,
113+ stdin: Option<String>,
114+}
115+
116+fn directives(src: &str) -> Directives {
117+ let mut d = Directives::default();
118+ for line in src.lines() {
119+ let line = line.trim();
120+ let Some(rest) = line.strip_prefix("//@") else {
121+ // Directives must be in the leading comment block; stop at the
122+ // first line of real code so a `//@` inside a string can't count.
123+ if line.is_empty() || line.starts_with("//") || line.starts_with("#!") {
124+ continue;
125+ }
126+ break;
127+ };
128+ let rest = rest.trim();
129+ let (key, val) = match rest.split_once(':') {
130+ Some((k, v)) => (k.trim(), v.trim().to_string()),
131+ None => (rest, String::new()),
132+ };
133+ match key {
134+ "reject" => d.reject = Some(val),
135+ "skip" => d.skip = Some(val),
136+ "args" => d.args = val.split_whitespace().map(str::to_string).collect(),
137+ "stdin" => d.stdin = Some(format!("{val}\n")),
138+ _ => {}
139+ }
140+ }
141+ d
142+}
143+
144+/// Byte-for-byte diff, rendered readably: show the first differing line with
145+/// escapes, so a trailing-newline or whitespace difference is visible.
146+fn diff_report(want: &[u8], got: &[u8]) -> String {
147+ let w = String::from_utf8_lossy(want);
148+ let g = String::from_utf8_lossy(got);
149+ let (wl, gl): (Vec<_>, Vec<_>) = (w.lines().collect(), g.lines().collect());
150+ let mut out = String::new();
151+ for i in 0..wl.len().max(gl.len()) {
152+ let (a, b) = (wl.get(i), gl.get(i));
153+ if a != b {
154+ let _ = writeln!(out, " first difference at line {}:", i + 1);
155+ let _ = writeln!(out, " rustc: {}", a.map(|s| format!("{s:?}")).unwrap_or("<eof>".into()));
156+ let _ = writeln!(out, " nim : {}", b.map(|s| format!("{s:?}")).unwrap_or("<eof>".into()));
157+ break;
158+ }
159+ }
160+ if out.is_empty() {
161+ // Lines all matched, so the difference is in trailing bytes.
162+ let _ = writeln!(out, " lines match; raw bytes differ (trailing newline?)");
163+ let _ = writeln!(out, " rustc: {:?}", w);
164+ let _ = writeln!(out, " nim : {:?}", g);
165+ }
166+ let _ = writeln!(out, " ({} bytes from rustc, {} from nim)", want.len(), got.len());
167+ out
168+}
169+
170+fn tail(s: &str, lines: usize) -> String {
171+ let v: Vec<&str> = s.trim_end().lines().collect();
172+ let start = v.len().saturating_sub(lines);
173+ v[start..]
174+ .iter()
175+ .map(|l| format!(" {l}"))
176+ .collect::<Vec<_>>()
177+ .join("\n")
178+}
179+
180+// --------------------------------------------------------------- one case
181+
182+fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome {
183+ let name = case.file_stem().unwrap().to_string_lossy().into_owned();
184+ let src = match fs::read_to_string(case) {
185+ Ok(s) => s,
186+ Err(e) => return Outcome::fail("read", e.to_string()),
187+ };
188+ let d = directives(&src);
189+ if let Some(why) = d.skip {
190+ return Outcome::Skip(why);
191+ }
192+
193+ let dir = work.join(&name);
194+ let _ = fs::remove_dir_all(&dir);
195+ if let Err(e) = fs::create_dir_all(&dir) {
196+ return Outcome::fail("setup", e.to_string());
197+ }
198+
199+ // -- stage: transpile. Checked first, and checked strictly: exit status,
200+ // stderr on failure, and that the file actually has content in it.
201+ // Nim module names must be valid Nim identifiers, which case names
202+ // (`005-base16ct-decode-core`) are not.
203+ let mod_name: String = format!(
204+ "c_{}",
205+ name.chars().map(|c| if c.is_alphanumeric() { c } else { '_' }).collect::<String>()
206+ );
207+ let nim_src = dir.join(format!("{mod_name}.nim"));
208+ let transpile = match run(
209+ Command::new(RUSTNIM).arg(case).arg("-o").arg(&nim_src).env("TMPDIR", &dir),
210+ None,
211+ ) {
212+ Ok(r) => r,
213+ Err(e) => return Outcome::fail("rustnim", e),
214+ };
215+
216+ if let Some(want) = &d.reject {
217+ // A `reject` case asserts the "fail loudly" rule.
218+ if transpile.ok() {
219+ return Outcome::fail(
220+ "rustnim",
221+ format!("expected rejection containing {want:?}, but transpile succeeded"),
222+ );
223+ }
224+ if !transpile.stderr.contains(want.as_str()) {
225+ return Outcome::fail(
226+ "rustnim",
227+ format!("rejected, but message lacks {want:?}:\n{}", tail(&transpile.stderr, 10)),
228+ );
229+ }
230+ return Outcome::Pass;
231+ }
232+
233+ if !transpile.ok() {
234+ return Outcome::fail(
235+ "rustnim",
236+ format!("exit {:?}\n{}", transpile.status, tail(&transpile.stderr, 20)),
237+ );
238+ }
239+ match fs::metadata(&nim_src) {
240+ Err(_) => {
241+ return Outcome::fail("rustnim", "exited 0 but wrote no output file".to_string());
242+ }
243+ Ok(m) if m.len() == 0 => {
244+ return Outcome::fail("rustnim", "exited 0 but wrote an empty output file".to_string());
245+ }
246+ Ok(_) => {}
247+ }
248+
249+ // -- stage: rustc. No -O: debug profile, overflow checks on.
250+ let rs_bin = dir.join("rs.bin");
251+ let rc = match run(
252+ Command::new("rustc")
253+ .arg("--edition=2021")
254+ .arg("-A").arg("warnings")
255+ .arg(case)
256+ .arg("-o").arg(&rs_bin)
257+ .env("TMPDIR", &dir),
258+ None,
259+ ) {
260+ Ok(r) => r,
261+ Err(e) => return Outcome::fail("rustc", e),
262+ };
263+ if !rc.ok() {
264+ return Outcome::fail("rustc", format!("case does not compile as Rust:\n{}", tail(&rc.stderr, 20)));
265+ }
266+
267+ // -- stage: nim c
268+ let nim_bin = dir.join("nim.bin");
269+ let nc = match run(
270+ Command::new(nim)
271+ .arg("c")
272+ .arg("--hints:off")
273+ .arg("--warnings:off")
274+ .arg("--colors:off")
275+ .arg(format!("--nimcache:{}", dir.join("nimcache").display()))
276+ .arg(format!("-o:{}", nim_bin.display()))
277+ .arg(&nim_src)
278+ .env("TMPDIR", &dir),
279+ None,
280+ ) {
281+ Ok(r) => r,
282+ Err(e) => return Outcome::fail("nim", e),
283+ };
284+ if !nc.ok() {
285+ // Nim reports compile errors on stdout.
286+ let msg = if nc.stderr.trim().is_empty() {
287+ String::from_utf8_lossy(&nc.stdout).into_owned()
288+ } else {
289+ nc.stderr.clone()
290+ };
291+ return Outcome::fail(
292+ "nim",
293+ format!("generated Nim does not compile:\n{}\n --- generated ---\n{}", tail(&msg, 20), numbered(&nim_src)),
294+ );
295+ }
296+
297+ // -- stage: execute both
298+ let exec = |bin: &Path| {
299+ let mut c = Command::new(bin);
300+ c.args(&d.args).env("TMPDIR", &dir);
301+ run(&mut c, d.stdin.as_deref())
302+ };
303+ let (a, b) = match (exec(&rs_bin), exec(&nim_bin)) {
304+ (Ok(a), Ok(b)) => (a, b),
305+ (Err(e), _) | (_, Err(e)) => return Outcome::fail("run", e),
306+ };
307+
308+ if a.stdout != b.stdout {
309+ return Outcome::fail("diff", diff_report(&a.stdout, &b.stdout));
310+ }
311+ if a.status != b.status {
312+ return Outcome::fail(
313+ "diff",
314+ format!(
315+ "stdout matches but exit status differs: rustc {:?}, nim {:?}\n nim stderr:\n{}",
316+ a.status,
317+ b.status,
318+ tail(&b.stderr, 10)
319+ ),
320+ );
321+ }
322+ Outcome::Pass
323+}
324+
325+fn numbered(p: &Path) -> String {
326+ fs::read_to_string(p)
327+ .unwrap_or_default()
328+ .lines()
329+ .enumerate()
330+ .map(|(i, l)| format!(" {:>3} | {l}", i + 1))
331+ .collect::<Vec<_>>()
332+ .join("\n")
333+}
334+
335+// ------------------------------------------------------------------ driver
336+
337+#[test]
338+fn differential() {
339+ let root = Path::new(env!("CARGO_MANIFEST_DIR"));
340+ let cases_dir = root.join("tests/cases");
341+ let work = root.join("tests/.work");
342+ let _ = fs::create_dir_all(&work);
343+
344+ let nim = match find_nim() {
345+ Ok(n) => n,
346+ Err(e) => panic!("cannot locate Nim: {e}"),
347+ };
348+
349+ let mut cases: Vec<PathBuf> = fs::read_dir(&cases_dir)
350+ .unwrap_or_else(|e| panic!("no {}: {e}", cases_dir.display()))
351+ .filter_map(|e| e.ok().map(|e| e.path()))
352+ .filter(|p| p.extension().is_some_and(|x| x == "rs"))
353+ .collect();
354+ cases.sort();
355+
356+ // An empty corpus must not read as success. That is the exact failure this
357+ // runner exists to catch, and it applies to the runner itself.
358+ assert!(!cases.is_empty(), "no cases in {}", cases_dir.display());
359+
360+ let filter = std::env::var("RUSTNIM_CASE").ok();
361+ let mut results: BTreeMap<String, Outcome> = BTreeMap::new();
362+ for case in &cases {
363+ let name = case.file_stem().unwrap().to_string_lossy().into_owned();
364+ if let Some(f) = &filter {
365+ if !name.contains(f.as_str()) {
366+ continue;
367+ }
368+ }
369+ let outcome = run_case(case, &work, &nim);
370+ match &outcome {
371+ Outcome::Pass => eprintln!("ok {name}"),
372+ Outcome::Skip(why) => eprintln!("skip {name} ({why})"),
373+ Outcome::Fail { .. } => eprintln!("FAIL {name}"),
374+ }
375+ results.insert(name, outcome);
376+ }
377+
378+ let mut failed = Vec::new();
379+ let (mut pass, mut skip) = (0, 0);
380+ for (name, o) in &results {
381+ match o {
382+ Outcome::Pass => pass += 1,
383+ Outcome::Skip(_) => skip += 1,
384+ Outcome::Fail { stage, detail } => failed.push(format!(
385+ "\n--- {name}: failed at stage `{stage}`\n{}",
386+ detail.trim_end()
387+ )),
388+ }
389+ }
390+
391+ eprintln!("\n{pass} passed, {} failed, {skip} skipped", failed.len());
392+ if !failed.is_empty() {
393+ panic!("{}", failed.join("\n"));
394+ }
395+}