serez-cobol
A COBOL → Serez (.sz) source-to-source translator, written entirely in pure Serez Code, with a reverse pass that takes a documented subset of Serez back to COBOL. It turns legacy COBOL into Serez scripts that run on sz — and turns Serez, whether the forward pass produced it or you wrote it yourself, back into COBOL.
//@ annotations and the clean / sync options. This page is the reference.Install
sz install serez-cobolserez-cobol runs entirely in user-space as a command-line utility. It requires the File permission to read and write source files, and Env for system interactions.
Exact Fixed-Point Arithmetic
The core reason for serez-cobol's accuracy is Serez-Code's native dec type. COBOL arithmetic expects strict decimal fixed-point rounding (e.g., PIC 9V99 COMPUTE ... ROUNDED). Standard binary floats (f64) introduce rounding drift, which is unacceptable for financial calculations. Serez's exact decimal type mirrors this behavior faithfully:
For example, the COBOL statement:
COMPUTE WS-TAX = WS-SUBTOTAL * 0.21 ROUNDED.Is translated directly into Serez Code as:
WS_TAX = ((WS_SUBTOTAL * 0.21m) + 0m).setScale(2, "half-up");The arithmetic itself is exact: dec is base-10 with 28–29 digits, setScaleapplies COBOL's half-up rounding, and the language refuses to mix dec with binary floats, so drift cannot creep in silently.
COMP-3, EBCDIC or REDEFINES (see the unsupported list below).Use it to read, understand and prototype legacy COBOL from a modern language. For migrating a core banking or government system, use a toolchain with decades of production history.
Usage
Once installed, the package exposes a single convert command (via sz run) that routes the direction automatically from the file extension — no sub-commands to remember:
sz run convert program.cob // Translate COBOL (.cob/.cbl) → Serez (.sz)
sz run convert program.sz // Reverse translate Serez (.sz) → COBOL (.cob)
sz run convert notes.txt // Invalid extension raises an errorsz run convertforwards the file to the package's entry point (index.sz), which locates its engines next to itself. Running that entry directly also works — handy when developing inside the repo:
sz index.sz program.cob // same routing, run from the repoYou can also invoke the individual translation engines directly:
// 1. Translate COBOL to Serez
sz cobol.sz examples/invoice.cob // Emits examples/invoice.sz
// 2. Run the translated program
sz examples/invoice.sz
// 3. Translate Serez back to COBOL
sz serez.sz program.sz // Emits program.cobOptions
Options are bare tokens with no leading dashes — the sz runtime rejects unknown --flags before the package runs:
| Option | Applies to | Effect |
|---|---|---|
clean | .cob input | Omit the //@ annotations from the generated .sz. The file still converts back, but through inference. |
sync | .sz input | Write the reconciled annotation block back into the source .sz. Without it the source is never modified and drift is only reported. |
sz run convert legacy.cob clean
sz run convert app.sz syncReverse pass: annotations and inference
Serez cannot express everything COBOL declares — level numbers, exact picture clauses, OCCURS, level-88 and FD/SELECT have no counterpart. The forward pass therefore embeds those declarations as //@ comment annotations: a lightweight source map, ignored when the program runs.
They are not required. The reverse engine takes one of two routes depending on the file it is given:
does the .sz have //@ annotations?
├── yes → reconciled against the code, then used
└── no → the DATA DIVISION is inferred from the code aloneA file with some annotations gets both: what is annotated wins, what is missing is inferred. Because annotations are comments, they can drift out of step with the code, so every conversion cross-checks them against the actual let declarations — keeping what still matches, adding inferred entries for variables that were never annotated, dropping the orphans, and reporting all of it. Only sync writes the result back.
Round-trip preserves behavior, not text: ADD 1 TO X returns as COMPUTE X = X + 1, and EVALUATE as nested IF. What inference cannot recover: exact widths (a PIC X(20) VALUE "world" comes back as PIC X(5)), level hierarchies, OCCURS, level-88, edited masks and FD/SELECT.
Serez → COBOL: the supported subset
A hand-written .sz needs no annotations. Top-level statements become the main PROCEDURE DIVISION, and each fn name(params) becomes a COBOL subprogram reached with CALL … USING.
| Serez | COBOL |
|---|---|
let x = 5; | 01 X PIC S9(9) VALUE 5 |
x = a + b; | COMPUTE X = A + B |
x = y; | MOVE Y TO X |
x = "a" + y; | STRING … DELIMITED BY SIZE INTO X |
out expr; | DISPLAY expr |
if / else if / else | IF / ELSE / END-IF |
while (cond) | PERFORM UNTIL NOT (cond) |
for (let i = 0; i < n; i = i + 1) | PERFORM VARYING … FROM … BY … UNTIL |
fn f(a, b) { return … } | subprogram + CALL … USING |
fn int f(int a, int b) | typed: PIC S9(9) on the LINKAGE items |
fn dec f(dec m) | typed: PIC S9(9)V9… with the scale the arithmetic produces |
fn string f(string s) | typed: PIC X(n), sized to exactly what is returned |
exit(0); | STOP RUN |
let iva = precio * 0.21m; is declared PIC S9(9)V9999, not the PIC S9(9)a non-literal initializer would otherwise suggest — a product adds its operands' scales, a sum takes the widest. Anything narrower and COBOL would print 21 where Serez prints 21.0000.What has no COBOL equivalent
COBOL has no dynamic arrays, dictionaries, objects, closures, exceptions, booleans or dynamic typing. Every construct below is refused with the line number and the reason, and no .cob is written at all — the translator never emits a half-program, and never emits COBOL that would not compile:
$ sz run convert app.sz
ERROR: cannot translate app.sz to COBOL — nothing was written.
line 9: dictionaries have no COBOL equivalent
Only a subset of Serez maps onto COBOL; see the README for what translates.| Serez | Why COBOL cannot take it |
|---|---|
[1, 2, 3] · a.push(x) | A COBOL table is a fixed OCCURS, declared up front — there is no growing array. |
({ k: v }) | No dictionary or map type exists in COBOL. |
x => x * 2 | No closures and no first-class functions. |
class · interface · enum | No user-defined types; a group item is data, not behavior. |
try · throw · catch | No exceptions. |
import | No module system. A CALL reaches a subprogram in the same file. |
for (x in coll) | Iteration over a collection is a counted PERFORM VARYING. |
t[i] = x | Needs a table declared with OCCURS, which inference cannot invent. |
obj.campo = x | No object to hold the field. |
let ok = true; · let ok = a > b; | No boolean data item — use 0/1, or a level-88 condition name on the COBOL side. |
a % 3 | A remainder is a statement (DIVIDE … REMAINDER), not an operator. |
a += 2 | No compound assignment — write a = a + 2; |
break · continue | No mid-loop exit; the loop condition has to carry it. |
do { … } while (c) | PERFORM UNTIL tests before the body, never after. |
match · switch | EVALUATE is only produced going the other way; use if / else if. |
s.toUpperCase() and any stdlib call | Only functions defined in the same file become a COBOL CALL. |
out "n: " + F(x); | A CALL is a statement, not an operand — assign the result first. |
.sz line by line, so a body squeezed onto its header line — if (c) { out 1; } — is refused with a block written on one line is not translated. The same if spread over three lines translates fine.COBOL → Serez: supported features
| Category | Details |
|---|---|
| Divisions & Format | Supports IDENTIFICATION, ENVIRONMENT, DATA, and PROCEDURE. Autodetects fixed-format (cols 1-72) and free-format layout. Resolves COPY copybooks. |
| Data Types | Translates PIC 9(n) to int, PIC 9(n)V9(m) / S9 to dec, and PIC X(n) / A(n) to string. Supports VALUE clauses, level 77 variables, and level 88 condition names. |
| Group Items & Tables | Supports group items (nested structural dicts) and OCCURS tables (arrays) accessed via subscripts like T(i). Supports PIC editing (Z, commas, decimals, signs, asterisks). |
| Control Flow | Translates IF/ELSE/END-IF, EVALUATE (including multi-subject ALSO tables), and PERFORM forms (UNTIL, TIMES, VARYING). Supports GO TO using a program-counter loop driving paragraph functions. |
| Arithmetic Stmts | Supports COMPUTE [ROUNDED], ADD, SUBTRACT, MULTIPLY, DIVIDE (with TO, FROM, BY, INTO, GIVING, and REMAINDER) supporting multiple receivers. |
| Subprograms | Compiles nested units and END PROGRAM blocks. Translates CALL ... USING by reference, returning modified variables via tuples. |
| Strings & Files | Reference modification X(p:len), STRING, UNSTRING, and INSPECT (TALLYING / REPLACING). File I/O (LINE SEQUENTIAL) with SELECT, FD, OPEN, READ, WRITE, and CLOSE. |
Example Output
Here is a brief demonstration of how a simple paragraph loop translates.
Original COBOL source:
IDENTIFICATION DIVISION.
PROGRAM-ID. LOOPDEMO.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 I PIC 9(2) VALUE 0.
PROCEDURE DIVISION.
MAIN-PARA.
PERFORM LOOP-PARA VARYING I FROM 1 BY 1 UNTIL I > 3.
STOP RUN.
LOOP-PARA.
DISPLAY "Iteration: " I.Generated Serez Code:
// Generated by serez-cobol from COBOL source.
//@ IDENTIFICATION DIVISION.
//@ PROGRAM-ID. LOOPDEMO.
//@ DATA DIVISION.
//@ WORKING-STORAGE SECTION.
//@ 01 I PIC 9(2) VALUE 0.
let I = 0;
let __goto = "";
fn MAIN_PARA_() {
I = 1;
while (!(I > 3)) {
LOOP_PARA_();
I = (I + (1));
}
exit(0);
}
fn LOOP_PARA_() {
out "Iteration: " + I;
}
// ── main ──
let __order = ["MAIN_PARA", "LOOP_PARA"];
let __fns = [MAIN_PARA_, LOOP_PARA_];
let __pc = 0;
while (__pc < __fns.length()) {
__goto = "";
__fns[__pc]();
if (__goto == "") { __pc = __pc + 1; }
else { /* GO TO: jump to the named paragraph */ }
}Paragraphs become fn NAME_() and run under a program-counter driver — that is what lets GO TO, ALTER and fall-through behave exactly as they do in COBOL.
Not Yet Supported (Roadmap)
- Inter-file calls (CALL to subprograms defined in separate source files).
- REDEFINES clause byte-overlay logic.
- COMP / COMP-3 packed binary numbers and EBCDIC character encodings.
- SORT / MERGE verbs, Report Writer, and Screen Section layouts.