- PVSM.RU - https://www.pvsm.ru -
Когда я работал в одной зарубежной компании, мне поставили задачу мигрировать COBOL-систему расчета инвойсов с мейнфрейма на Java. Она звучала довольно просто: «Нужно переписать старый COBOL-код на Java, чтобы система жила дальше».
Я тогда подумал, что это будет очередная рутина - взять дремучее легаси, аккуратно переложить его на современную версию Java, возможно, чуть подчистить архитектуру, внести небольшие косметические правки и закрыть задачу. В голове уже созрел рабочий план: пара итераций по автогенерации кода с помощью нейронки и готово. В общем, казалось, что ничего особенного.
Каково же было моё удивление, когда я увидел реальные объёмы и связанность компонентов этого легаси. Сотни модулей на COBOL, вызывающие друг друга, десятки лет эволюции, copybook«и, которые тянутся через полсистемы, логика, размазанная между PERFORM, GO TO и неочевидными условиями. Стало ясно, что весь этот код физически не поместится в контекст ни одной из существующих нейронок, а даже если и поместится — понять его целиком она не сможет. Отсюда следует, что нужно было придумать какое‑то оригинальное нетривиальное решение для данной инженерной задачи. Именно в этот момент и возникло желание поделиться накопленным опытом в данной статье. Не как универсальным рецептом, а как инженерным путём — с ошибками, тупиками и выводами, потому что рано или поздно подобная задача может возникнуть еще у кого-либо: в банках, телекоме, страховании, госсекторе.
Сначала я решил по-честному попробовать переписать хотя бы один модуль вручную. Взял относительно небольшой COBOL-файл, на��ал читать PROCEDURE DIVISION, разбираться в WORKING STORAGE и FILE STORAGE, искать, откуда приходят данные и куда они потом уходят.
Очень быстро выяснилось, что:
один модуль почти никогда не живёт сам по себе;
за ним тянется цепочка copybook’ов;
бизнес-логика зависит от глобального состояния;
понимание потока выполнения требует держать в голове полпрограммы.
Через несколько дней стало очевидно: даже если переписывать идеально, этот подход не приведёт к результату за разумное время. Слишком медленно, слишком дорого и слишком рискованно.
Следующим шагом стало использование нейронок. Казалось логичным: COBOL — текст, Java — текст, значит, можно просто транслировать исходники кусками. На маленьких фрагментах это даже выглядело многообещающе: нейронка аккуратно превращала DISPLAY в вызовы логгера, IF — в if, иногда даже приводила код в приличный вид.
Но стоило попробовать применить этот подход не к игрушечному, а к реальному коду, как вскрылись ограничения:
AI терял контекст между файлами;
copybook’и интерпретировались каждый раз по-разному;
сложные PERFORM THRU разваливались;
один и тот же вход мог давать разный результат.
Самое неприятное — результат нельзя было формально проверить. Код выглядел правдоподобно, но уверенности, что бизнес‑логика сохранена полностью, не было. Для business critical системы это означало экстремально высокую вероятность наличия ошибок, что недопустимо.
В какой-то момент стало ясно, что проблема не в COBOL, не в Java и даже не в инструментах трансляции кода. Проблема в том, что я пытался мигрировать текст, а не язык программирования.
COBOL — это формальный язык со строгой грамматикой и предсказуемой структурой. Пока транслятор не понимает эту структуру, любая миграция либо превращается в ручной труд, либо остаётся вероятностной.
После того как стало ясно, что без формального понимания COBOL дальше не продвинуться и задача миграции сводится к практическому инженерному вопросу: как вообще должна выглядеть архитектура такого транслятора COBOL кода ?
Важно было не просто «распарсить код», а выстроить процесс, который:
масштабируется на сотни и тысячи COBOL модулей;
даёт воспроизводимый результат;
позволяет проверять корректность миграции;
не привязан жёстко к конкретной версии Java или COBOL-диалекту.
Вот здесь и появляется парсер ANTLR (ANother Tool for Language Recognition), который оказывается удобной точкой сборки для такой архитектуры, а также отвечает всем указанным требованиям.
ANTLR является генератором лексеров и парсеров, который позволяет формально описать транслируемый язык и получать его структурное представление.
В упрощённом виде пайплайн миграции выглядит так:
Каждый этап решает свою задачу и изолирован от остальных. Это принципиально важно: ошибки и изменения на одном уровне не должны «протекать» дальше по пайплайну.
До того, как ANTLR вообще сможет что-либо разобрать, COBOL-код нужно привести в форму, пригодную для парсинга. На практике это означает:
обработку COPY и REPLACING;
разворачивание copybook’ов в единый текст;
работу с fixed-format (колонки 1-6, 7, 8-72);
нормализацию кодировки и строк.
Этот этап часто недооценивают, но именно здесь отсеивается значительная часть проблем.
Важно понимать: ANTLR парсит уже «собранный» COBOL, а не хаотичный набор файлов.
В основе всего лежит грамматика. ANTLR-грамматика описывает:
структуру DIVISION / SECTION / PARAGRAPH;
синтаксис управляющих конструкций (IF, EVALUATE, PERFORM);
объявления данных в DATA DIVISION;
специфические конструкции диалектов (CICS, EXEC SQL и т. д.).
Ключевой момент:
грамматика — это не эвристика и не «примерное понимание языка», а формальное описание, по которому ANTLR строит парсер.
Это принципиальное отличие от AI-подхода, где понимание языка вероятностное.
На основе грамматики ANTLR генерирует:
лексер, который превращает текст в токены;
парсер, который строит parse tree.
На этом этапе:
выявляются синтаксические ошибки;
код получает чёткую структуру;
исчезает неоднозначность интерпретации.
Важно: parse tree — это ещё не то, с чем удобно работать. Это лишь отражение грамматики, а не смысла программы.
Следующий шаг — преобразование parse tree в AST (Abstract Syntax Tree).
AST — это уже инж енерная модель программы, а не отражение грамматики. В AST:
объединяются логически связанные конструкции;
нормализуется управление потоком;
убираются синтаксические детали, не важные для семантики.
Например:
PERFORM PARAGRAPH превращается в узел вызова;
IF/ELSE — в единый условный блок;
группы данных — в иерархию структур.
Именно AST становится центральной точкой всей миграции.
После того как структура программы понятна, начинается самый важный этап — семантика.
Здесь решаются вопросы:
где объявлена переменная и в какой области видимости;
какие copybook’и реально используются;
какие данные модифицируются и где;
как устроен реальный control flow.
ANTLR сам по себе не делает семантический анализ, но он даёт идеальную основу для его реализации. В отличие от AI, здесь всё:
детерминировано;
трассируемо;
проверяемо.
Только после этого имеет смысл переходить к генерации Java.
Преимущество ANTLR-подхода в том, что генерация:
не зависит от исходного текста;
опирается на структурную модель;
легко адаптируется под нужную архитектуру.
Типичные соответствия выглядят так:
COBOL record → Java class / record;
WORKING-STORAGE → state object;
PERFORM → метод или лямбда;
FILE-CONTROL → DAO / repository;
EXEC SQL → слой доступа к БД.
Важно, что правила генерации можно менять, не переписывая парсер и анализатор.
Одно из главных преимуществ ANTLR-подхода — возможность проверки результата.
Поскольку:
парсинг детерминирован;
AST строится одинаково каждый раз;
генерация следует формальным правилам,
мы можем:
повторять миграцию сколько угодно раз;
находить полученную разницу в результатах;
автоматизировать тестирование;
уверенно утверждать, что поведение системы сохранено.
Это ровно то, чего не хватает AI-based подходам.
Далее покажу базовый пример реализации такого пайплайна миграции
IDENTIFICATION DIVISION.
PROGRAM-ID. HELLO.
PROCEDURE DIVISION.
DISPLAY "HELLO, WORLD".
STOP RUN.
<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>
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 : '"' (~["rn])* '"' ;
DOT : '.' ;
WS : [ trn]+ -> skip ;
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";
}
}
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‑систем — значит, цель достигнута.
А если перед вами сейчас стоит похожая задача — не бойтесь сложности. С правильной архитектурой и инструментами она вполне решаема.
Автор: savenkom
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/440253
Ссылки в тексте:
[1] Источник: https://habr.com/ru/companies/alfastrah/articles/980846/?utm_source=habrahabr&utm_medium=rss&utm_campaign=980846
Нажмите здесь для печати.