DEV Community

Cover image for I Built a Desktop COBOL Migration Tool That Converts to 6 Modern Languages
Mecanik1337
Mecanik1337

Posted on

I Built a Desktop COBOL Migration Tool That Converts to 6 Modern Languages

COBOL migration is the hottest topic in enterprise tech right now. Anthropic's Claude Code blog post knocked $40 billion off IBM's market cap in a single day. Banks are panicking about the "COBOL cliff" as 92% of COBOL developers approach retirement by 2030.

I spent the last six months building a COBOL migration tool that takes a different approach from the AI-powered solutions making headlines. Easy COBOL Migrator is a desktop app that transpiles COBOL to C++ 17, Java 17, C# 12, Python 3, Rust and Go. It uses a full compiler pipeline, not an LLM. Everything runs offline. Your source code never leaves your machine.

Why not use AI for COBOL migration?

For a 200-line program, pasting COBOL into ChatGPT works fine. For a 200,000-line codebase with 300 shared copybooks and COMP-3 packed decimal arithmetic, AI-based COBOL migration breaks down for three reasons.

The output is non-deterministic. Same COBOL in, different code out. Every time. Try reviewing or diffing that across a project with 500 files. A deterministic compiler gives you the same output for the same input, which means your migration results are auditable and reproducible.

Decimal precision gets destroyed. COBOL's PIC 9(7)V99 COMP-3 does exact fixed-point math using binary-coded decimal. LLMs routinely map this to double or float. That introduces rounding errors. A bank finding a 0.01 discrepancy in reconciliation doesn't file a bug report. They file a regulatory incident.

Copybook resolution doesn't exist. Real COBOL codebases use COPY ... REPLACING to share data definitions across hundreds of programs. The same copybook might be included 50 times with different substitutions. No LLM has a mechanism to resolve this across a whole project.

AI is great for understanding COBOL. It's not reliable for converting it at scale with deterministic results.

How the COBOL migration pipeline works

The architecture is a traditional multi-stage compiler:

COBOL Source
  > COPY Preprocessor (resolves copybooks, applies REPLACING)
  > Lexer (220+ keywords, fixed/free format auto-detection)
  > Parser (recursive descent, 36 statement types, full AST)
  > Semantic Analyzer (symbol tables, type checking, scope validation)
  > Code Generator (one per target language)
  > Generated Source + Migration Report
Enter fullscreen mode Exit fullscreen mode

Each stage is a separate C++ class. The parser builds a complete AST with expression trees, condition nodes and hierarchical data structures. The semantic analyzer validates everything before code generation starts.

The hardest parts of building a COBOL migration tool

PIC clauses are their own language. PIC S9(7)V99 COMP-3 means signed, 7 integer digits, 2 decimal digits, stored as packed BCD. Mapping this correctly to BigDecimal in Java, decimal in C#, decimal.Decimal in Python, f64 in Rust, float64 in Go and a custom Decimal type in C++ took weeks of careful work. This is where most COBOL migration tools silently break.

PERFORM THRU is cursed. PERFORM PARA-A THRU PARA-C executes all paragraphs from A through C in source order, including whatever falls between them. It's implicit, position-dependent control flow with no equivalent in any modern language. I generate sequential paragraph calls, which is correct as long as paragraph order is preserved.

Level numbers create invisible nesting. COBOL doesn't use braces or indentation for data structures. 01 starts a record, then 05, 10, 15 underneath are children based purely on numeric hierarchy. Miss this relationship and your struct generation produces flat fields instead of nested structures.

EVALUATE TRUE is not a switch statement. Each WHEN clause is an independent boolean condition, not a value comparison. I spent a week generating switch statements before realizing the correct output is if/else if.

Fixed-format column rules are unforgiving. Columns 1-6 are sequence numbers. Column 7 is an indicator (* for comment, - for continuation, D for debug). Columns 8-72 are code. Columns 73-80 are identification that gets ignored. Accidentally reading column 73 as code corrupts your parse on every single line.

What the COBOL migration output looks like

