BankLang

A compiler from a small banking language to readable IBM COBOL.

You write BankTS. Its types are TypeScript's — decimal<18, 2>, string<16> — and its statements are its own: transaction, file, cursor, queue. It emits IBM Enterprise COBOL, a copybook for every record, and the JCL to build and run it. No model decides what is generated, and the same input always produces byte-identical output.

Validated with GnuCOBOL, not IBM. No IBM Enterprise COBOL validation has been performed, and none is claimed. What that leaves open →

It refuses to compile unsafe programs

The build fails and produces no artifact, the way a type error does.

BankTS
transaction postTransfer(request: TransferRequest) {
  debit(request.debitAccount, request.amount);
  credit(request.creditAccount, request.fee);
}
What the compiler says
BANK-TXN-001  Transaction postTransfer has no idempotency key.
BANK-AUD-001  Transaction postTransfer does not emit an audit event.
BANK-LED-001  Transaction postTransfer does not balance: debited request.amount against credited request.fee.

Open this program in the playground →

A retry that posts twice, money moving with no audit trail, and a ledger that does not balance. Each is normally caught by a person — a reviewer who knows to look, an auditor reconciling afterwards. There are 16 such rules, among 113 diagnostics in all — each documented with an explanation and a remediation, and each one provoked by a test that fails if the rule stops working.

The COBOL it produces

Enterprise COBOL has one rounding phrase, and ROUNDED is half-up away from zero. Banker's rounding is arithmetic this compiler writes out.

BankTS
// Interest is balance * rate, rounded to the currency scale with banker's
// rounding. The rounding mode is required by the compiler, not optional.
function accrue(balance: MoneyBDT, rate: Rate): MoneyBDT {
  return round(balance * rate, "HALF_EVEN");
}
Generated COBOL
       ACCRUE.
           *> HALF_EVEN is generated. COBOL has
           *> only ROUNDED, which is HALF_UP.
           COMPUTE BANK-RND-1-VALUE = (ACCRUE-P1 * ACCRUE-P2)
               ON SIZE ERROR
                   DISPLAY "ARITHMETIC OVERFLOW ACCRUE-RESULT" UPON
                       SYSOUT
                   MOVE 12 TO BANK-RETURN-CODE
                   MOVE "ARITHMETIC-OVERFLOW" TO BANK-FAILURE-CODE
                   GO TO ACCRUE-EXIT
           END-COMPUTE
           COMPUTE BANK-RND-1-EXCESS =
               (ACCRUE-P1 * ACCRUE-P2) - BANK-RND-1-VALUE
           COMPUTE BANK-RND-1-STEP = 0.01
           IF BANK-RND-1-EXCESS < 0
               COMPUTE BANK-RND-1-STEP = -0.01
           END-IF
           COMPUTE BANK-RND-1-UNITS = BANK-RND-1-VALUE * 100
           EVALUATE TRUE
               WHEN FUNCTION ABS (BANK-RND-1-EXCESS) > 0.005
                   ADD BANK-RND-1-STEP TO BANK-RND-1-VALUE
               WHEN FUNCTION ABS (BANK-RND-1-EXCESS) = 0.005
                   IF FUNCTION MOD (BANK-RND-1-UNITS, 2) = 1
                       ADD BANK-RND-1-STEP TO BANK-RND-1-VALUE
                   END-IF
           END-EVALUATE
           MOVE BANK-RND-1-VALUE TO ACCRUE-RESULT
           CONTINUE.

That sequence is executed and compared against exact arithmetic over every boundary case, for a product and for a quotient, in all seven rounding modes. The overflow branch is not decoration: storing a scale-6 product in a scale-2 field would discard four digits, and the compiler will not do that silently.

What happens to the COBOL you already have

A classic sequential update: read a transaction file, apply each posting, write a new master and a reject file. Every estate has thirty of these.

The original
       FILE-CONTROL.
           SELECT TRANS-FILE   ASSIGN TO TRANSIN.
           SELECT MASTER-IN    ASSIGN TO MASTIN.
           SELECT MASTER-OUT   ASSIGN TO MASTOUT.
           SELECT REJECT-FILE  ASSIGN TO REJECTS.
      *    41 lines: the FDs and WORKING-STORAGE
       0000-MAIN.
           OPEN INPUT  TRANS-FILE
                       MASTER-IN
                OUTPUT MASTER-OUT
                       REJECT-FILE.
The BankTS it becomes
file transFile sequential input record TransRecord status transStatus;

file masterIn sequential input record MasterRecord status masterInStatus;

file masterOut sequential output record MasterRecord status masterOutStatus;

file rejectFile sequential output record RejectRecord status rejectStatus;

on error transFile {
  log "TRANSIN FAILED, STATUS ", transStatus;
  returnCode = 12;
}

Four SELECT statements with no FILE STATUS clause between them, and one OPEN whose outcome nothing tests. A missing input gives file status 35 and the first read hits AT END — the job ends with return code 0 having processed nothing. The layouts are unchanged, field for field, so the same datasets are read by the same DD names. What changes is everything the original left implicit.

Five conversions, side by side →

What it is not

The full list, with what each limit costs → For the person deciding →