Rust → Nim in Code-Transpiler: what actually happens
A reproducible bug report for tarekwasfy01/Code-Transpiler,
which lists rust as a supported source language.
Summary: Rust cannot be used as a source language, and the CLI does not say so.
Most real Rust fails in the lexer. The rest parses into a tree containing no
functions, and the CLI writes an empty-but-valid target file and exits 0.
Nothing here is a criticism of the project's R-derived core, which works. The
issue is specifically the Rust frontend, and the silent failure that hides it.
Reproduce
./repro.sh
Clones upstream, builds cmd/r2many, runs the minimal cases through the CLI,
then re-runs them through a prober that reports which UAST node kinds the
frontend actually produced. Needs Go 1.26+ and network access. Recorded output:
EXPECTED.md.
What was tried first
A random real crate: base16ct 1.0.0 — 613 lines, pure Rust, no_std, no
dependencies. Five of its six source files failed to parse. The sixth, which
contains three functions, transpiled to this complete Nim program:
{
}
For the minimal cases in cases/, the emitted Nim file is even smaller: one
byte, a newline. Exit code 0 in both situations.
Full transcript: outputs/base16ct-1.0.0-transcript.txt.
The four defects
0. // line comments are not supported
internal/backend/parser.go:157
This tokenizer recognises # comments only, alongside R operators (<<-,
%in%, [[). A Rust // becomes two division operators:
expected expression near "/"
cases/00-comment.rs and
cases/00-comment-fails.rs differ by exactly one
comment line, and only the second fails. Every file in a real crate has
comments, base16ct included — doc comments alone account for much of why it
never reaches the later stages.
1. ' is always a string opener, so lifetimes break the lexer
internal/matrixir/lexical.go:261
if c == '"' || c == '\'' || c == '`' {
<'a> and Formatter<'_> are consumed as unterminated char literals. The
delimiter pairing in AnalyzeTokenStructure then desyncs, which is why the
reported error is a bracket complaint at an offset that always turns out to be
a lifetime. Case: cases/04-lifetime.rs.
This one looks genuinely small to fix: for source == "rust", treat ' as a
literal only when it matches '\?.', and emit a lifetime token otherwise.
2. The Rust parser is the R parser
internal/backend/frontend_fact_parser.go:387
func (p *factParser) parseIf() (ParsedNode, error) {
p.next()
if _, e := p.expect(tokLParen, ""); e != nil { // requires if ( ... )
This is a second, separate tokenizer from the one in defect 1 — the two stages
disagree — but both are R's. Parenthesised conditions are mandatory, so Rust's if cond { } can never
parse — it fails with expected "" near "<first token of cond>". Case:
cases/02-if-block.rs. Macro invocations are likewise
unhandled (cases/03-macro.rs).
The emitted semantic contract is hardcoded R for a file declared as Rust:
"language_profile": "rust",
"value_model": "tagged_dynamic_binary64",
"index_base": 1,
"type_contract": { "truth": "r_compatible", "ownership": "unknown" }
3. fn is never recognised as a declaration
This is the one that yields empty output. Node kinds produced for two
equivalent programs, via prober/main.go:
| input | function nodes |
|---|---|
cases/05-equivalent.R — f <- function(a) {…} |
FUNCTION:1, CLOSURE:1, function:5, parameter:2 |
cases/01-trivial.rs — fn f(a: i32) -> i32 {…} |
none — call:2, CALL:1 |
fn f(a: i32) is parsed as a call to f, with fn left as a stray
identifier. The tree holds no functions, so the Nim emitter correctly emits
nothing. Note that case 01 reports no parse error — it is the silent path.
The silent-failure path
-runtime defaults to true. The fallback catches the parse failure, writes a
syntactically valid empty program, and exits 0. The machinery to report this
already exists and is correct when asked:
-native strict native frontend for "rust" is not implemented
-no-runtime 1/1 translations failed: [{nim DIRECT_NATIVE_UNAVAILABLE (stage=direct)}]
(default) exit 0, empty file
Refusing to emit a program with zero functions when the input declared some
would turn a silent wrong answer into an honest error, without touching the
frontend at all.
Scope of a real fix
Defects 0 and 1 are patches. Defects 2 and 3 are not: there is no Rust frontend to
repair. Supporting Rust means writing one — items, generics, lifetimes,
impl/traits, match, macros, ? — and lowering it into a semantic model
that isn't R-shaped, since the type contract is R throughout. That is
comparable in size to the existing ~1,400-line R frontend, and probably larger.
Contents
cases/ minimal inputs, one per defect, plus the R control
prober/ drop-in cmd/dbg that prints UAST node-kind counts
outputs/ the base16ct run: transcript and the one emitted .nim
repro.sh clone, build, run everything
prober/main.go imports internal/backend, so it only compiles from inside
the upstream tree; repro.sh copies it to cmd/dbg for you.
Upstream is MIT. No upstream or crate source is vendored here — repro.sh
fetches what it needs.