Porting Interpreters: A Voxam Case Study

I’ve always been curious whether interpreter authors have ever considered porting their code to another language. Or porting someone else’s interpreter, for that matter. I designed Voxam to be (relatively) easy to port, but I’m now going to put that to the test. Incidentally, this post may seem pure vanity. But I’m posting my thinking because I’m genuinely curious to hear others’ thinking and how they have approached this idea.

So, given my design, I’ve been thinking either Go or Rust. Either seems like a big undertaking, but, again, given that design, I think the port is easier than the almost 50,000 lines suggest. Three things about Voxam’s design work heavily in a porter’s favor:

  1. The core has zero dependencies. The VM cores, blorb/IFF/Quetzal/babel, even the PNG and AIFF decoders are hand-rolled stdlib Python. There’s no library-shaped hole to fill; it’s all byte-level code that translates mechanically. And in Go or Rust, the cores actually get cleaner: all the & 0xFFFF / & STATE_MASK masking that Python’s arbitrary-precision ints force becomes native uint16/uint32 wrapping. The opcode dispatch dicts (“random”: Machine._op_random) become switch statements or method tables. Mechanical either way.
  2. The RNG is already language-independent. I deliberately built xorshift32 in-house so recordings would never be invalidated by an interpreter upgrade. That decision now pays off twice. Port those ~50 lines faithfully, and every seeded recording replays identically under the new implementation.
  3. Which means my certification corpus carries over for free. The 44 acceptance recordings, the regtest suites, glulxercise, and the byte-identical Å-machine batteries are all language-neutral. I don’t need to port thousands of lines of pytest. Instead, port unit tests selectively for the tricky decoders (ZSCII, operand decoding, Quetzal), and let the recordings certify everything else end-to-end. In my experience, a port project with a pre-existing, spec-grade acceptance harness is rare, and this is probably my single biggest asset.

Where the real work crops up is that the codebase splits into three layers with very different port characters.

  • VM cores (~32k lines: zmachine 11.6k, glulx 15k, aamachine 5.5k). That’s mechanical translation, low risk, recordings as the net. The Glk layer (glulx/glk/, ~7k lines with its object registries and dispatch) is the subtlest part.
  • Binary formats (blorb, iff, quetzal, png, aiff, babel, listing, sixel). That’s mechanical byte work. Go’s stdlib even hands me image/png while staying zero-dependency, so png.py could vanish outright.
  • Frontends: These are rewrites in kind, not ports, and this is where the design effort concentrates.

One architectural difference I would have to plan for: Pygame lets the interpreter block for input within its own event loop; Ebitengine (my likely replacement) inverts control: it owns the loop, calls your Update/Draw at 60 fps, and you must never block. The idiomatic Go answer is for the interpreter to run in its own goroutine, communicating with the frontend over channels. Now, here’s the nice part: that seam already exists in Voxam. It’s the wire, which is my GlkOte stanza protocol. If the port makes that the internal seam for every face, then bubbletea (to replace my blessed), Ebitengine (to replace my Pygame), the web tab, and stdin all become consumers of one channel protocol, and the desktop shell keeps working unchanged since it already drives glkote down a pipe.

There is a caveat with bubbletea: it uses the Elm architecture (model/update/view), whereas my glass.py/painter.py paint terminal cells directly, the way blessed does. tcell (the library bubbletea itself sits on) is the closer stand-in for my painted glass: direct cell-level control, no framework inversion. Bubbletea would mean redesigning the glass, not porting it.

But what about Go vs Rust?

Go’s case is fairly simple. It matches the project’s spirit: Voxam is stdlib-only Python, and Go’s stdlib covers PNG, HTTP, and JSON, in the same batteries-included way. Goroutines+channels dissolve the blocking-input problem naturally. Ebitengine bundles cross-platform audio, which replaces both pygame and sounddevice, thus no more PortAudio apt-install caveat on Linux. And distribution collapses to a single static binary: the entire pip install “voxam[screen,graphics,sound]” extras story becomes “download voxam.exe, every face included.” Iteration speed stays close to Python’s.

