Decompiled Source Code Is Evidence, Not Ground Truth

Decompiled Source Code Is Evidence, Not Ground Truth
I was porting an old Flash-based NES game randomiser to a modern browser application.
The requirement was strict. Users would provide their own ROM, and the JavaScript version needed to preserve the original byte-level behaviour. A result that merely looked similar was not good enough. One changed operation could generate a broken game.
The decompiler produced readable ActionScript. It looked plausible, translated cleanly, and passed JavaScript syntax checks.
Then the map generator ran far longer than the original bounded loop should have.
The code was readable. It was also wrong.
A decompiler reconstructs intent
Compiled software does not contain the original source file in the form its author wrote it. Variable names may survive, but formatting, comments, and some high-level structure do not.
A decompiler takes lower-level instructions and tries to rebuild readable source code from them. That reconstruction can be remarkably good. It can also make small choices that change meaning.
In this case, the readable output had lost parentheses in a retry condition. The original bytecode applied a 100-attempt limit across two checks. The reconstructed expression made that limit apply differently.
Both versions looked like ordinary code. Only one matched the program.
Readability created false confidence
If the decompiler had produced a mess, I would have treated every line with suspicion. The danger was that it produced something clean.
Clean code invites normal source-code reasoning:
- The condition looks sensible.
- The loop has a counter.
- The file compiles.
- The names match the original interface.
Those observations are useful, but they do not establish equivalence.
The first real warning came from runtime behaviour. A loop that should have stopped after a known number of attempts kept going. Instead of patching the JavaScript until it "felt right," I went back to the lower-level p-code exported from the Flash file.
That showed the actual order of operations and where the bound applied.
Cross-language differences added more traps
The missing parentheses were not the only problem. ActionScript and JavaScript look related, but similar syntax can hide different runtime behaviour.
One translated loop used JavaScript's for...in over an array. JavaScript supplied string property keys, while the original algorithm expected numeric indices.
Nested functions also handled this differently. Code that referred to the surrounding object in ActionScript could point somewhere else after a direct JavaScript translation.
Other areas worth checking in this kind of port include:
- integer overflow and coercion
- signed versus unsigned values
- array enumeration order
- random-number generation
- byte writes
- operator precedence
- object and function binding
A syntax checker catches none of these. It can prove that JavaScript accepts the file. It cannot prove that JavaScript performs the same operations.
Use three sources of truth
For a careful port, I now think in terms of three kinds of evidence.
Readable decompiler output explains the likely structure and makes the program understandable.
Lower-level instructions settle questions about operation order, branches, coercion, and byte access.
Observed runtime behaviour tells you whether the reconstructed understanding matches what the original program actually does.
No single one is enough on its own.
Readable output without bytecode can hide a reconstruction error. Bytecode without readable structure is slow and easy to misinterpret. Runtime testing without either may tell you that something differs but not why.
Together they form a much stronger reference.
Preserve behaviour before improving style
There is a strong temptation during a port to clean things up. Old compiled code often contains awkward loops, strange names, and patterns that look unnecessary in a modern language.
I avoided that at first.
The initial goal was not beautiful JavaScript. It was a faithful JavaScript version whose behaviour could be compared with the Flash application.
Once equivalence is established, the code can be refactored behind tests. Cleaning it while translating mixes two difficult jobs:
- understanding what the original does
- deciding how the new version should express it
When the result differs, you no longer know which job introduced the fault.
The verification gap remains important
The browser version eventually ran on a dummy buffer, changed ROM bytes, and loaded without console errors. Those were useful smoke tests.
They did not prove that it generated the same playable ROM as the Flash version.
True verification still requires a legitimate user-supplied ROM, the same seed in both programs, a byte-for-byte comparison of the outputs, and a real emulator test.
That distinction matters enough to say plainly. A working interface is not proof of a faithful port.
Treat the output as a hypothesis
Decompiler output is one of the most useful starting points for recovering old software. Without it, this port would have taken far longer.
But it should be treated as a well-informed hypothesis, not the original truth.
When the readable code and the runtime disagree, believe the behaviour and inspect the lower-level instructions. The prettiest reconstruction is still a reconstruction.