Why are we translating code to Rust?

Rust
bioinformatics
LLM
Our lab has since 2026 started using LLMs. It wasn’t an easy step …
Published

July 4, 2026

Background

Our lab has since 2026 started using LLMs. It wasn’t an easy step - there’s no lack of ethical concerns here, and we take pride in our programming skills. But the world moves on and it’s evolve-or-die: we can’t stop the progress. We can only try to move in the least bad direction.

We are delivering increasingly complex bioinformatics pipelines and we are a small number of people, with limited monetary supply and time, against an ecosystem that keeps asking more out of us. Our use of LLMs was born out of necessity to keep up, fixing the long list of trivial problems, so we can focus our attention on the really hard ones. Below is a blurb that all our crates will have, while they also point here for the latest one and further info.

The story started with our Zorn/Bascet single-cell analysis software (preprint). We have been developing a new method for reference-free analysis, i.e., when no reference genome is available - such as for unknown microbial communities, and maybe even late-stage cancer (the pan-genome of humans is in either case underappreciated). Long story short, this has required us to reinvent single-cell analysis from scratch, as we aren’t merely interested in cell states, and it’s even difficult to generate a count matrix. As sequencing data is huge, we developed Zorn/Bascet in Rust, as it seems to be the language of the future (newer C++ standards are interesting, but it still isn’t a very inviting language). However, any senior engineer will agree that solving technical problems is trivial - social problems are not. We expect our users to still want to use aligners they are familiar with, and run downstream software that has been proven (maybe even published). But this is maybe not as trivial as it sounds: should we output one FASTQ-pair for each cell we sequence? This will soon end up with millions of files, which operating systems are not designed to handle. Besides OS and file system overhead, what is the overhead of loading a KRAKEN database/reference genome for each cell? Eukaryotic single-cell analysis has solved this for alignment by having one big file of all cells and handling (de)multiplexing ourselves. But we need to handle this problem in general, including de novo assembly. So great, we realized we had to replace Nextflow as a first step, but with version that is optimized for files in different kinds of object stores (mostly zip files). Tall order, but you do what you got to do…

Delivering complex packages of software to users is nontrivial. Given that we already bundled SKESA, BWA, STAR, various genome annotation software written in Perl, you-name-it, we went for the standard logistic solution: containers and package managers. Specifically, Conda is the de facto solution for packing software across platforms (or was, since they changed licensing policies and APIs mid-project). But asking users to pull in all dependencies in Conda can be asking for a fair bit - so we also went for Singularity (now Apptainer; the foundation is as solid as mud, as we were about to discover). Life was well, until we realized that only Linux users can use Apptainer, so we had to also add Docker support. But these container systems don’t conform to the same standard, so to avoid ridiculously long build times, we ended up with two build scripts (for Docker and Apptainer). Usually working well, as long as you didn’t rebuild the image at the wrong time, as some software suddenly got different CLI parameters and broke everything (no semantic versioning, hard to predict). Don’t breathe on it if it works, moving on…

Progress was made, but problems kept building up. We had to read some files we produced in R - do we write this in C, or do we link to Rust? As of writing, R-Rust FFI is immature and portability-wise a gamble (R has a separate download for even a basic C compiler on Windows!). We decided to call Bascet to extract the file instead. This worked great until we wanted 10,000 of them. Problem? 5 seconds to start Bascet in the container (due to all dependencies; it’s a big boy). We kept working around the packaging system, using the processx R package. Start a single session with a specialized “R-friendly CLI console”, keep it alive, pray that no error happens during transmission (despite R having little error handling and weak low-level communication). Worked, good enough. Had to fix 3-4 times due to various platform-related issues which kept coming back, but we do care about ease of use and not breaking stuff.

