COBOL + ANTLR = JAVA
COBOL + ANTLR = JAVA

Введение

Когда я работал в одной зарубежной компании, мне поставили задачу мигрировать COBOL-систему расчета инвойсов с мейнфрейма на Java. Она звучала довольно просто: «Нужно переписать старый COBOL-код на Java, чтобы система жила дальше».

Я тогда подумал, что это будет очередная рутина - взять дремучее легаси, аккуратно переложить его на современную версию Java, возможно, чуть подчистить архитектуру, внести небольшие косметические правки и закрыть задачу. В голове уже созрел рабочий план: пара итераций по автогенерации кода с помощью нейронки и готово. В общем, казалось, что ничего особенного.

Cobol to Java ? Easy!
Cobol to Java ? Easy!

Каково же было моё удивление, когда я увидел реальные объёмы и связанность компонентов этого легаси. Сотни модулей на COBOL, вызывающие друг друга, десятки лет эволюции, copybook«и, которые тянутся через полсистемы, логика, размазанная между PERFORM, GO TO и неочевидными условиями. Стало ясно, что весь этот код физически не поместится в контекст ни одной из существующих нейронок, а даже если и поместится — понять его целиком она не сможет. Отсюда следует, что нужно было придумать какое‑то оригинальное нетривиальное решение для данной инженерной задачи. Именно в этот момент и возникло желание поделиться накопленным опытом в данной статье. Не как универсальным рецептом, а как инженерным путём — с ошибками, тупиками и выводами, потому что рано или поздно подобная задача может возникнуть еще у кого-либо: в банках, телекоме, страховании, госсекторе.

Первая попытка — вручную, «по-честному»

Сначала я решил по-честному попробовать переписать хотя бы один модуль вручную. Взял относительно небольшой COBOL-файл, начал читать PROCEDURE DIVISION, разбираться в WORKING STORAGE и FILE STORAGE, искать, откуда приходят данные и куда они потом уходят.

Очень быстро выяснилось, что:

  • один модуль почти никогда не живёт сам по себе;

  • за ним тянется цепочка copybook’ов;

  • бизнес-логика зависит от глобального состояния;

  • понимание потока выполнения требует держать в голове полпрограммы.

Через несколько дней стало очевидно: даже если переписывать идеально, этот подход не приведёт к результату за разумное время. Слишком медленно, слишком дорого и слишком рискованно.

Вторая попытка — с помощью AI

Следующим шагом стало использование нейронок. Казалось логичным: COBOL — текст, Java — текст, значит, можно просто транслировать исходники кусками. На маленьких фрагментах это даже выглядело многообещающе: нейронка аккуратно превращала DISPLAY в вызовы логгера, IF — в if, иногда даже приводила код в приличный вид.

Но стоило попробовать применить этот подход не к игрушечному, а к реальному коду, как вскрылись ограничения:

  • AI терял контекст между файлами;

  • copybook’и интерпретировались каждый раз по-разному;

  • сложные PERFORM THRU разваливались;

  • один и тот же вход мог давать разный результат.

Самое неприятное — результат нельзя было формально проверить. Код выглядел правдоподобно, но уверенности, что бизнес‑логика сохранена полностью, не было. Для business critical системы это означало экстремально высокую вероятность наличия ошибок, что недопустимо.

Осознание проблемы

В какой-то момент стало ясно, что проблема не в COBOL, не в Java и даже не в инструментах трансляции кода. Проблема в том, что я пытался мигрировать текст, а не язык программирования.

COBOL — это формальный язык со строгой грамматикой и предсказуемой структурой. Пока транслятор не понимает эту структуру, любая миграция либо превращается в ручной труд, либо остаётся вероятностной.

Почему ANTLR — основа надёжной миграции

ANTLR parser
ANTLR parser

После того как стало ясно, что без формального понимания COBOL дальше не продвинуться и задача миграции сводится к практическому инженерному вопросу: как вообще должна выглядеть архитектура такого транслятора COBOL кода ?

