For mainframe engineers
You are the person who has to accept this output. Everything else in this repository is written for someone curious about compilers; this page is written for you, and it reads the generated COBOL with you, construct by construct.
The short version: BankLang is a source-to-source compiler. It takes a
restricted, statically typed language and emits IBM Enterprise COBOL for z/OS
6.4 with the JCL to build and run it. There is no runtime, no framework and no
interpreter — what ships is a .cbl member, .cpy members, and a .jcl job.
If the compiler disappeared tomorrow, the COBOL would still be yours.
The rest of this page is why it looks the way it does.
Read a whole program first
evidence/account-file-batch/cobol/ACCOUNTF.cbl is a batch program that reads a
sequential master, posts to the ledger and writes an advice file. It is 200-odd
lines and it is the honest sample: not a hello-world, and not the biggest thing
here either.
Read it before reading any of the reasoning below. If something in it looks wrong, the reasoning is what you are checking.
The prologue
Every program opens with one, and it is generated rather than written:
*> ---------------------------------------------------------------
*> ACCOUNTF — AccountFileBatch
*>
*> Generated by bankc from main.bank.ts.
*> Do not edit this file directly. Source maps are in dist/maps.
*>
*> PURPOSE
*> POST-ACCOUNTS, the transaction this program is entered at.
*>
*> ENTRY
*> A batch program, started by EXEC PGM and entered with the
*> job's PARM behind a halfword length: 36 characters,
*> positional.
*> idempotencyKey X(36) (36)
*>
*> FILES
*> ACCOUNTI input sequential ACCOUNT-RECORD (26 bytes)
*> POSTINGO output sequential POSTING-RECORD (27 bytes)
*>
*> CALLS
*> BANKAUDT audit trail
*>
*> RETURN CODES
*> 0 The work completed.
*> 12 A failure the program named. BANK-FAILURE-CODE says
*> which.
*> 16 A sort or merge did not complete. SORT-RETURN says so.
*>
*> RESTART
*> Not restartable. Rerun from the top: the generated job
*> deletes a half-written output dataset rather than
*> cataloguing it.
*> ---------------------------------------------------------------
Derived from the program, not maintained by editing — which is the failure mode of every prologue anybody has ever had to trust. If a DD name changes, this changes with it.
CBL before anything else
CBL ARITH(COMPAT),TRUNC(STD),NUMPROC(NOPFD),NOSSRANGE
CBL RENT,NODYNAM,QUOTE,PGMNAME(COMPAT)
Every one of those is IBM's own default. Stating them is the point: your default
options module can change any of them, and several change what the program
computes rather than how it is compiled. TRUNC(OPT) does not truncate a
binary receiver at all. ARITH(EXTEND) gives 31 digits where every generated
picture is sized against 18.
If your site forbids CBL statements, take them out and put the same options on
the compile step's PARM. What must not happen is the program being compiled
under options nobody wrote down.
Single exit, and one failure path
Every routine is a paragraph and an exit paragraph, and every caller performs it
THRU the exit:
BANK-MAIN.
MOVE 0 TO BANK-RETURN-CODE
MOVE SPACES TO BANK-FAILURE-CODE
PERFORM POST-ACCOUNTS THRU POST-ACCOUNTS-EXIT
MOVE BANK-RETURN-CODE TO RETURN-CODE
GOBACK.
BANK-MAIN is the only paragraph that ends the program. Every failure —
a status the program did not expect, an overflow, a subscript outside its table,
an MQ reason code — does the same three things:
MOVE 12 TO BANK-RETURN-CODE
MOVE "READ-FAILED" TO BANK-FAILURE-CODE
GO TO POST-ACCOUNTS-EXIT
Set the return code, name the failure, leave through the enclosing routine's
exit. It used to be a GOBACK written where the failure was found, which sat
inside a range the caller had performed — so a transaction with an on failure
handler ran it for one kind of failure and skipped it for another.
The two registers are EXTERNAL because a recursive or nested function is a
separate program rather than a paragraph, and a failure raised inside one has to
reach the caller that tests for it. Neither carries a VALUE clause: IBM
honours one on an elementary EXTERNAL item and GnuCOBOL ignores it, so
BANK-MAIN sets both rather than the two targets starting from different
states.
Why COMP-3 for money
decimal<18,2> is PIC S9(16)V99 COMP-3. Packed decimal, because a bank's
arithmetic is decimal and binary floating point cannot represent 0.10. There is
no floating-point type in the language at all, and COMP-1/COMP-2 are the one
thing the copybook importer refuses outright.
binary<n> is COMP for counters and codes, zoned<p,s> is display with a
separate trailing sign for a file a person reads, and unsigned<p,s> is
PIC 9(n) — which is what most dates, counts and codes on an estate actually
are, and is a byte narrower than zoned.
See numeric-model.md for intermediate results and rounding.
Why the bounds guard rather than SSRANGE
COBOL does not check subscripts, and an index past the end of a table inside a record addresses the field after it — so an out-of-range write does not fail, it quietly changes a different field of the same record.
The compiler emits an explicit check where the subscript is not provably in range:
IF POST-ACCOUNTS-P2 < 1 OR POST-ACCOUNTS-P2 > 10
MOVE "23" TO BANK-BOUNDS-STATUS
DISPLAY "SUBSCRIPT OUT OF RANGE " POST-ACCOUNTS-P2 UPON SYSOUT
MOVE 12 TO BANK-RETURN-CODE
MOVE "BANK-BOUNDS-VIOLATION" TO BANK-FAILURE-CODE
GO TO POST-ACCOUNTS-EXIT
END-IF
SSRANGE would do the checking for free, and the CBL statement says
NOSSRANGE deliberately. Three reasons. It abends rather than failing the step
with a code the next step's COND= can read. It checks every subscript
including the ones the compiler already proved. And it is a compile option,
so a program built without it silently loses the checking — the guard is in the
source, where it cannot be switched off by a JCL change.
A literal subscript gets no guard: it was proved in range when it was compiled, and a branch that can never be taken is worse than no branch.
Names
Generated names are the source name in COBOL's spelling: accountId becomes
ACCOUNT-ID, postAccounts becomes POST-ACCOUNTS. Suffixes are added for
storage the source did not name — -RESULT for a function's answer, -P1 for
its first parameter, -EXIT for its exit paragraph, -IDX for a table's index.
Three rules you will notice:
- Thirty characters, always. A name at the limit with a suffix on the end is
a name the target rejects, so long names are abbreviated word by word:
IS-ELIGIBLE-FOR-INTEREST-RESULT(31) becomesIS-ELIG-FOR-INTEREST-RESULT. Deterministic, and two source names that abbreviate to one word areBANK-NAME-001rather than something to paper over. - A reserved word gets
-FLD. A field calledstatusisSTATUS-FLD. The list is the union of IBM's and GnuCOBOL's, because the local validator has to accept it too. - No source coordinates. A loop counter is
POST-ACCOUNTS-LOOP-1, notWS-LOOP-34-3. Adding a blank line to the source used to rename working storage, which made a diff of two generated programs unreadable.
PROGRAM-ID is eight characters with no hyphens, and so is the load module
member, the artifact's file name and the job's EXEC PGM=. Under
PGMNAME(COMPAT) an external program-name is "folded to uppercase ...
truncated to eight characters ... hyphens are translated to zero", so
PROGRAM-ID. ONLINE-ENQUIRY. would define the entry point ONLINE0E while
every other name in the build said something else.
File handling
An FD for a QSAM file carries what you would expect:
FD ACCOUNT-INPUT-FILE
BLOCK CONTAINS 0 RECORDS
RECORDING MODE IS F.
BLOCK CONTAINS 0 RECORDS asks for a system-determined block size, and the
job's BLKSIZE=0 is the other half of it.
The status key is read through condition names:
01 ACCOUNT-INPUT-STATUS PIC X(2).
88 ACCOUNT-INPUT-STATUS-OK VALUE "00" THRU "09".
88 ACCOUNT-INPUT-STATUS-EOF VALUE "10".
"00" THRU "09" is IBM's successful-completion class, not just "00": an
OPTIONAL file created on its first run reports "05", and a check written
NOT = "00" would stop a restartable batch on its first night.
Every I/O statement is checked, not only OPEN. And the copy out of the record
area is in the READ's own success phrase:
READ ACCOUNT-INPUT-FILE
AT END MOVE "10" TO ACCOUNT-INPUT-STATUS
NOT AT END
MOVE ACCOUNT-ID OF ACCOUNT-INPUT-RECORD TO
ACCOUNT-ID OF ACCOUNT-RECORD
END-READ
After AT END the record area is undefined. GnuCOBOL leaves the last record
sitting in the buffer, which is why this was wrong for a while and every local
test passed.
Loops
A loop carries a mandatory bound, and reaching it is a failure:
PERFORM UNTIL POST-ACCOUNTS-LOOP-1 >= 1000000 OR NOT
(ACCOUNT-INPUT-STATUS = "00")
...
END-PERFORM
IF POST-ACCOUNTS-LOOP-1 >= 1000000 AND
(ACCOUNT-INPUT-STATUS = "00")
DISPLAY "LOOP LIMIT 1000000 REACHED, WORK UNFINISHED"
UPON SYSOUT
MOVE 12 TO BANK-RETURN-CODE
...
END-IF
The condition is re-evaluated so the two exits are told apart exactly: the counter at the limit and the condition still true is the bound stopping work that had not finished. Without that branch, a five-million-record master processed the first million and ended RC=0.
What the JCL assumes
See jcl-model.md for the whole of it. The short version: the
default job EXECs IGYWCL, IBM's own compile-and-link cataloged procedure,
overriding only the DDs its parameter list documents as the caller's. Dataset
names are BANKLANG.COBOL, BANKLANG.LOADLIB and so on — placeholders for your
standards, and the one thing you will certainly change.
What to change for site standards
| What | Where |
|---|---|
| Dataset high-level qualifier | JCL_SOURCE_LIBRARY and its neighbours in the JCL emitter |
| Job card accounting | The JOB statement's (BANKLANG) |
CLASS, MSGCLASS, NOTIFY |
The JOB statement |
UNIT and SPACE |
The output DDs |
| LE run-time options | runtimeOptions in banklang.json |
| Compiler options | The CBL statements |
Whether records are inline or COPYd |
copybookMode in banklang.json |
What has not been done
No BankLang program has been compiled by IBM Enterprise COBOL, precompiled by
DSNHPC, bound to a Db2 package, or started in a CICS region. Everything local
runs under GnuCOBOL, which is a different compiler.
That is the project's standing limit, zos/README.md is the kit for closing it, and divergences.md is the list of places the two compilers are known or suspected to disagree. If you have a machine, an afternoon closes it.
Related pages
- generated-code-standards.md — the house style as a contract
- target-conformance.md — the rules the output obeys, with citations
- error-handling.md — the return-code contract
- numeric-model.md — precision, scale and rounding
- jcl-model.md — the generated job
- divergences.md — GnuCOBOL against Enterprise COBOL