Version: 1.0 · Engine branch:
feature/autonomous-legacy-code
Supported targets: Java 17+, Spring Boot 3.x, Jakarta Servlet 6.x
Table of Contents
- Introduction & System Overview
- Supported Language Matrix
- Key Concepts & The Emulation Layer
- Configuration & Customization Guide
- Troubleshooting & Edge Cases
1. Introduction & System Overview
What the Engine Does
The Migration Engine is a source-to-source translation system that converts legacy mainframe and web-tier programs — written in COBOL, Fortran, Classic ASP, PHP 4/5, and related languages — into modern, production-ready Java code.
It does more than a text substitution. For every input program it:
- Detects the execution context (batch/CLI vs. web template) before generating a single line of Java.
- Preserves behavioral equivalence — the translated Java must produce identical outputs for identical inputs.
- Upgrades the architectural tier — a COBOL sequential file becomes a buffered byte stream; a Classic ASP page becomes a Spring Boot
@Controller; a Fortran GOTO loop becomes an idiomaticwhileordo-whileconstruct. - Emits a runtime emulation layer (Section A) containing helper classes that faithfully reproduce legacy type semantics (fixed-width strings, 1-indexed tables, packed-decimal arithmetic).
Core Philosophy
| Principle | What it means in practice |
|---|---|
| Behavioral equivalence | The Java output produces the same results as the legacy source for all valid inputs, including edge cases like COBOL out-of-range ODO indices or Fortran computed GOTO with out-of-range selectors. |
| Architectural tier upgrade | Legacy patterns are mapped to their modern structural equivalent — not just their syntactic one. A web page becomes a controller, not a main() method. |
| Precision over convenience | Financial fields (COMP-3, PIC 9(n)V9(m)) are always emitted as BigDecimal, never double or float. |
| Fail loud | Out-of-bounds table access and binary buffer overflows throw detailed exceptions rather than silently returning garbage data. |
| Zero stubs | The engine never emits // TODO, placeholder methods, or unimplemented blocks. Every line of the source is translated. |
2. Supported Language Matrix
2.1 COBOL
| Source Construct | Java Output | Notes |
|---|---|---|
PIC X(n) | CobolString(n) | Fixed-width, space-padded, truncating |
PIC 99 / PIC 9(n) | CobolInteger(initial, digits) | Max-value enforced (e.g. PIC 99 → max 99) |
PIC 9(n)V9(m) | CobolDecimal(initial, scale) | BigDecimal-backed; HALF_EVEN rounding |
COMP-3 / packed decimal | CobolDecimal + CobolComp3Reader | Byte-stream unpacking; never text splitting |
OCCURS n TO m DEPENDING ON v | CobolTable<T>(min, max, dependingOn) | ODO variable reference; 1-based access |
PERFORM VARYING v FROM s BY i UNTIL c | for (v.set(s); !c; v.increment(i)) | Loop variable mutated directly; post-loop value preserved |
MOVE x TO y | y.set(x.get()) or direct assignment | Type-appropriate |
DISPLAY msg | System.out.println(msg) | |
STOP RUN | return; | |
Sequential file (FD — display records) | BufferedReader + line.substring() | Text-safe records only |
Sequential file (FD — COMP-3/binary) | BufferedInputStream + CobolComp3Reader | Byte-stream; never BufferedReader |
Code comparison — COBOL packed-decimal file vs. Java byte stream:
FD TRANS-FILE.
01 TRANS-RECORD.
05 TR-AMOUNT PIC 9(7)V99 COMP-3.
05 TR-NAME PIC X(20).// Java target (binary record — Rule 9)
final int RECORD_BYTES = 5 + 20; // 4 bytes COMP-3 + 20 bytes char
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("TRANS-FILE.dat"))) {
byte[] buf = new byte[RECORD_BYTES];
while (CobolComp3Reader.readRecord(bis, buf)) {
BigDecimal amount = CobolComp3Reader.unpack(buf, 0, 4);
String name = new String(buf, 4, 20, StandardCharsets.ISO_8859_1).trim();
processTransaction(amount, name);
}
}2.2 Fortran
| Source Construct | Java Output | Notes |
|---|---|---|
GOTO (L1, L2, L3), expr (computed) | switch (expr) with no default | Out-of-range silently skipped — Fortran semantics |
IF (expr) neg, zero, pos (arithmetic) | if (e<0) … else if (e==0) … else … | Three-way branch |
Label loop: 10 CONTINUE / body / IF (c) GOTO 10 | do { body; } while (c) | Do-while, not while — body runs before check |
DO var = start, end [, step] | Standard for loop | Post-loop var value documented in Section C |
IMPLICIT NONE | All variables explicitly declared | Compile-time directive, no runtime effect |
PRINT *, expr | System.out.println(expr) | Formatting gap noted (Fortran adds leading spaces) |
CONTINUE | (discarded) | No-op label target; not Java continue |
INTEGER :: v | int v | 32-bit signed, direct mapping |
REAL :: v | float v (flagged in Section C if financial) | Precision risk noted |
DOUBLE PRECISION :: v | double v |
Code comparison — Fortran computed GOTO vs. Java switch:
GOTO (20, 30, 40), I
20 PRINT *, 'I = 1'
GOTO 100
30 PRINT *, 'I = 2'
GOTO 100
40 PRINT *, 'I = 3'
100 PRINT *, 'Done'// i == 11 here (post-loop) — out of range → no case matches → silent skip
switch (i) {
case 1: System.out.println("I = 1"); break;
case 2: System.out.println("I = 2"); break;
case 3: System.out.println("I = 3"); break;
// no default: intentional — matches Fortran out-of-range silent skip
}
System.out.println("Done");Important: The engine traces variable state line-by-line. In the example above,
Iequals 11 after the accumulation loop exits. The switch correctly receives 11 (not 1, 2, or 3), so no case fires and "Done" is printed directly — matching Fortran's exact runtime output.
2.3 Classic ASP / VBScript
| Source Construct | Java / Spring Boot Output |
|---|---|
ASP page (<% … %>) | @Controller method (Spring MVC) or HttpServlet |
Response.Write("text") | out.println("text") on HttpServletResponse writer |
Response.Redirect(url) | return "redirect:" + url |
Request.Form("field") | @RequestParam("field") String field |
Request.QueryString("key") | @RequestParam("key") String key |
Session("key") = value | session.setAttribute("key", value) |
Session("key") | session.getAttribute("key") |
Server.MapPath("/path") | servletContext.getRealPath("/path") |
Server.CreateObject("ADODB.Connection") | javax.sql.DataSource (injected) |
On Error Resume Next | try { … } catch (Exception e) { log.warn(…); } |
| HTML literals in output | Preserved as-is in out.println() calls |
Code comparison — ASP session handling vs. Spring Boot:
<%
Session("UserName") = Request.Form("username")
Response.Write("<h1>Welcome, " & Session("UserName") & "</h1>")
%>@Controller
public class WelcomeController {
@PostMapping("/welcome")
public void welcome(
@RequestParam("username") String username,
HttpSession session,
HttpServletResponse response) throws IOException {
session.setAttribute("UserName", username);
PrintWriter out = response.getWriter();
out.println("<h1>Welcome, " + session.getAttribute("UserName") + "</h1>");
}
}2.4 PHP 4/5
| Source Construct | Java / Spring Boot Output |
|---|---|
$_POST["field"] | @RequestParam("field") |
$_GET["key"] | @RequestParam("key") |
$_SESSION["key"] | session.getAttribute("key") |
header("Location: $url") | return "redirect:" + url |
mysql_connect(h,u,p) | DataSource (JDBC) — parameterized only |
mysql_query($sql) | jdbcTemplate.query() or PreparedStatement |
echo "text<br>" (web) | out.println("text<br>") |
echo "text" (CLI) | System.out.println("text") |
die($msg) / exit($msg) | Unchecked exception or HTTP error response |
CLI script ($argv, no $_POST) | public static void main(String[] args) |
SQL Injection Prevention: Every
mysql_*API call is translated to a parameterizedPreparedStatementorJdbcTemplatecall. Raw string-concatenated SQL is never emitted.
3. Key Concepts & The Emulation Layer
Why an Emulation Layer?
Legacy languages enforce semantics that Java does not — fixed-width character fields, 1-based array indexing, packed-decimal arithmetic, and ODO-controlled dynamic table sizes. Mapping these naively to Java primitives introduces subtle bugs:
| Legacy assumption | Naive Java mapping | What goes wrong |
|---|---|---|
PIC X(10) is always exactly 10 chars | String | Trailing spaces stripped; comparison failures |
PIC 9(7)V99 is exact | double | Rounding errors accumulate over financial calculations |
| Arrays are 1-indexed | int[] (0-based) | Off-by-one on every access |
| ODO table size tracks a variable | ArrayList with manual size | Size drift when the controlling variable changes |
The emulation layer solves each of these with a minimal set of wrapper classes.
CobolString
CobolString name = new CobolString(10); // PIC X(10)
name.set("ALICE"); // stored as "ALICE " (5 chars + 5 spaces)
name.set("VERY LONG NAME"); // stored as "VERY LONG " (truncated at 10)
name.get(); // returns "ALICE " — preserves trailing spaces
name.trimmed(); // returns "ALICE" — for display/comparison- Internally a
char[]of the declared width. set()fills from left, pads right with spaces, truncates if longer.toString()returns the padded form (matching COBOL field output).
CobolInteger
CobolInteger count = new CobolInteger(3, 2); // PIC 99 VALUE 3
count.set(150); // stored as 99 (max = 10^2 - 1 = 99)
count.increment(1); // now 100 → stored as 99 (capped)- Enforces the maximum value derived from the digit count.
toString()zero-pads to the declared width ("03"for PIC 99 with value 3).
CobolDecimal
CobolDecimal balance = new CobolDecimal("0", 2); // PIC 9(7)V99
balance.add(new BigDecimal("1000.505")); // stored as 1000.51 (HALF_EVEN)
balance.multiply(new BigDecimal("1.05")); // exact BigDecimal arithmetic- All arithmetic uses
BigDecimalwithMathContext.DECIMAL128. - Rounding mode
HALF_EVEN(banker's rounding) matches COBOL's default. - Scale (decimal places) enforced on every
set()call. - Never uses
doubleorfloatinternally.
CobolTable
CobolInteger wsCount = new CobolInteger(3, 2);
CobolTable<Entry> table = new CobolTable<>(1, 10, wsCount, Entry::new);
table.get(1).name.set("ALICE"); // 1-based — maps to data[0]
table.get(3).name.set("CHARLIE");
wsCount.set(5); // table.logicalSize() now returns 5 automaticallyget(n)uses 1-based indexing; throwsArrayIndexOutOfBoundsExceptionif out of bounds.- Pre-allocated to
maxCapacity; accessing any slot within capacity returns a default entry rather than throwing. - The
dependingOnreference keeps logical size synchronized with the COBOL ODO variable. - Bounds exception message includes: the 1-based index, the capacity range, and the current ODO variable value — making root-cause diagnosis fast.
CobolComp3Reader
// Reading a 5-byte COMP-3 field at offset 0 in a 25-byte record buffer
BigDecimal amount = CobolComp3Reader.unpack(recordBuf, 0, 5);
// 5 bytes → 9 decimal digits + sign nibble
// e.g. 0x01 0x23 0x45 0x67 0x8C → 12345678 (positive)- Each byte holds two BCD nibbles; the last nibble is the sign (
0x0C/0x0F= positive,0x0D= negative). readRecord(bis, buf)handles partial I/O reads and clean EOF detection.- Before unpacking, validates
offset + length <= buf.length; throwsBufferOverflowExceptionwith field coordinates if violated.
4. Configuration & Customization Guide
4.1 Execution Context: Web vs. CLI
The engine performs a Context Assessment before generating any code. You can also force the target architecture explicitly.
Auto-detection (default)
The engine scans the source for web markers:
| Marker found | Detected context | Java output |
|---|---|---|
Response.Write, Request.Form, $_POST | Web template | Spring Boot @Controller |
<html>, <form>, <body> | Web template | Spring Boot @Controller |
session_start(), Session("key") | Web template | Spring Boot @Controller |
$argv, System.out, console-only output | CLI / Batch | public static void main |
| No web markers | CLI / Batch | public static void main |
Forcing a target architecture
Add a business context hint in the translator's context field:
Context: "Target a standalone Spring Boot REST API — @RestController with
JSON responses, not HTML output"Context: "This is a nightly batch job — generate a CLI application with
main(String[] args) even if HTTP headers are present in the source"4.2 Selecting the Target Framework
| Context hint | What the engine generates |
|---|---|
| (default for web) | Spring Boot @Controller with Model / Thymeleaf view |
"REST API" / "JSON response" | @RestController with ResponseEntity<> returns |
"Servlet" / "no Spring" | Raw HttpServlet with doGet/doPost |
"CLI" / "batch" | public static void main(String[] args) |
"Spring Boot microservice" | Full @SpringBootApplication with auto-configuration |
4.3 File I/O Mode
The engine auto-detects binary records from FD definitions. You can override:
Context: "All files are fixed-length text records (EBCDIC display) —
use BufferedReader for all file I/O"Context: "File TRANS-FILE is packed binary (COMP-3 throughout) —
use BufferedInputStream and CobolComp3Reader"4.4 BigDecimal Scale Override
By default, the scale is derived from the COBOL V clause (e.g., PIC 9(7)V99 → scale 2). To override:
Context: "All monetary fields require 4 decimal places for internal
precision — use scale 4 on all CobolDecimal instances"4.5 Freemium Tier Limits
| Tier | Files per migration | Max code size | Language pairs |
|---|---|---|---|
| Anonymous | 1 | 50 KB | JavaScript, Python 2, PHP 4 → TypeScript / Python |
| Free | 5/day | 100 KB | All 14 source languages |
| Pro | Unlimited | 5 MB | All pairs + autonomous multi-file |
| Enterprise | Unlimited | 25 MB | All pairs + GitHub PR integration |
5. Troubleshooting & Edge Cases
5.1 Variable State After Loops
Symptom: The translated Java produces wrong output from a switch or if block that follows a loop.
Root cause: The legacy loop exits with the variable at its first-failing value, not its last-matching value. This is standard COBOL and Fortran behavior. A naive translator may incorrectly reset the variable or use its initial value.
What to check:
- In COBOL
PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 10, after the loopWS-Iequals 11, not 10. - In Fortran
DO I = 1, 10, after the loopIis implementation-defined but typically equals 11. The engine documents this in Section C of the migration notes. - If the translated
switchafter the loop never fires, trace the variable's post-loop value — it may be out of the case range, which is correct (Fortran computed GOTO out-of-range = silent skip).
5.2 Database / JDBC Configuration
Symptom: A translated PHP or ASP program that calls mysql_connect() / ADODB.Connection compiles but throws a NullPointerException or DataSourceException at runtime.
What to check:
- The engine generates a
DataSourceinjection point — you must configure the actual connection inapplication.propertiesorapplication.yml:
spring:
datasource:
url: jdbc:mysql://localhost:3306/your_db
username: your_user
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver- Confirm the JDBC driver is on the classpath. Add to
pom.xml:
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>- Every translated SQL statement uses
PreparedStatementparameters — review Section C migration notes for the exact parameter binding order.
5.3 Binary File I/O Mismatches
Symptom: A COBOL batch program migrated with CobolComp3Reader reads records but produces garbled numeric values.
What to check:
- Record length — the declared
RECORD CONTAINS n CHARACTERSmust matchRECORD_BYTESin the generated Java constant. Packed COMP-3 bytes are not the same as the number of decimal digits:PIC 9(5)V99 COMP-3→(7 digits + 1 sign nibble) / 2= 4 bytes, not 7.- Formula:
ceil((total_digits + 1) / 2)bytes.
- Sign nibble — some legacy platforms use
0x0Cfor positive and0x0Ffor unsigned; others use only0x0C. Check the source system's documentation and adjustCobolComp3Reader.unpack()if needed. - EBCDIC vs. ASCII — character fields in a COMP-3 file may be EBCDIC-encoded. Use
new String(buf, offset, length, Charset.forName("IBM037"))instead ofStandardCharsets.ISO_8859_1.
5.4 Spring Boot Controller Not Mapping Requests
Symptom: An ASP or PHP page translated to a Spring Boot @Controller returns 404 or a blank response.
What to check:
- Confirm the
@RequestMappingpath in the generated controller matches the URL your application server expects. - For
Response.Write(html)→out.println(html): ensure the method setsContent-Type:javaresponse.setContentType("text/html; charset=UTF-8"); PrintWriter out = response.getWriter(); - If the original ASP page used
Server.Transfer(internal redirect), the engine maps it to Spring'sforward:prefix — confirm the target URL exists in the application.
5.5 CobolTable Index Out of Bounds
Symptom: Runtime throws:
ArrayIndexOutOfBoundsException: COBOL 1-based index 11 out of table bounds [1, 10]
(ODO control variable current value: 11). Check loop termination condition or ODO variable mutation.What to check:
- The ODO variable (
wsCountin this example) was incremented pastmaxCapacity(10) before table access. - In COBOL, accessing beyond the ODO logical size is undefined; beyond
maxCapacityis always illegal. - Review whether the PERFORM VARYING loop should have its UNTIL condition adjusted, or whether
maxCapacityinCobolTableneeds to be raised to match the actual table definition.
5.6 Fortran CONTINUE Mistranslated
Symptom: A Fortran loop exits prematurely or a loop body is unexpectedly skipped.
Root cause: A translator that naively maps Fortran CONTINUE to Java continue breaks the control flow. Fortran CONTINUE is a label target no-op; Java continue jumps to the next loop iteration.
What to check:
- Search the generated Java for
continue;statements that appear without anifguard. If one corresponds to a FortranCONTINUElabel, it should be removed entirely. - The engine discards Fortran
CONTINUEand does not emit Javacontinue.
5.7 COBOL 88-Level Conditions / REDEFINES / COPY
These constructs require manual review and are flagged in Section C of every translation:
| Construct | What to do |
|---|---|
88 NAME VALUE 'X' | Translate to a Java boolean helper method or enum manually |
REDEFINES | The overlapping memory layout cannot be auto-translated; implement as a Java union-style class manually |
COPY member | Inline the copybook member definition before submitting to the engine |
EVALUATE WHEN | The engine translates simple cases; complex multi-condition EVALUATE requires manual review |
This guide reflects engine version as of commit cd504144 on branch feature/autonomous-legacy-code.