BankLang

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:

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.


Read this page as Markdown on GitHub →