Translate COBOL & back
A step-by-step tutorial for serez-cobol. The forward direction turns a legacy COBOL program into Serez that runs on sz. The reverse direction turns Serez back into COBOL — and it works two quite different ways depending on where the .sz came from. That asymmetry is the one thing worth understanding before anything else.
//@ annotations are for, the two routes back to COBOL, how a hand-written .sz becomes a COBOL program, what the translator refuses outright, and when to reach for clean or sync.Step 1 — Install
sz install serez-cobolOne command does both directions; it picks the direction from the file extension:
sz run convert program.cob # COBOL → Serez: writes program.sz
sz run convert program.sz # Serez → COBOL: writes program.cobsync, not --sync. The sz runtime rejects unknown --flags before the package ever runs.Step 2 — COBOL → Serez
This is the direction the translator is built around. Start from real COBOL:
IDENTIFICATION DIVISION.
PROGRAM-ID. HELLO.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-NAME PIC X(20) VALUE "world".
01 WS-COUNT PIC 9(3) VALUE 0.
PROCEDURE DIVISION.
MAIN.
DISPLAY "Hello, " WS-NAME.
ADD 1 TO WS-COUNT.
ADD 4 TO WS-COUNT.
DISPLAY "Count: " WS-COUNT.
IF WS-COUNT = 5 THEN
DISPLAY "Count is five"
END-IF.
STOP RUN.sz run convert hello.cob # writes hello.sz
sz hello.sz # Hello, world / Count: 5 / Count is fiveStep 3 — The //@ lines in the result
Look at what came out. Paragraphs became functions, and the file opens with a block of //@ comments:
// Generated by serez-cobol from COBOL source.
//@ IDENTIFICATION DIVISION.
//@ PROGRAM-ID. HELLO.
//@ DATA DIVISION.
//@ WORKING-STORAGE SECTION.
//@ 01 WS-NAME PIC X(20) VALUE "world".
//@ 01 WS-COUNT PIC 9(3) VALUE 0.
let WS_NAME = "world";
let WS_COUNT = 0;
fn MAIN_() {
out "Hello, " + WS_NAME;
WS_COUNT = (WS_COUNT + (1));
...They are ordinary comments — sz ignores them when the program runs. They exist because Serez cannot express everything COBOL declares. Look at WS-NAME: the COBOL says PIC X(20), the Serez says let WS_NAME = "world". The width 20 is nowhere in the Serez code. Same story for level hierarchies, OCCURS, level-88, edited masks and FD/SELECT.
.sz cannot. Everything the code does state — names, values, the whole procedure — is never taken from them.Step 4 — Two routes back to COBOL
This is the part that trips people up. Going back is not one mechanism, it is two, and the translator picks by looking at your file:
does the .sz have //@ annotations?
├── yes → they are reconciled against the code and used
│ (the original PICs, sections and FD clauses come back intact)
└── no → the DATA DIVISION is inferred from the code alone
(widths come from the literals actually assigned)You never choose a mode. A .sz that the forward pass produced takes the first route, a file you typed yourself takes the second, and a file withsome annotations gets both: what is annotated wins, what is missing is inferred.
Step 5 — Serez you wrote by hand → COBOL
No annotations needed. Top-level statements become the main PROCEDURE DIVISION, and every fn becomes a COBOL subprogram:
fn int Sumar(a, b) {
return a + b;
}
let resultado = Sumar(10, 20);
out resultado;sz run convert sumar.szIDENTIFICATION DIVISION.
PROGRAM-ID. SUMAR-MAIN.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 RESULTADO PIC S9(9) VALUE 0.
01 SUMAR-A PIC S9(9) VALUE 0.
01 SUMAR-B PIC S9(9) VALUE 0.
01 SUMAR-RESULT PIC S9(9) VALUE 0.
PROCEDURE DIVISION.
MOVE 10 TO SUMAR-A
MOVE 20 TO SUMAR-B
CALL "SUMAR" USING SUMAR-A SUMAR-B SUMAR-RESULT
MOVE SUMAR-RESULT TO RESULTADO.
DISPLAY RESULTADO.
STOP RUN.
END PROGRAM SUMAR-MAIN.
IDENTIFICATION DIVISION.
PROGRAM-ID. SUMAR.
DATA DIVISION.
LINKAGE SECTION.
01 LK-A PIC S9(9).
01 LK-B PIC S9(9).
01 LK-RESULT PIC S9(9).
PROCEDURE DIVISION USING LK-A LK-B LK-RESULT.
COMPUTE LK-RESULT = LK-A + LK-B
GOBACK.
END PROGRAM SUMAR.COBOL has no expression-level function call, so a fn becomes a separate compilation unit and the call becomes three statements: stage the arguments, CALL, take the result. The SUMAR-* items are that staging area — the translator adds them, they are not your variables.
Step 6 — Type your parameters
Parameters and return values can be typed, and it is worth doing: the declared type sets the PIC directly instead of being guessed from whatever the call sites happen to pass.
fn int Suman(int a, int b) { return a + b; }
fn string Saludo(string quien) { return "hola " + quien; }
fn dec ConIva(dec monto) { return monto * 1.21m; }| Declared | PIC |
|---|---|
int | PIC S9(9) |
string | PIC X(n) — sized to exactly what is returned, so DISPLAY does not pad with blanks the .sz never prints |
dec | PIC S9(9)V9… — with the scale the arithmetic actually produces |
monto * 1.21m with a V99 argument produces four decimals, so the result is declared PIC S9(9)V9999. Pinning it at V99 would make COBOL silently truncate what Serez computed exactly — and exact decimal arithmetic is the whole reason this translator exists.Plain variables get the same treatment, so you do not have to wrap arithmetic in a function to keep your decimals: let iva = precio * 0.21m; is declared PIC S9(9)V9999 even though its initializer is an expression rather than a literal.
Step 7 — What translates, and what is refused
Inside a hand-written file, this is the subset that maps onto COBOL:
| Serez | COBOL |
|---|---|
let x = 5; | 01 X PIC S9(9) VALUE 5 (in WORKING-STORAGE) |
x = a + b; | COMPUTE X = A + B |
x = y; | MOVE Y TO X |
x = "a" + y; | STRING "a" … 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 I FROM 0 BY 1 UNTIL NOT (I < N) |
fn f(a, b) { return … } | a subprogram reached with CALL … USING |
exit(0); | STOP RUN |
Everything else is refused with the line number and the reason, and no .cob is written at all. COBOL has no dynamic arrays, dictionaries, objects, closures, exceptions, booleans or dynamic typing, so there is nothing honest to emit — and a half-translated program that does not compile, or quietly prints a different number, would be worse than a refusal. Reach for a dictionary and you get this:
let cliente = ({ nombre: "Acme", saldo: 1500.75m });
out cliente.nombre;$ sz run convert app.sz
ERROR: cannot translate app.sz to COBOL — nothing was written.
line 1: dictionaries have no COBOL equivalent
Only a subset of Serez maps onto COBOL; see the README for what translates.The whole list, grouped by what you were probably reaching for:
| If you write | The reason it cannot go |
|---|---|
| Data: [1,2,3], a.push(x), ({k: v}), obj.campo = x, t[i] = x | A COBOL table is a fixed OCCURS declared up front, and there is no map and no object. |
| Types: class, interface, enum, try / throw / catch | No user-defined types and no exceptions. |
| Functions: x => x * 2, import, s.toUpperCase() | No closures, no module system, and only functions defined in the same file become a CALL. |
| Conditions as values: let ok = true;, let ok = a > b; | COBOL has no boolean data item — use 0/1, or a level-88 condition name. |
| Control flow: for (x in coll), break, continue, do/while, match | Iteration is a counted PERFORM VARYING, and PERFORM UNTIL tests before the body. |
| Operators: a % 3, a += 2 | A remainder is a statement (DIVIDE … REMAINDER), and there is no compound assignment. |
| Nested calls: out "hi " + Saludo(x); | A CALL is a statement, not an operand — assign the result to a variable first. |
.sz line by line, so if (c) { out 1; } written on a single line is refused — spread the same if over three lines and it translates.Nothing on that list is exotic: it is the Serez you would write without thinking. Translating to COBOL is a narrower target, and the point of the refusal is that you find out at convert time, on the exact line, instead of on a mainframe. The reference page lists every case with the message the translator prints.
Step 8 — clean and sync
Use clean on the way out when you are migrating off COBOL and never coming back. You get Serez with no annotation block:
sz run convert legacy.cob cleanThe trade is real: that file can still go back to COBOL, but through inference, so the exact widths and any FD/SELECT are gone for good.
sync is the other side. Annotations are comments, so nothing stops them from drifting away from the code — rename a variable and the annotation still names the old one. Every conversion cross-checks them and tells you:
annotation drift in app.sz:
dropped: 'WS-NAME' is annotated but no longer declared in the code
added: 'WS-NOMBRE' is declared in the code but was not annotated (PIC inferred)
note: a dropped + added pair is what a rename looks like from here;
if it was one, the original PIC could not be carried over.
(pass `sync` to reconcile them in the source .sz)Without sync your .sz is never touched — the drift is only reported. With it, the reconciled block is written back into the file:
sz run convert app.sz syncIt is also the way to start annotating a hand-written file: run it once with sync and the inferred block lands in your source, where you can tighten a PIC S9(9) down to PIC S9(4) by hand. From then on your value wins over the inferred one, field by field.
What inference cannot recover
Worth knowing before you reach for clean. Widths come from the literals actually assigned, so a COBOL PIC X(20) VALUE "world" that became let WS_NAME = "world"; infers back as PIC X(5). Level hierarchies (05 / 10 inside an 01), OCCURS, level-88, numeric-edited masks and FD/SELECT clauses are not in the .sz at all. For those, keep the annotations.
ADD 1 TO X comes back as COMPUTE X = X + 1, and EVALUATE comes back as nested IF.Where to go next
- serez-cobol reference — the full list of supported COBOL statements and the type mapping.
- Language reference — the
dectype, which is what makes COBOL fixed-point arithmetic translate without rounding drift.