r/Compilers • u/NXDLang • 2d ago
Rewriting a Python Compiler Frontend in Rust: Separate Repository or In-Place Migration?
I've been working on a language/compiler project called NXD for a while now.
The current architecture looks roughly like this:
NXD Source
↓
Python Frontend
↓
JSON AST/IR Handoff
↓
Rust Semantic Analysis
↓
Rust IR
↓
Rust Backends
The reason for the split is pretty simple: Python was the language I knew best when I started the project.
That decision got the language off the ground quickly, but now I'm seeing the downside of having a Python → JSON → Rust boundary in the middle of the compiler.
Things like:
• AST serialization/deserialization
• duplicate data structures
• frontend/backend synchronization
• debugging across language boundaries
all add complexity that wouldn't exist in a fully Rust implementation.
Current Compiler Status
The Rust side already contains:
• semantic analysis
• diagnostics
• IR
• Nim backend
• async diagnostics and validation
• ~260 language tests
The frontend is still Python.
Why I Created a Second Repository
My initial thought was to rewrite the frontend inside the main repository.
The more I thought about it, the less I liked that idea.
Instead, I created a new repository:
nxd_language (reference implementation)
nxd-rust-frontend (migration project)
The migration repo contains the Rust compiler core, semantic layer, IR, backend, tests, etc., but none of the Python frontend code.
My reasoning is:
No chance of accidentally invoking Python during validation.
No uncertainty about which parser generated a result.
No repo bloat from carrying two frontends indefinitely.
The original compiler always remains functional.
The goal is to run both repositories side-by-side and treat the Python implementation as a reference implementation.
What Surprised Me
I expected the new repository to fail spectacularly.
Instead, after removing a couple of backend references I wasn't migrating yet, it built immediately:
`cargo build`
and even runs:
Usage:
`cargo run <input_json> <output_nim>`
`cargo run -- --semantics <input_json> <output_nim>`
Which tells me the architecture is already centered around:
JSON
↓
Rust Semantic Layer
↓
Rust Backend
and the frontend replacement surface may be smaller than I originally thought.
Planned Migration Strategy
Current:
Python Source
↓
Python Lexer
↓
Python Parser
↓
JSON
↓
Rust Compiler
Target:
NXD Source
↓
Rust Lexer
↓
Rust Parser
↓
Rust AST
↓
Rust IR
↓
Semantic Analysis
↓
Backend
I still plan to keep JSON AST dumping for debugging, but no longer as a required transport mechanism.
Questions
For those who have rewritten significant parts of a compiler:
Did you keep the original implementation around as a behavioral reference?
Did you migrate inside the existing repository or create a second migration repository?
What ended up being harder than expected?
What architectural mistakes would you avoid if doing it again?
Is there any reason to keep a separate frontend IR if a compiler IR already exists?
I'm especially interested in experiences from people who migrated between languages (Python → Rust, C++ → Rust, Java → Kotlin, etc.) rather than greenfield rewrites.
1
u/no---u- 2d ago
Or, you know, just delete the python frontend and rewrite it? I don't see a point in making new repos, unless you want the python frontend still working for some reason.
1
u/NXDLang 2d ago
Fair question. I could rewrite it directly in the existing repo, and the Python frontend is not intended to survive long-term. I made the second repo so the current implementation remains a working reference while I verify the Rust lexer and parser against it. It also gives me a clean environment where I know Python cannot be involved in the result. Once parity is proven, the Rust frontend replaces Python in the main repo, so the second repo is really migration scaffolding rather than a permanent fork.
1
u/zomerbk 1d ago
From experience with language migrations, one aspect that often proves trickier than anticipated is achieving full diagnostic and error recovery parity with the original frontend. The Python lexer/parser might have had implicit behaviors, recovery strategies for malformed input, or specific error messages that are deeply tied to its implementation details. Replicating those exact nuances in Rust to ensure the new frontend behaves identically, especially for user-facing errors, can require a surprisingly extensive set of tests focusing on erroneous input rather than just valid programs.
2
u/dnpetrov 19h ago
"Rewrite" from Java to Kotlin is not required. From the start, Kotlin was designed so that Java and Kotlin sources can coexist in the same project with minimal issues. There are abstractions in Kotlin that Java doesn't have (coroutines, inline functions with reified generics, to name a few). But otherwise interoperability is absolutely transparent, to the point that Kotlin built-ins and standard library were so heavily based on JDK, that there are some "dark corners" in Kotlin/JS behavior due to mismatching behavior of the underlying platform types.
We did that kind of rewrite in Kotlin, when it was already in production, and extensively used. We kept the original implementation ("old" backend, "old" frontend) in the same repo. We used them as a language and behavioral references. We also maintained backward compatibility as much as possible, gradually cleaning up those pesky corner cases in the old frontend before rolling out a new one. We kept a list of those changes, passed them through some formal procedure (review and approval by language design committee), and had a feature toggling mechanism built into the compiler at an early stage. We also maintained binary compatibility for the public ABI, and ran binary compatibility checks on major open source projects before making new backend default.
In other words, it was quite an effort, but it was something we expected and was prepared for.
2
u/yuehuang 2d ago