Rust’s case seems to have two genuine advantages. First, sum types with exhaustive matching are philosophically aligned with my spec-fidelity ethos: an opcode/operand enum makes the compiler enforce the coverage my 100%-branch gate currently enforces by discipline, and Wrapping<u16> makes overflow semantics explicit in the type. Second, my desktop shell is already Tauri, i.e., already Rust. A Rust core could eventually link into the shell in-process instead of spawning a subprocess, and Rust’s wasm story is mature enough to run the interpreter in the browser page rather than server-side behind --web, as I currently do. On the frontend side, ratatui is excellent (arguably stronger than bubbletea for grid-and-window layouts), but the pygame-equivalent story is weaker. I guess macroquad or ggez work, but neither is the community default Ebitengine is, and I would have to assemble sound (rodio/cpal) separately. Plus, the borrow checker adds real ceremony to the interpreter-thread/frontend-channel plumbing that Go gives me for free.

So, my current recommendation to myself is Go. The cores port equally well to either; the frontends decide it, and Go’s frontend path (tcell + Ebitengine + net/http) is a paved road where Rust’s is (or at least feels like) a parts list. I think, at this point, I would choose Rust only if in-browser wasm or in-process Tauri integration is a driving goal rather than a nice-to-have.

Still noodling on this, and I’m thinking about the Tauri (desktop) port in particular.

One thing is for certain outside of any language you choose. You’re going to find where “all the bodies are buried” in all that other code. You’ll also be embarking on a generally thankless task until it is finished. That takes a lot of stamina.

Voxam is kinda unusual in that it’s implementing Z-machine and the UI around it. Most projects depend on an embedded interpreter (Bocfel, Glulxe, etc.) and provide a Glk host (perhaps a RemGlk host) with UI affordances.

You could port Bocfel to Go or Rust, but I don’t think users would see any benefit from that.

It’s the UI where porting actually makes a difference, because of platform-specific UI frameworks. Basically, you’d port to something else in order to port off of pygame.

If you’re developing a Mac app, you should do it in AppKit (or SwiftUI, but, probably AppKit), and you should write it in Swift. Mac users can really feel when your app isn’t truly native.

(If you’re developing a Windows app or a Linux app, nobody cares what UI framework you use, because Windows/Linux apps are so heterogenous. There’s no such thing as an app that “feels like Windows.” Even VS Code feels wildly different from Office 365, which feels quite different from the Edge browser.)

Similarly, if you’re developing an iOS app, you should do it in UIKit (or SwiftUI; it’s not as bad on iOS), and you should write it in Swift.

If you’re developing an Android app, you should do it in Compose, and you should write it in Kotlin. There’s a native C API for Android, but Compose is a Kotlin library, so you’ll have to use JNI to access Compose UI widgets. It’s more performant to just use Kotlin.

If you’re developing a web app, you should do it in JavaScript or TypeScript. WASM performs really badly in web UI, because WASM isn’t allowed to directly touch the DOM (adding/removing elements/attributes). To do any UI work, WASM has to use JS to do it, and calling from WASM to JS is quite slow.

Also, WASM has almost no standard library, not even an ICU library, so you have to do a lot of work to prevent WASM projects from pulling in megabytes of dependency libraries that the browser provides out-of-the-box for JS, even just for bog-standard string management. Lots of projects just ship their own ICU library in WASM, just so they can manage UTF-8 strings, and it’s terrible for performance.

I think there would probably be a real benefit in porting all of the terps in garglk/terps to TypeScript; I think it would improve on the runtime performance of Parchment (which uses WASM + RemGlk today).

The IF dev community has mostly backed away from that, because nobody’s had time to maintain a separate implementation of these interpreters. Parchment switched away from zvm.js and Quixe and onto Bocfel and Glulxe in WASM, and it made performance much worse, but that decision was made because nobody was available to maintain ZVM and Quixe. (Even just porting ADRIFT from Visual Basic to Scarier in C++ in Spatterlight has been a lot of work. It sounds very painful to do it again to port it to TypeScript.)

But, if you’re asking, that’s where I think porting could provide the most value in 2026: porting all the popular interpreters to TypeScript and keeping them well maintained.