Важно было не просто «распарсить код», а выстроить процесс, который:

  • масштабируется на сотни и тысячи COBOL модулей;

  • даёт воспроизводимый результат;

  • позволяет проверять корректность миграции;

  • не привязан жёстко к конкретной версии Java или COBOL-диалекту.

Вот здесь и появляется парсер ANTLR (ANother Tool for Language Recognition), который оказывается удобной точкой сборки для такой архитектуры, а также отвечает всем указанным требованиям.

ANTLR является генератором лексеров и парсеров, который позволяет формально описать транслируемый язык и получать его структурное представление.

Архитектура ANTLR-подхода к миграции

В упрощённом виде пайплайн миграции выглядит так:

ANTLR translator pipeline
ANTLR translator pipeline

Каждый этап решает свою задачу и изолирован от остальных. Это принципиально важно: ошибки и изменения на одном уровне не должны «протекать» дальше по пайплайну.

1. Preprocessing: подготовка COBOL-кода

COBOL preprocessing
COBOL preprocessing

До того, как ANTLR вообще сможет что-либо разобрать, COBOL-код нужно привести в форму, пригодную для парсинга. На практике это означает:

  • обработку COPY и REPLACING;

  • разворачивание copybook’ов в единый текст;

  • работу с fixed-format (колонки 1-6, 7, 8-72);

  • нормализацию кодировки и строк.

Этот этап часто недооценивают, но именно здесь отсеивается значительная часть проблем.

Важно понимать: ANTLR парсит уже «собранный» COBOL, а не хаотичный набор файлов.

2. Грамматика COBOL

В основе всего лежит грамматика. ANTLR-грамматика описывает:

  • структуру DIVISION / SECTION / PARAGRAPH;

  • синтаксис управляющих конструкций (IF, EVALUATE, PERFORM);

  • объявления данных в DATA DIVISION;

  • специфические конструкции диалектов (CICS, EXEC SQL и т. д.).

Ключевой момент:

грамматика — это не эвристика и не «примерное понимание языка», а формальное описание, по которому ANTLR строит парсер.

Это принципиальное отличие от AI-подхода, где понимание языка вероятностное.

3. Lexer и Parser

На основе грамматики ANTLR генерирует:

  • лексер, который превращает текст в токены;

  • парсер, который строит parse tree.

На этом этапе:

  • выявляются синтаксические ошибки;

  • код получает чёткую структуру;

  • исчезает неоднозначность интерпретации.

Важно: parse tree — это ещё не то, с чем удобно работать. Это лишь отражение грамматики, а не смысла программы.

4. Построение AST (Abstract Syntax Tree)

AST
AST

Следующий шаг — преобразование parse tree в AST (Abstract Syntax Tree).

AST — это уже инж енерная модель программы, а не отражение грамматики. В AST:

  • объединяются логически связанные конструкции;

  • нормализуется управление потоком;

  • убираются синтаксические детали, не важные для семантики.

Например:

  • PERFORM PARAGRAPH превращается в узел вызова;

  • IF/ELSE — в единый условный блок;

  • группы данных — в иерархию структур.

Именно AST становится центральной точкой всей миграции.

5. Семантический анализ

После того как структура программы понятна, начинается самый важный этап — семантика.

Здесь решаются вопросы:

  • где объявлена переменная и в какой области видимости;

  • какие copybook’и реально используются;

  • какие данные модифицируются и где;

  • как устроен реальный control flow.

ANTLR сам по себе не делает семантический анализ, но он даёт идеальную основу для его реализации. В отличие от AI, здесь всё:

  • детерминировано;

  • трассируемо;

  • проверяемо.

6. Трансформация в Java

Java transformation
Java transformation

Только после этого имеет смысл переходить к генерации Java.