This COBOL:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. PAYROLL-CALC.

       DATA DIVISION.
       WORKING-STORAGE SECTION.

       01  WS-EMPLOYEE.
           05  WS-NAME            PIC X(25).
           05  WS-HOURS           PIC 9(3)V99.
           05  WS-RATE            PIC 9(3)V99.
           05  WS-GROSS-PAY       PIC 9(5)V99.
           05  WS-TAX             PIC 9(5)V99.
           05  WS-NET-PAY         PIC 9(5)V99.

       01  WS-TAX-RATE            PIC V99 VALUE 0.20.
       01  WS-OVERTIME-MULT       PIC 9V9 VALUE 1.5.
       01  WS-OVERTIME-THRESHOLD  PIC 9(3) VALUE 40.
       01  WS-OVERTIME-HOURS      PIC 9(3)V99 VALUE 0.
       01  WS-REGULAR-HOURS       PIC 9(3)V99 VALUE 0.
       01  WS-COUNTER             PIC 9(2) VALUE 0.

       01  WS-PAY-GRADE           PIC 9 VALUE 0.
           88  JUNIOR             VALUE 1.
           88  SENIOR             VALUE 2.
           88  MANAGER            VALUE 3.

       PROCEDURE DIVISION.
       MAIN-PROGRAM.
           MOVE "Alice Johnson" TO WS-NAME
           MOVE 45.00 TO WS-HOURS
           MOVE 32.50 TO WS-RATE
           MOVE 2 TO WS-PAY-GRADE

           PERFORM CALCULATE-PAY
           PERFORM DISPLAY-PAYSLIP
           PERFORM DISPLAY-GRADE

           STOP RUN.

       CALCULATE-PAY.
           IF WS-HOURS > WS-OVERTIME-THRESHOLD
               COMPUTE WS-REGULAR-HOURS =
                   WS-OVERTIME-THRESHOLD
               SUBTRACT WS-OVERTIME-THRESHOLD FROM WS-HOURS
                   GIVING WS-OVERTIME-HOURS
               COMPUTE WS-GROSS-PAY =
                   (WS-REGULAR-HOURS * WS-RATE) +
                   (WS-OVERTIME-HOURS * WS-RATE *
                    WS-OVERTIME-MULT)
           ELSE
               COMPUTE WS-GROSS-PAY =
                   WS-HOURS * WS-RATE
           END-IF

           MULTIPLY WS-GROSS-PAY BY WS-TAX-RATE
               GIVING WS-TAX ROUNDED

           SUBTRACT WS-TAX FROM WS-GROSS-PAY
               GIVING WS-NET-PAY.

       DISPLAY-PAYSLIP.
           DISPLAY "================================"
           DISPLAY "  PAYROLL SUMMARY"
           DISPLAY "================================"
           DISPLAY "Employee:  " WS-NAME
           DISPLAY "Hours:     " WS-HOURS
           DISPLAY "Rate:      " WS-RATE
           DISPLAY "Gross Pay: " WS-GROSS-PAY
           DISPLAY "Tax (20%): " WS-TAX
           DISPLAY "Net Pay:   " WS-NET-PAY
           DISPLAY "================================".

       DISPLAY-GRADE.
           EVALUATE TRUE
               WHEN JUNIOR
                   DISPLAY "Grade: Junior"
               WHEN SENIOR
                   DISPLAY "Grade: Senior"
               WHEN MANAGER
                   DISPLAY "Grade: Manager"
               WHEN OTHER
                   DISPLAY "Grade: Unknown"
           END-EVALUATE.
Enter fullscreen mode Exit fullscreen mode

Becomes this C++:

// Transpiled from COBOL program: PAYROLL-CALC
// Generated by Easy COBOL Migrator on 2026-03-16 07:09:47

#include <iostream>
#include <string>
#include <cmath>
#include <cstdlib>
#include <cstdint>

// COBOL string move helper: truncate or right-pad to field width
inline std::string cobolMove(const std::string& src, int len) {
    std::string r = src; r.resize(len, ' '); return r;
}

// COBOL string comparison: pad shorter operand with spaces
inline int cobolCmp(const std::string& a, const std::string& b) {
    size_t len = std::max(a.size(), b.size());
    std::string la = a, lb = b;
    la.resize(len, ' '); lb.resize(len, ' ');
    return la.compare(lb);
}

void main_program();
void calculate_pay();
void display_payslip();
void display_grade();

// WORKING-STORAGE variables
struct {
    std::string ws_name = std::string(25, ' ');
    double ws_hours = 0.0;
    double ws_rate = 0.0;
    double ws_gross_pay = 0.0;
    double ws_tax = 0.0;
    double ws_net_pay = 0.0;
} ws_employee;
double ws_tax_rate = 0.20;
double ws_overtime_mult = 1.5;
int ws_overtime_threshold = 40;
double ws_overtime_hours = 0;
double ws_regular_hours = 0;
int ws_counter = 0;
int ws_pay_grade = 0;
inline bool junior() { return ws_pay_grade == 1; }
inline bool senior() { return ws_pay_grade == 2; }
inline bool manager() { return ws_pay_grade == 3; }

int64_t return_code = 0;