The files are bigger, but the runtime performance should be better. @Zed has been testing a bunch of things and the Emglken Glulx VMs are usually twice as fast as Quixe. They’ll be even faster when JSPI is usable.

That said porting stuff to Typescript, with time to continue maintaining them, would also be valuable. For maximum performance it would need to be in semi asm.js. Hmm, but I see browsers are actually removing their asm.js optimisations now… so the question is really at what point do the WASM-JS boundary crosses outweigh the WASM performance benefit. Minimising the number of times WASM calls out to JS is key.

Load time performance is worse with Bocfel WASM compared to ZVM, and always will be, because it’s going to be more bytes on the wire (and in memory).

Post-load performance seems comparable; I’d be surprised if there were a user-visible difference.

I’ve been pondering this myself. I get pinged on a regular basis on when I am going to port Cairn to Windows and Android. I’ve got a solid OpenSpec spec. I am seriously considering running an experiment.

I decided to give this a try. I ended up going with Rust instead of Go because I can do a lot more interesting cross-platform stuff that way. (Both languages are capable, of course, but the ecosystem for Rust is proving a little more amenable). The initial work is here:

Because Voxam is entirely spec-driven and annotated with the spec information as I learned to implement it, this is proving fairly easy. To do this, I used a certify approach. Since the Rust port is being built the same way, I could use the same technique in another language.

All this said, I’m now at the more complicated part: my revised GlkOte for sound and graphics, plus the frontends. In other words, porting the “machines” was, unsurprisingly, relatively easy. How the frontends will go remains to be seen, but I’m going to try to condense as much of that as I can into a single interface that can be split into many.

And again, I’ll hasten to add: I’m not building Voxam (either the Python or Rust version) to compete, in any sense, with any established interpreter. My goal was to provide as complete a spec-accurate implementation as possible so that (1) someone could read the code to learn how to do it and (2) porting it would be easy because you would be porting spec-accurate chunks that have all been validated, not just by unit tests but by acceptance tests.

pretty sure this was done in Rust. Code is on GitHub, but I think it’s been inactive for almost a decade. Encrusted - Home

Yep, I did see that one. I’m purposely not relying on any of its code. I’m using Voxam (my Python version) as the sole reference implementation. Which I realize probably sounds stupid. But I did that largely so (1) I could test the pedagogical usefulness of Voxam’s code and (2) so that I went in with not too many preconceptions of how another implementation did it.

I should note I’m learning Rust as I do this, so my code is going to be … well, let’s just say, not so great. (Then again, my code in Go would have been not so great as well. All I’ve done in Go is my Defender attempt.)

I’ve always been curious whether interpreter authors have ever considered porting their code to another language. Or porting someone else’s interpreter, for that matter. I designed Voxam to be (relatively) easy to port, but I’m now going to put that to the test. Incidentally, this post may seem pure vanity. But I’m posting my thinking because I’m genuinely curious to hear others’ thinking and how they have approached this idea.

I initially wrote my own Å-machine interpreter because I had a use case that the reference implementation didn’t support, and the modifications needed were hard due to the low-level design. I felt like porting the interpreter was out of the question, since I wanted to understand the purpose behind every decision laid out in the specification. Like others in the thread have mentioned, porting is a lot of work and mainly worth it if writing an interpreter from scratch isn’t feasible.

Indeed, but I find it depends on how testable the reference interpreter is. Case in point: I already ported to Rust, which was really easy to get going, but I just found I don’t like the language. (Or, rather, the language is just fine; it just isn’t something my cognitive space is ready for yet.) So, then I started porting (in the project) to C# using the AvaloniaUI, and it was similarly easy to port. (Current work on that.) What this has shown me is that I can take a pure Python interpreter that was fully spec-annotated and test-driven and quickly port it to Rust and C#. But notice how this goes back to exactly what you said: I wrote Voxam for pedagogical purposes and thus tried to show “the purpose behind every decision laid out in the specification.”

Now, that being said, I do support the Å-machine, but it’s also the one I feel I most need to make sure is annotated well. And I haven’t ported that to C# yet.