Преимущество ANTLR-подхода в том, что генерация:

  • не зависит от исходного текста;

  • опирается на структурную модель;

  • легко адаптируется под нужную архитектуру.

Типичные соответствия выглядят так:

  • COBOL record → Java class / record;

  • WORKING-STORAGE → state object;

  • PERFORM → метод или лямбда;

  • FILE-CONTROL → DAO / repository;

  • EXEC SQL → слой доступа к БД.

Важно, что правила генерации можно менять, не переписывая парсер и анализатор.

7. Проверяемость и воспроизводимость

Одно из главных преимуществ ANTLR-подхода — возможность проверки результата.

Поскольку:

  • парсинг детерминирован;

  • AST строится одинаково каждый раз;

  • генерация следует формальным правилам,

мы можем:

  • повторять миграцию сколько угодно раз;

  • находить полученную разницу в результатах;

  • автоматизировать тестирование;

  • уверенно утверждать, что поведение системы сохранено.

Это ровно то, чего не хватает AI-based подходам.

Пример реализации транслятора COBOL с использованием ANTLR-подхода

Далее покажу базовый пример реализации такого пайплайна миграции

Пример входной COBOL-программы:

IDENTIFICATION DIVISION.
    PROGRAM-ID. HELLO.
    PROCEDURE DIVISION.
      DISPLAY "HELLO, WORLD".
      STOP RUN.

