Resurrecting COBOL with receipts: how we built COBOL Phoenix
This post is written as part of the Kiroween 2026 hackathon. Every legacy-modernization pitch promises to “convert your COBOL”. Converting is not the problem. The problem is the question every architect asks afterwards: how do we know the new code behaves exactly like the old one? COBOL Phoenix is our answer: understand the program deterministically, generate code you can trace line by line, and prove behaviour by running the original and the generated code side by side. Rule 1: no guessing in the core We wrote a real COBOL front end in Python — a fixed/free-format lexer, a COPY … REPLACING preprocessor and a recursive-descent parser — producing an intermediate representation where every node carries a file and line range. Business rules, the dependency graph and the generated code are all derived from that IR. Anything outside the supported subset (file I/O, INSPECT, ALTER, EXEC SQL…) is reported with its location and becomes an explicit UnsupportedConstruct in the generated code. Nothing is silently dropped. Rule 2: COBOL data is bytes The generated Python runs on a small runtime where each record is a bytearray and each data item a typed view. That single decision makes REDEFINES, group MOVEs and CALL BY REFERENCE behave like COBOL instead of like Python dictionaries. Numbers are Decimal, with COBOL's truncation, ROUNDED (half away from zero), sign overpunch and numeric-editing rules — each confirmed against GnuCOBOL with tiny probe programs. Rule 3: prove it For a callable program we generate a COBOL harness that ACCEPTs each USING parameter, CALLs the program and DISPLAYs the results; the generated Python CLI speaks the same protocol. Identical bytes go into GnuCOBOL (inside a container with no network, a read-only filesystem and resource limits) and into Python. Outputs are decoded with the record layout and compared field by field. Test inputs come from fixtures plus values derived from the code itself: every comparison against a literal yields boundary values, every 88-level and EVALUATE WHEN yields matching and non-matching values. Then we got smarter: we trace the fixtures through the generated Python and attach each boundary value to a fixture that actually executes the statement it came from — coverage-guided placement. On a public insurance repository that raised the mutation score from 67% to over 80% without a single hand-written test. The best bug was our own An edge case with the maximum vehicle value failed. GnuCOBOL printed $04,059.41; our runtime printed $4,059.41. When significant high-order digits are truncated away, GnuCOBOL considers zero suppression already finished. We would never have found that by reading documentation. Differential testing found it in seconds. Trust, measured Passing cases are evidence, not proof, so we measure the tests themselves: mutation analysis generates single-fault variants (drop a ROUNDED, flip a < to
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to