void main_program() {
    ws_employee.ws_name = cobolMove("Alice Johnson", 25);
    ws_employee.ws_hours = 45.00;
    ws_employee.ws_rate = 32.50;
    ws_pay_grade = 2;
    calculate_pay();
    display_payslip();
    display_grade();
    exit(0);
}

void calculate_pay() {
    if (ws_employee.ws_hours > ws_overtime_threshold) {
        ws_regular_hours = ws_overtime_threshold;
        ws_overtime_hours = ws_employee.ws_hours - (ws_overtime_threshold);
        ws_employee.ws_gross_pay = ((ws_regular_hours * ws_employee.ws_rate) + ((ws_overtime_hours * ws_employee.ws_rate) * ws_overtime_mult));
    } else {
        ws_employee.ws_gross_pay = (ws_employee.ws_hours * ws_employee.ws_rate);
    }
    ws_employee.ws_tax = std::round((ws_employee.ws_gross_pay * ws_tax_rate) * 100.0) / 100.0;
    ws_employee.ws_net_pay = ws_employee.ws_gross_pay - (ws_employee.ws_tax);
}

void display_payslip() {
    std::cout << "================================" << std::endl;
    std::cout << "  PAYROLL SUMMARY" << std::endl;
    std::cout << "================================" << std::endl;
    std::cout << "Employee:  " << ws_employee.ws_name << std::endl;
    std::cout << "Hours:     " << ws_employee.ws_hours << std::endl;
    std::cout << "Rate:      " << ws_employee.ws_rate << std::endl;
    std::cout << "Gross Pay: " << ws_employee.ws_gross_pay << std::endl;
    std::cout << "Tax (20%): " << ws_employee.ws_tax << std::endl;
    std::cout << "Net Pay:   " << ws_employee.ws_net_pay << std::endl;
    std::cout << "================================" << std::endl;
}

void display_grade() {
    if (junior()) {
        std::cout << "Grade: Junior" << std::endl;
    } else if (senior()) {
        std::cout << "Grade: Senior" << std::endl;
    } else if (manager()) {
        std::cout << "Grade: Manager" << std::endl;
    } else {
        std::cout << "Grade: Unknown" << std::endl;
    }
}

int main() {
    main_program();
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Which compiles and runs without even polishing it:

Generated by Easy COBOL Migrator on 2026-03-16 07:09:47

The migration report flags any constructs that need manual attention, like EXEC SQL blocks or dynamic CALL targets.

COBOL migration coverage

The parser handles all four COBOL divisions:

  • 36 statement types including MOVE, COMPUTE, IF/EVALUATE, all PERFORM variants including THRU and VARYING with AFTER, SORT/MERGE with key-field extraction, STRING/UNSTRING, INSPECT, SEARCH/SEARCH ALL, CALL with static and dynamic dispatch, GO TO DEPENDING ON, and RELEASE/RETURN for sort procedures
  • Full data description with levels 01-88, OCCURS DEPENDING ON, REDEFINES, RENAMES, FILLER, all PIC/USAGE variants, and MOVE/ADD/SUBTRACT CORRESPONDING
  • LINKAGE SECTION transpilation for subprogram interfaces with CALL BY REFERENCE, BY CONTENT and BY VALUE parameter passing
  • File I/O with OPEN/CLOSE/READ/WRITE/REWRITE/DELETE/START, record packing/unpacking, seek-based in-place update and FILE STATUS tracking
  • COPY preprocessor with nested copybooks up to 10 levels, REPLACING with pseudo-text substitution and circular-include detection
  • 40+ intrinsic functions mapped to native equivalents in all 6 languages
  • EXEC SQL, EXEC CICS and EXEC DLI blocks preserved as comments with migration notes
  • Built-in analysis tools: dead code detection, cross-reference maps, complexity metrics and COBOL source utilities

Known limitations: indexed/relative file random/dynamic access converts to sequential with a migration note recommending database replacement. Report Writer and Screen Section are not supported.

Who needs a COBOL migration tool?

  • Migration consultants who want to accelerate their conversion projects
  • Development teams at banks, insurers or government agencies migrating off mainframes
  • Solo developers contracted for COBOL modernization work
  • Anyone curious about what a 1959 programming language looks like as Rust

Demo

There's a free demo that converts single files up to 500 lines to C++ output.

Product page

If your organisation needs help with the parts the tool can't convert, like EXEC SQL replacement, database re-platforming or end-to-end migration, I also offer hands-on COBOL migration consulting.

498 automated tests. 6 languages. One developer in London who now knows more about COBOL than he ever planned to.

I'd love to hear what breaks. Drop your weirdest COBOL in the demo and let me know.

Top comments (0)