Maven: зависимости + генерация парсера:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>ru.alfastrah</groupId>
  <artifactId>cobol-to-java-antlr</artifactId>
  <version>1.0-SNAPSHOT</version>
  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <antlr4.version>4.13.1</antlr4.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.antlr</groupId>
      <artifactId>antlr4-runtime</artifactId>
      <version>${antlr4.version}</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.antlr</groupId>
        <artifactId>antlr4-maven-plugin</artifactId>
        <version>${antlr4.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>antlr4</goal>
            </goals>
            <configuration>
              <visitor>true</visitor>
              <listener>false</listener>
              <packageName>demo.cobol</packageName>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Упрощённая ANTLR-грамматика:

grammar CobolMini;
program
 : identificationDivision procedureDivision EOF
 ;
identificationDivision
 : IDENTIFICATION DIVISION '.' PROGRAM_ID '.' progName '.' 
 ;
procedureDivision
 : PROCEDURE DIVISION '.' statement* 
 ;
statement
 : displayStmt
 | stopRunStmt
 ;
displayStmt
 : DISPLAY stringLiteral '.'
 ;
stopRunStmt
 : STOP RUN '.'
 ;
progName
 : IDENTIFIER
 ;
stringLiteral
 : STRING
 ;
IDENTIFICATION : [Ii][Dd][Ee][Nn][Tt][Ii][Ff][Ii][Cc][Aa][Tt][Ii][Oo][Nn] ;
PROGRAM_ID     : [Pp][Rr][Oo][Gg][Rr][Aa][Mm]'-'[Ii][Dd] ;
PROCEDURE      : [Pp][Rr][Oo][Cc][Ee][Dd][Uu][Rr][Ee] ;
DIVISION       : [Dd][Ii][Vv][Ii][Ss][Ii][Oo][Nn] ;
DISPLAY        : [Dd][Ii][Ss][Pp][Ll][Aa][Yy] ;
STOP           : [Ss][Tt][Oo][Pp] ;
RUN            : [Rr][Uu][Nn] ;
IDENTIFIER : [A-Za-z][A-Za-z0-9-]* ;
STRING     : '"' (~["\r\n])* '"' ;
DOT    : '.' ;
WS     : [ \t\r\n]+ -> skip ;

Java: парсинг COBOL и генерация Java-класса (Visitor):

package ru.alfastrah;
 
import ru.alfastrah.cobol.CobolMiniBaseVisitor;
import ru.alfastrah.cobol.CobolMiniParser;
 
public class CodeGenVisitor extends CobolMiniBaseVisitor<Void> {
 
    private String programName;
 
    private final StringBuilder body = new StringBuilder();
 
    @Override
    public Void visitIdentificationDivision(CobolMiniParser.IdentificationDivisionContext ctx) {
        programName = ctx.progName().getText();
        return null;
    }
 
    @Override
    public Void visitDisplayStmt(CobolMiniParser.DisplayStmtContext ctx) {
        String literal = ctx.stringLiteral().getText();
        body.append("        System.out.println(").append(literal).append(");\n");
        return null;
    }
 
    @Override
    public Void visitStopRunStmt(CobolMiniParser.StopRunStmtContext ctx) {
        body.append("        return;\n");
        return null;
    }
 
    public String toJavaSource() {
        String className = (programName == null || programName.isBlank()) ? "CobolProgram" :programName;
        return ""
            + "public class " + className + " {\n"
            + "    public static void main(String[] args) {\n"
            + body
            + "    }\n"
            + "}\n";
    }
}

Запуск: читаем COBOL, транслируем, выводим сгенерированный Java

package ru.alfastrah;
 
import ru.alfastrah.cobol.CobolMiniLexer;
import ru.alfastrah.cobol.CobolMiniParser;
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.ParseTree;
import java.nio.file.Files;
import java.nio.file.Path;
 
public class MigrateDemo {
    public static void main(String[] args) throws Exception {
        String cobol = Files.readString(Path.of("src/main/resources/sample.cob"));
        CharStream input = CharStreams.fromString(cobol);
        CobolMiniLexer lexer = new CobolMiniLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        CobolMiniParser parser = new CobolMiniParser(tokens);
        ParseTree tree = parser.program();
        CodeGenVisitor gen = new CodeGenVisitor();
        gen.visit(tree);
        String java = gen.toJavaSource();
        System.out.println(java);
    }
}

Что получится на выходе

public class HELLO {

    public static void main(String[] args) {
        System.out.println("HELLO, WORLD");
    }
}

Заключение

Оглядываясь назад, могу сказать, что миграция COBOL-системы с мейнфрейма на Java оказалась совсем вообще нетривиальной задачей, какой виделась в самом начале.

Ручной подход дал полезный, но очень дорогой опыт — он помог увидеть особенности транслируемого кода, но быстро упёрся в ограничения времени и масштаба. AI‑инструменты выглядели многообещающе и действительно помогали в мелочах, но как основа миграции они оказались слишком непредсказуемыми. Когда речь идёт о критической бизнес‑логике, «похоже на правду» — при миграции этого недостаточно.

Переломным моментом стало осознание простой вещи: COBOL — это язык программирования, а не просто текст. И, если мы хотим мигрировать систему надёжно, нам нужен инструмент, который понимает этот язык формально, а не вероятностно. Именно здесь ANTLR оказался тем самым недостающим элементом.

ANTLR не сделал задачу простой — миграция всё равно остаётся сложной инженерной работой. Но он сделал её контролируемой. Появилась структура, воспроизводимость, возможность проверять результат и масштабировать процесс. Исчезло ощущение, что мы каждый раз начинаем с нуля и надеемся, что в этот раз «прокатит».

Важно и то, что этот подход не противоречит современным инструментам. AI отлично вписывается в процесс и дополняет его — как помощник, как средство рефакторинга, как ускоритель работы разработчика. Но фундамент миграции, особенно для больших систем из финтеха, должен оставаться формальным и детерминированным.

В этой статье мне хотелось поделиться не «правильным» решением, а реальным инженерным опытом, который мне пришлось пройти на практике и собрать все подводные камни подобных трансляторов. Если кому‑то она поможет быстрее выбрать верное направление или избежать очевидных ловушек при миграции COBOL‑систем — значит, цель достигнута.

А если перед вами сейчас стоит похожая задача — не бойтесь сложности. С правильной архитектурой и инструментами она вполне решаема.