All in all, it’s an interesting challenge!

I’ve rewritten my interpreter many times over the years - C#, Java, Haskell, Rust.
My latest interpreter core in Rust has no dependencies* and I have CLI, GUI, and Webassembly front-ends for it.

*Well, almost no dependencies - it is nostd, but does use alloc for a couple heap allocations at machine instantiation.

If you want a reference, Lanthorn has dependency free Rust Glulx (with full op acceleration), Z-Machine, and Scott-Adams engines at its core as separate crates. None of these were ports of existing engines.

The project also has simple cli-clients for each of these for testing purposes (although they work perfectly in their own right if you don’t want the full TUI experience.)

Dependency free Rust is not a good goal. Especially for something like a VM implementation, when crates like byteorder are high quality and basically zero overhead, the common community accepted solution, and no_std compatible.

There are heavy dependencies which can make sense avoiding, like Serde, especially if you’re wanting to make something that suits embedded or wasm deployment. But Rust has so many great zero cost packages. The whole strength of Rust is to bring modern ergonomic patterns together with high efficiency implementations, and the package ecosystem reflects that, with most popular packages being no_std compatible it possible. Dependency free Rust is actually going against Rust’s strengths.

(Dependency free Rust for libraries can be a valid goal, especially when they’re for small stand alone features. The byteorder crate has no dependencies for example. But it doesn’t make sense for apps.)

It certainly can be. It depends. My z-machine implementation is a nostd library; and while pulling in other nostd libraries is acceptable, the vast majority of libraries out there are going to drag in more than needed and can also be opinionated on design. I simply didn’t need any other libraries to accomplish my goal.

And don’t call me Shirley.

For what it’s worth, I think Lanthron is staggeringly impressive. I could never compete with that in terms of implementation, at least with the skills I have now.

Voxam was essentially my kitchen-sink approach to finally try something. So, I put in a plain terminal (sort of dfrotz-like, so I could do a simple wire for capturing output), a TUI (using blessed), a GUI (pygame), a modified GlkOte, and Tauri. (The latter mainly to look like I wasn’t entirely cribbing on Lectrote.) I wanted to provide a cross-platform RegTest and also have a strong acceptance suite based on game walkthroughs, all of which was backed up by spec-annotated code so that I knew the implementation was following every jot and tittle of the specs. All of this without AI, which was a personal goal for me. So far achieved!

Now I’m putting in C# with AvaloniaUI, mainly because C# is a language I haven’t considered in a long time, and for no real good reason, I’m finding. I actually like the language quite a bit.

The one danger (if such it can be called) I found is that it’s easy to stop making an “interpreter” (which should arguably do one thing and do it well) and start making a “platform” for text adventures, by which I mean going beyond what a strict interpreter has to do. Which has also been its own learning experience!

I will say I had never considered the Scott-Adams engine, so I started looking into that. I see ScottFree and ScottKit, but just starting to learn my way around those.

This is good info. Y’all are making me want to take a second look at Rust.

Jeff, Lanthorn was primarily an experiment to learn Claude Code. So while it took A LOT of hand holding on my part to keep it from making silly design decisions I wrote almost no code directly. I also found it gets 90% there quickly, but that last 10% takes a lot of human attention with a clear idea of how things should work. So it is very helpful to have written a lot of code over the years, although this is my first interpreter, so it could probably be better if designed by someone with expertise in that area.

Yeah, I suspect anyone looking at my code will certainly be able to see I used no AI in this case. But I can entirely see how it would have sped up whole lots of foundational things for me. As it was, I had to fully build up an entire repo of things I could use as a reference (entharion).

Indeed, Voxam is my first real attempt at something substantive here. For what it’s worth, I was playing around with a local copy of Lanthron. I added support for z1 and z2, just to get used to Rust. I then added a lanthorn-gui just to see how much I could get a GUI to look like the TUI. What I’m finding is that to take that further, I would have to move the host-neutral parts into a library that both binaries (the TUI and GUI) call. My skills ain’t there yet!

All of this, including the discussion here, has convinced me to give Rust another shot. I’m starting to learn better how to think in terms of its code idioms.