Transpilers: your codebase rewritten by a machine that understands both languages.
A transpiler translates a whole codebase from one language into another, by rule rather than by hand. The rules are written once, reviewed once, and applied identically to line 1 and line 1,900,000 — which is the only reason moving a system of that size is possible at all.
Hand-rewriting a large legacy system is a project with a famously poor record: a September 2024 Forrester survey, cited by IBM, found that nine in ten rewrite projects fail at the first attempt. The reason is rarely a shortage of talent. It is that a person reading a million lines makes a different decision on Tuesday than on Monday, and there is no way to review a million individual decisions.
A transpiler moves the decisions into one place. You review the rule, not the output of the rule, and when you change your mind you re-run it. The old system keeps running in production the entire time.
A transpiler is a language implementation, not a converter
This distinction decides whether you get a system or a very large pile of code that happens to compile.
Rewrites the text
Tokens and shapes. Pattern in, pattern out, one statement at a time, with no idea what any name in the file refers to.
Code in the target language that preserves the structure, the volume and the accidents of the original — including the ones you were migrating to escape.
Silently. It compiles, it passes the tests you wrote for it, and it disagrees with the old system on the cases nobody thought to test.
Models the semantics
Types, scopes, control flow, state and arithmetic behavior. Every name resolved to its declaration before a single line of target code is emitted.
Idiomatic code in the target language, tuned against your own codebase until your developers accept it as something they could have written.
Loudly and early, at a specific line, where you can look at it. That is the whole point of building the semantic layer.
Five stages, and none of them is optional.
Every transpiler we build has the same spine. The languages change, the rules change, the target architecture changes. The order does not.
The middle of it, in the open
Stage 03 is the part clients most often ask to see, because it is where the claim “we translate at the pattern level” either means something or does not. Below is the target model from an RPG-to-Java transpiler we publish as a teaching example: typed classes with names, not a bag of strings, defined on top of our open-source Starlasu framework.
The model, as code
data class GProgram(
override val name: String,
val globalVariables:
MutableList<GGlobalVariable> = mutableListOf(),
val mainFunction:
GFunction = GFunction("executeProgram"),
val otherFunctions:
MutableList<GFunction> = mutableListOf()
) : Node(), GNamedThe model, as a tree
GProgram name=Test
├── GGlobalVariable NBR : GIntegerType
└── GFunction executeProgram
└── GAssignment
├── GGlobalVariableTarget → NBR
└── GIntegerLiteral value=123Both are from Strumenta/rpg-to-java-transpiler, published alongside the tutorial How to write a transpiler. It is deliberately small and it is not the tooling we run on client work — but the architecture is the same one, and you can read all of it instead of taking our description on trust.
Where the difficulty actually is
Grammar coverage is the part everyone worries about and the part that gets solved. What decides whether the migrated system behaves like the old one is the semantic layer, and in legacy languages it concentrates in three places.
- State that is not where it looks like it is. RPG’s 99 indicators, for instance, are global and survive between calls unless the program explicitly ends. Translating each one into a local boolean produces code that passes unit tests and diverges in production. The transpiler has to do the data-flow analysis and emit named state.
- Memory layouts pretending to be records. Overlaid data structures let the same bytes be read as packed decimal, character or binary depending on the name used. The transpiler needs an explicit layout model and generated accessors. A byte array would be faithful and unmaintainable.
- Arithmetic that does not port. Rounding and truncation rules differ between every source and target pair we have worked on. The transpiler has to generate calls into a runtime support library rather than hope the target language agrees. Skip it and you ship a system that is right about everything except money.
The rule you can review is worth more than the output you cannot. That is the whole argument for building a transpiler instead of hiring forty developers.
Transpilers we have built
- IBM EGL to Java and Spring Boot — 1.9 million lines, for a pension fund manager, delivered through our partner CGI.
- SAS to SQL, Python, Spark and Databricks. A transpiler built on our SAS Language Engine has processed tens of millions of lines of SAS. The transpiler technology itself belongs to the client we built it for; the engine underneath it is ours.
- RPG to Java and RPG to Python, the IBM i work that our RPG modernization practice runs on, including estates where the RPG was generated by Synon / CA 2E rather than written by hand.
- COBOL to Elixir, VBA to C++, VBA to JavaScript, PL/SQL to Java, Java to Python. Some of these are proofs of concept and some went to production; we will tell you which is which for the pair you care about.
“Strumenta’s team demonstrated deep expertise in Transpiler implementation and legacy and modern programming languages. Strumenta expertise in transpiler-based migration strategies was evident throughout the engagement.”
“Strumenta has been a great partner in pursuing our objective of building a custom transpiler. They have been professional and collaborative. I would absolutely recommend them. They have been an excellent business partner, true to their word, consistent in their delivery, and consummate professionals.”
More testimonials are on the clients page, and individual projects are written up as case studies.
Why the Strumenta approach works
Four things make the difference, and all four are checkable rather than adjectival.
- Both ends are modeled, not just the source. Most tools parse the legacy language and then print target-language text. We build a model of the target too, so the output is constructed as a program and printed by something that knows the target’s syntax — which is why it comes out formatted and idiomatic rather than assembled from strings.
- The source parser usually already exists. Fifteen Language Engines are ready to license, RPG and COBOL and SAS and PL/SQL among them, each with a command-line tool so you can run it over your own sources before committing to anything. Starting a transpiler from a parser that has been through years of production use is a materially different project from starting at a grammar file.
- One AST framework across everything. Every Strumenta parser is built on Starlasu and produces a tree in a shared shape, so the source and target models in a transpiler speak the same dialect, the parsers interoperate with each other, and the trees can be exchanged through LionWeb with tooling we did not write. The models are reachable from Java, Kotlin, Python, TypeScript and C#.
- Open foundations, no lock-in. Starlasu is Apache-2.0 and public. There is no proprietary intermediate representation standing between you and your own codebase, and no license that stops working if you stop paying us.
What a transpiler does not do for you
- It does not remove the need to test. Automation changes the shape of the verification problem — you test rules and behavior rather than lines — but a migration is not finished when the code compiles. We help define the end-to-end testing strategy and provide the templates; running it in your environment is yours.
- It does not redesign your application. A transpiler moves the logic that exists. If the answer to your problem is a different system rather than the same system in a different language, we would rather tell you that at the first meeting.
- It is not free of a planning phase. The rules are tailored to your codebase, which means somebody has to read your codebase first. That work is the Migration Blueprint, and it produces a plan and a price rather than a promise.
- It does not always pay. Below a certain size, or above a certain amount of dead code, the arithmetic does not work. The Blueprint is also how you find that out cheaply.
Tell us what you are translating, and what into.
We will tell you whether we already have an engine for the source, what the semantic hard parts are for that particular pair, and whether a transpiler is the right instrument at all. If it is not, we will say so.