Final straw(s) came when trying to make everything work on OS X, Windows and Linux (Linux now frequently means Ubuntu-in-Windows, which for R-in-Windows-with-Docker really means Windows), I

  1. got challenged with making Docker work across operating systems. Different permissions on different OS’. If you work outside the current directory, you need different treatment. And no simple way to programmatically enable permissions on, e.g., /Volumes. It will be us taking the support requests for all users inevitably running into this.
  2. got a request to make Podman a container option. A fair request given how it makes Docker less annoying, so we tried. But this meant a 3rd container option, and it once again had different behavior across OS’.
  3. wanted to run GECCO on each of our cells’ assembled genome to find Biosynthetic Gene Clusters (BGCs). Conda (un)delivered, GECCO installed but crashed due to a broken Matplotlib (I think) dependency (a common joke is that any serious Python developer has at least 10 broken Conda environments - very true).
  4. tried to make a new container using Conda and failed. Or so I thought. It stalled for over an hour, suggesting the presence of a bug. But it was not quite true; Python environments only allow one package version to be installed in parallel and Conda did its best to figure out how to make it all work out. This is an NP-complete problem and it just took unreasonable time to resolve versions. Mamba partially fixed it, as it is a faster version of Conda, and can brute-force the NP-complete problem. It would likely still explode one day, but at least we didn’t break the ecosystem for now. Accept and move on.
  5. tried to patch things up using a Podman script to make a container, and then conversion to an Apptainer container from there. No deviation of behavior across platforms, good engineering, but also 20 minute build time before we could test our software on our compute center. Forgot that detail, that’s why we had two build scripts in the first place…
  6. and then I tried to make our Windows platform work (make no mistake: Windows needs to go - but when even “Linux-users” use WSL2, that’s a tough message to sell). We realized some users may have data on d: (d//??)… how on earth will we patch these Unix paths through the container transparently in Windows?

While I am not a fan of AI, I couldn’t but notice the post by Lior Pachter. It got me wondering if it was time to challenge old ideas… Short story, it worked beyond my imagination, although I still have reservations about use of AI in general

Why Rust? - Rust is a good language for vibe coding

It seems that vibe coding is here to stay. While we have plenty of tasks where Codex/Claude fail, and only relying on vibe coding will make your brain rot, there clearly are cases where it is useful. (1) Translation is a good application because the presence of original software provides a solid base for testing. It is also a thankless task that is near impossible to fund. (2) Prototyping code which you don’t want to invest too much in, or the chance of failure is too high to involve a PhD/postdoc, is perfect for AI. (3) Throw-away tools which you don’t plan to maintain can live with the half-baked code quality that you get from AI curently (though I assmume it will improve).

If you then vibe code, which language should you instruct the AI to use? Some say that Python is good because there is plenty of code in Python (seems like a strong argument). But then, almost all languages in use are some variant of C, and they are thus just dialects. Python might shine because of the libraries that AI might be trained on (I have tried developing custom VAEs in Python/Pytorch and I am definitely impressed). But Python lacks a huge benefit of Rust: the safeguard of static typing.

Idiomatic Rust code will not compile if certain types of logical errors are present:

  • Obviously, all types have to add up
  • Exceptions must be handled, and exceptions are plenty
  • If you have an enum (x = Node v | Tree t t), Rust match constructs force you to handle all types, which is especially good if the enum is ever expanded
  • Unused variables and functions trigger warnings
  • Variables cannot leak into the global scope and get consumed in the wrong place
  • Null is gone

I think this makes Rust an ideal language for vibe coding. What AI doesn’t yet have in precision, Rust makes up for in safeguards

Why Rust? - The case is already closed

Trying get a crowd of programmers to agree on a language is no easy feat. Everyone has an opinion. For better or worse, the decision is seldom technical, but rather falls on history. I have always recommended beginners to pick the same language as the other guys in the office so you have someone to ask for help. This makes the choice quite a bit of a popularity contest which is why bioinformatics now uses a variety of Java, Python, R, Perl, Fortran, followed by Go, Julia and Rust (certainly some language is forgotten). The main reason the community now uses R and Python is - frankly - because universities churn out R and Python programmers. We do so, because we want students to be productive on the market. It is a Catch 22.

But popularity is not actually a good way of choosing a solution. If you want to learn a really mainstream language (defined by commonality), you would go for COBOL. COBOL kind of proves a point: just because a language is common doesn’t mean it is actually good. Luckily, once a solution ends up really good, popularity does seem to shift.

A real headache in bioinformatics education is what language to teach. It seemed until now that everyone had to learn R and Python. The increasing demand for web deployment also called for Javascript (and HTML and CSS etc). And if someone still would have capacity to learn left, then add C/C++ on the list (someone still has to develop those aligners!). Unfortunately, to write solid code you also need to learn about data structures, algorithms, parallelization, etc, but we can’t fit it while also teaching all the dialects of C. For anyone but the polyglots I struggle to recommend a language to learn this second, because it does seem that you need to be a superhuman to cover the full spectrum.

Luckily, industry seems to be closing the case:

  • Linux has picked Rust. If you know why C became famous, this alone settles it
  • Other major companies have dabbled with Go (seems ok!). But the garbage collection in Go has, e.g., gotten Discord to kick it out in favour of Rust. Other companies are following

It will take time but it is likely a one-directional street. I have always been conservative in which technology I use, but the broad adoption meant that Rust seems to be the safe default choice at the moment.

Results

After combined use of Claude and Codex to translate all upstream dependencies of Bascet, I conclude that

  • The download has reduced from 17gb to 200mb
  • Compile time has gone from 20 min to 1-2 minutes
  • The time to ship our binary to test in our national compute facility is no longer rate limiting
  • We now got proper support for the new Apple CPU (would otherwise require a second container image to be shipped)
  • We got better control of total RAM usage, since we can tap into our dependencies as libraries
  • 3-100x faster execution of libraries on single-cell libraries (1s startup time per cell x 1M cells means 12 days just starting software, not doing work!)
  • No need for Docker/Singularity/Podman
  • No need for Conda
  • Better integration with the web. No need to worry where code will run - it goes into webassembly or regular binary; just code as usual

As some bonus

  • Documentation in Zorn greatly improved, polished by LLM
  • Bugs in both Zorn and Bascet discovered and fixed by LLM
  • I managed to reverse engineer and add support for a file format that the imaging community has failed to handle since 2016. For a future blog post; expect much more to follow in this domain
  • Some experience vibe coding tools from scratch. Mixed bag. Maybe the biggest take-home message is that only AI likes to read code by AI. It’s much too verbose. I have also gathered a collection of the most stupid conversations ever. I can verify that it is perfectly plausible that an AI might just delete all your data one day.

Lessons

A document is being written up. But I think I have verified my prior assumption - to fully benefit from AI, one should use as little AI as possible. What do I mean by this?

If you want to add two numbers, a calculator is unbeatable. It is the minimum hardware needed, hardwired for the task. My brain struggles to keep up with it. LLMs can keep up but drains a lot of electricity, or might even just make up an answer. So make sure to use the right tool for the job!

But LLMs can seemingly design a calculator, and while probabilistic, they can be made to converge to a fix-point through repeated application and self-testing (so expect to see some reinforcement learning in this space).

I thus see a tool hierarchy: Use brains to steer LLMs. Use LLMs to design classic normal code. Run that code to solve tasks.

In my case, I wrote up static analysis tools that are much better at detecting mistranslation than LLMs are. They run faster, and consume less energy. The tools were made using LLMs, and the LLM picks up and uses the tools. I have also made other tools the same way, expanding the capability of the LLM while only relying on the LLM for tasks where we lack simple algorithms. They fill in the heuristics that we don’t want to encode in regular code.

Still, there are ways to go. I gave up translating BLAST after 3 attempts. Again, I think the solution is to rely less on LLMs for translation, except for the places where we lack better algorithms. Stay tuned for updates in this space as BLAST will definitely get translated at some point. Transpilers are the way. For now, I think BLAST can be seen as the frontier for LLM-driven translation.

We can now reduce technical debt in bioinformatics

We can only expect translation to get better from here on and this has made me committed to tackle the most serious issue now facing bioinformatics: Technical debt. In short, quick solutions need to be paid for by doing work in the future, and many issues are familiar:

  • Containers are needed to handle version conflicts. Because we haven’t updated old code, and the languages typically used can’t mix versions of dependencies
  • Various ad hoc solutions are needed to integrate code across different languages. Because labs have just kept churning out software in whatever language was available at the time
  • Packaging systems suddenly become unavailable. Everyone seems keen on making their own packaging systems. OS X has had too many, and even on Linux we just can’t seem to decide. So then we add a package manager to circumvent the system-wide package manager.
  • Poor documentation takes time from research. Who hasn’t spent hours trying to figure out how to use some obscure code?
  • Bugs take time from research. No need to comment

In short, Bioinformatics is built on the foundations of mud.

Those burning for bioinformatics infrastructure are well aware of the root problems:

  1. There is no funding for software maintenance
  2. There are few people with sufficient skill to maintain the software, and willing to accept academic salaries
  3. Those people are forced to leave academia at some point
  4. Maintaining code is not a fun job, even on the best of days
  5. There is little value in publishing about maintenance. Even while I think there is a lot of value in it, I frankly don’t even see a point in it - I have no interest in reading those papers, nor writing them
  6. When new papers about software are released, it is unclear how to handle the author list to make citations fair. The stick measuring contest just isn’t designed for software infrastructure

Computer hardware changes will make the technical debt acute

Back in 1970s, Cray was the cool company, producing super computers based on vector processors. Many bet their careers that this was the future. They are now gone. Later in the 90s, people were betting on 10+ GHz clock frequency CPUs; this would mean that classic algorithms could keep getting faster and we would not need to parallelize our code. They are now gone. Now graphics cards are becoming main stream and if you are still writing code for CPUs, you likely need to plan your retirement. But should we bet on CUDA? Likely not - deterministic tensor streaming processors are already here. Though I bet that’s just the beginning.

A lesson everyone learns is that more code is not better; rather, every line of code is a liability, because someone has to maintain it. This means that we shouldn’t grow too attached to the code we produce - it’s all temporary. At some point the code needs updating to fit modern needs, or it has to go into the trash bin (or museum).

Bioinformatics code sometimes looks a bit like something from a museum. Plenty of code rather reminds one of that 386 still taking 5.25 inch floppy disks (or Windows XP machine still running that instrument). It works for now but everyone dreads the day it stops running. While we can keep the old computer clean, it doesn’t change that it is still old hardware that still causes problems (can’t be connected to the network; need a floppy disk reader to transfer data). We shouldn’t confuse “it runs” with “this is modern infrastructure”. The older the technology gets, the more expensive it gets to keep running. Very little of current bioinformatics software can run on the many GPU-based computing centers now being built - and that’s going to be a problem in the future.

I see two options forward here:

  1. Refactor old code to make it fit on modern hardware
  2. Write new software

Since a lot of thought has gone into existing software, I believe refactoring should be the first step. AI is really good at parallelizing old code. If the code relies on matrix calculations then AI will make it run on CUDA in a split second. I think there is plenty of potential for AI to prolong the lifetime of code. Though in the end, we likely do have to accept that new hardware (like TPUs) will require entirely different algorithms (and thus software) to run efficiently.

R and Python (and Matlab) are fixable technical debt

There are especially three things that always result in technical debt:

  • Lack of documentation
  • Lack of unit tests
  • Poor performance

We can all do better in these areas, but I argue that dynamically typed languages are especially bad; and they also happen to be the most common languages in bioinformatics!

Here is a function in R. I wrote the code a few years ago, my memory of it is poor:

do_something <- function(a, b) {
  a + b
}

So I might call the function like this, which I think was the appropriate way:

do_something(5, "foo")

But this was wrong, giving a cryptic error:

Error in a + b : non-numeric argument to binary operator

Luckily I didn’t have to wait for an hour for error at least. Knowing better, turning into a serious R library developer, I would rather write the code like this:

do_something <- function(a, b) {
  stopifnot(is.double(a))
  stopifnot(is.double(b))
  a + b
}

Although the user will not see the stopifnot-statements, at least the code will crash early with a bit less cryptic errors:

Error in do_something(5, "foo") : is.double(b) is not TRUE

But compare this to how Rust forces you to do it:

pub fn do_something(a: f64, b: f64) {
    // ...
}

Here variables are typed right away. You won’t even get to run the code - Rust gives an error during compilation stage, so you will never have to wait for a mid-run explosion to know you did it wrong

error[E0308]: mismatched types
 --> src/main.rs:9:21
  |
9 |     do_something(5, "foo");
  |     ------------    ^^^^^ expected `f64`, found `&str`
  |     |
  |     arguments to this function are incorrect
  |
note: function defined here
 --> src/main.rs:1:8
  |
1 | pub fn do_something(a: f64, b: f64) {
  |        ^^^^^^^^^^^^         ------

What Rust does here is called static typing, enabling your tools (compiler/editor) to ensure that your code is fine. In other words, a great burden is taken off the programmer! In fact, the types serve two purposes:

  • Each type is documentation for what argument is expected. More specific types are better documentation (i.e., is this a path to a HMMER database file?)
  • Typed variables are unit tests. As your code will have many types defined, it means your code will have a great number of tests

In other words, static typing reduces technical debt. Corollary: any dynamically typed language drives technical debt.

Python is playing a game of catch-up here, as the maturing Python community has realized the short comings. Python has type hints, which I strongly recommend everyone to use. But they aren’t great as they are optional, and the Python interpreter cannot make faster code using type hints. Again, poor performance is technical debt.

Python and R gets worse as they don’t have good memory handling. Here is a class one could define to contain IDs from Ensembl:

class EnsemblID:
    def __init__(self, x):
        self.x = x

While you can use this class instead of a bare string, your code will become slow and use more memory, as it will now instead keep a pointer to a class, which is a dictionary having a key-value for “x”, and then a pointer from there to the actual string. That’s a lot to track. In Rust, the compiler just turns all of this back to a regular string. This is called a zero-cost abstraction and can be used to ensure you never mix up what type of ID to send where. So in Rust, you can add this guardrail for free; in Python/R, you get to choose between poor performance or poor documentation. Technical debt in either case.

No, Cython will not help you here. The Python memory model prevents any serious speedup as data is boxed by default. But you will get the fun of having to learn two languages (Python and Cython) instead of one, neither of which actually solves the technical debt.

Python also cannot always wrap C++ code. Python has made a career as “a glue language”, but it falls short of handling C++ templates, which are plenty in serious libraries (ITK is an example). This requires nasty workarounds such as SimpleITK, coming with horrid build times - and with no way of providing or inlining callback functions.

Younger generations of bioinformaticians have had, in general, by far shorter training in computers than previous generations. The average bioinformatician of today did a wetlab PhD, concluded that it wasn’t for them (or saw the opportunities), and did a 2 year postdoc picking up R and Python - possibly in tandem to still pipetting. Given the expectations for rapid academic careers, it’s unfair to expect the same CS background as those who effectively were born with a computer, and maybe even did a whole CS degree. However, I see that LLMs can help bridge this generation to use a better language like Rust, despite the significant bar that comes with it.

An open question is how to use LLMs the best way. The young ones will need to figure this out (I already know how to code; we all need to pick our own journey). But I think this especially affects how we should teach bioinformatics - Instead of teaching for R and Python, it now likely makes more sense to pick one and rather get at depth with CS concepts such that the fundamental understanding is there (how does a CPU work? an operating system?). I can spend a lifetime learning all the languages of the world, or I can focus on general linguistics and have the computer support me when I need a particular one. It will take time to get used to but it seems to be the way forward.

Before closing this section: I do not think it is completely time to abandon R and Python. Rust does not have a solid “interactive mode”, enabling quick prototyping of code with data loaded into memory. This is definitely a strength inherent to dynamically typed languages. That said, GHCI (the Glasgow Haskell Compiler-interpreter) actually pulled off interpreted mode statically typed Haskell. So I think “not yet” likely means “in a few years”. But there’s clearly work to be done before the average bioinformatician can move to Rust without a fair bit of activation energy (not to say, we still need to have the most important libraries such as DESeq/EdgeR ported).

Java can finally retire

I spent my whole PhD writing code in Java. It’s a decent language and I have a bit of soft spot for it - the tooling is good and it has a static type system (almost). Still, it has been popular in the image analysis community due to ImageJ. Image analysis really requires performance, and Java was at the time the less scary alternative to C++. However, since the introduction of GPU-computing, Python has had an odd upswing in this domain. Python works here because the actual computation is done in C++/on GPU (Python is fast whenever the code is not actually in Python).

I think with Rust, Java has nothing more to offer. Java has three major problems:

  • Java has the worst ever implementation of floating point. Don’t trust p-values coming out of Java code. Don’t ever implement numerical code unless you really know what you are doing

  • Java implements generics by erasure. I.e., HashMap<Integer> will map to HashMap<Object> for historic reasons. And then it will point to huge objects holding integers, rather than actually just a list of int. Major overhead. The Java engineers are definitely fighting for their life to somehow bridge this historical mistake but it’s not that easy if you want backwards compatibility

  • It is hard to mix Java with other languages due to its virtual machine

Good news is that Java is especially easy to translate to Rust; and if you insist on using R or Python, you can easily call Rust code using FFI!

We can finally fix the web

There has been a push the past 10(?) years to get bioinformatics onto the web. However, the web is an awful place to be, requiring a very different skillset than data analysis. Besides the intricacies of HTTP, HTTPS/SSL, DNS and other low-level plumbing, it also requires the use of Javascript. This is a language where even the equal (==) operator is broken. All my complaints about the bioinformatics ecosystem fall flat in the face of the web ecosystem. But now we have been tasked to also integrate bioinformatics and the web - and my employer does not provide enough mental healthcare for this to add up.

Luckily, Rust can compile into webassembly, and webassembly can (mostly) replace Javascript. So that’s one less (broken) language to learn. We have made three websites so far (1, 2, 3), just to get an idea, and it has been amazing to work with. Most of the times, the website actually ran when it compiled - a far stretch from my JS experience.

One of the most amazing feature is that I don’t need to worry about how to serialize the data for transfer. The code holding the common data is shared between back and front-end. Any type errors are caught during compile time, so I don’t waste time looking at a white blank page.

The next amazing feature is that we now don’t need to worry about if the code runs on backend or frontend. I can take any translated software, and run it client side. This is crucial because our academic budget does not offer scalable server backends - we need to ask users to foot the bill as much as possible. There has been ways to deploy code client side before, of course, but now it’s friction free (i.e., technical debt-free).

Want to learn the foundations of programming?

Summer is here, so maybe take some time getting back to fundamentals? There are two schools that all converge once you master programming:

  • The bottom-up hardware-oriented school: Learn C. It gives you a basic idea of how code works at the almost-lowest level - but you can still be productive. This is enough to later learn how operating systems work. Or if you don’t care about that, learn assembly. You will truly know how a CPU works after that

  • The symbolic mathematical school: Learn Haskell. All of computing can be represented as abstract mathematical evaluations, for the compiler to work out. This enables mathematical reasoning about the code and different programming patterns. While you might never use Haskell, you will never look at problems the same way again

In my view, Rust is effectively an unholy marriage of C + Haskell. So having a foot in both schools will help you get into Rust, which is a rather pragmatic construction with too many rules for being suited as the first language to learn.

Hopefully this has been a useful read. I have always avoided blogging and social media because it is such a time drain. But here’s a shout out to the OMGenomics channel who finally convinced me that I need a blog.

/J