BankLang

Divergences

Where GnuCOBOL and IBM Enterprise COBOL disagree, and where BankLang and the target disagree. Numbered so they can be cited.

This is the repository's best evidence that its claims are bounded, and it used to be buried in zos/README.md. Every entry is one of three kinds:

Local validation is GnuCOBOL 3.2.0 under tools/banklang-ibm.conf. Nothing in this repository has been compiled by IBM Enterprise COBOL, and until it has, every entry marked "suspected" stays suspected.


D1. USAGE NATIONAL inside a group — measured

GnuCOBOL 3.2.0 allocates four bytes per national character inside a group, where Enterprise COBOL holds each in two bytes of UTF-16. Standalone at the 01 level GnuCOBOL allocates two, which makes it an inconsistency in GnuCOBOL rather than a rule.

01  H.
    05  A2 PIC N(4) USAGE NATIONAL.
    05  C2 PIC X(4).
01  FLAT REDEFINES H PIC X(100).

C2 starts at byte 17 under GnuCOBOL and at byte 9 under Enterprise COBOL.

BankLang emits the Enterprise COBOL width, because Enterprise COBOL is the target, and warns BANK-TYPE-024 on every national<n> field to say that local validation does not cover it. The conversion between an alphanumeric and a national is refused outright: the bytes would differ between the two compilers and GnuCOBOL implements neither NATIONAL-OF nor DISPLAY-OF to make them agree.

This is the most likely thing in this repository to be wrong, and the cheapest to check.

D2. Report Writer totals a packed field wrongly — measured

GnuCOBOL 3.2.0 reads a COMP-3 operand of a SUM clause from the wrong place, picking up only its low-order digits. A PIC S9(7)V99 COMP-3 holding 1,000,000.00 totals as zero; one holding 9,999,999.99 totals as 999.99. The same field read by a SOURCE clause on the line above prints correctly, so the report shows right details under a wrong total.

01  TYPE IS CONTROL FOOTING FINAL.
    05  LINE PLUS 1.
        10  COLUMN 1  PIC ZZZ,ZZZ,ZZ9.99 SUM PACKED-AMT.
        10  COLUMN 20 PIC ZZZ,ZZZ,ZZ9.99 SUM DISPLAY-AMT.

Money is COMP-3 in every generated program, so every total in every report is wrong under the local validator and none of it says anything about z/OS. Small amounts survive the truncation and come out right by luck, which is how it went unnoticed. tests/report-writer.test.ts asserts the divergence directly and proves the totals over a zoned amount, which GnuCOBOL accumulates correctly.

Report totals are the first thing to check on z/OS.

D3. A report file will not bind to a DD name — measured

GnuCOBOL's default assign_clause resolves an unquoted ASSIGN TO <name> on a file carrying REPORT IS to report-section storage rather than to the DD name, so the output lands in a file named after a printed value — a filename like 0.00. Compile with -fassign-clause=external to bind it. On z/OS the DD comes from the JCL and the question does not arise.

D4. JSON PARSE and XML PARSE compile and do nothing — measured

GnuCOBOL warns -Wpending that neither is implemented, then leaves the record untouched and raises no exception — so a program reading a payload runs clean and processes an empty record. That is the worst shape a divergence can take, because every local signal says the program worked.

The local build routes both through the precompiler, which rewrites them into calls on BANKJSON and BANKXML in runtime/. The shipped artifact keeps the statement Enterprise COBOL implements.

The stubs are scans, not parsers: BANKJSON reads a quoted name at the top level and the scalar after its colon, BANKXML reads the next tag and the characters between tags. Nesting, arrays, escapes, attributes, namespaces, entity references and CDATA are past what either attempts. Every parse carries BANK-TYPE-025 for that reason.

D5. A CBL statement — measured

GnuCOBOL reads CBL in column 1 as text in the sequence number area and reports an invalid indicator in column 7. Every generated program opens with two of them, stating the compiler options its behaviour depends on, so every local compile goes through the precompiler to have them removed. The shipped artifact keeps them.

D6. A single-letter data name beside RECORDING MODEmeasured

Every generated QSAM FD carries RECORDING MODE IS F or V. GnuCOBOL will not then accept V as a data name in the same program; Enterprise COBOL will, V being nowhere in Appendix E's reserved word table. This is the local compiler being stricter than the target rather than a construct the target refuses, and it only arises for a one-character record or field name.

D7. VALUE on an EXTERNAL item — measured

Enterprise COBOL honours a VALUE clause on an elementary EXTERNAL item; GnuCOBOL ignores it and leaves the storage at LOW-VALUES. Both failure registers are EXTERNAL, so neither carries a VALUE clause and BANK-MAIN sets both — otherwise BANK-FAILURE-CODE NOT = SPACES would be true before anything had failed, on one of the two targets only.

D8. cobc -x refuses PROCEDURE DIVISION USINGmeasured

"Executable program requested but PROCEDURE/ENTRY has USING clause". A batch program that takes entry parameters reads them from the job's PARM, so it has a USING clause; a Unix process has no parameter list to pass. z/OS says the same thing the other way round, by having the initiator build one before the program is entered.

Locally such a program is compiled as a module and driven from a generated driver that builds the parameter list. The GnuCOBOL gate compiles it with -m.

D9. COMP sizing and SYNCHRONIZEDsuspected

Halfword, fullword and doubleword boundaries, and what SYNC skips to reach one. BankLang emits IBM's allocation — 1–4 digits in two bytes, 5–9 in four, 10–18 in eight — and computes slack from it.

D10. The COMP-3 sign nibble on an unsigned field — suspected

And what NUMPROC does to a comparison against one. The generated program is compiled NUMPROC(NOPFD), which does not assume a preferred sign.

D11. Collating sequence — suspected

EBCDIC ordering is not ASCII ordering. Any comparison of alphanumerics, any SORT key, and any 88 level with a THRU range orders differently on the two. FEED-STATUS-OK VALUE "00" THRU "09" is safe — digits are contiguous and in the same order in both — but a range over letters is not.

The Language Reference is explicit for a sort: "When both the COLLATING SEQUENCE phrase and the PROGRAM COLLATING SEQUENCE clause are omitted, the EBCDIC collating sequence is used." BankLang emits neither, so an alphanumeric sort key is ordered in EBCDIC on the target and in ASCII by both local engines. Every alphanumeric ordering in tests/sort-differential.test.ts is therefore agreement about ASCII and says nothing about z/OS; digits and uppercase letters keep their relative order in both, and a key mixing them does not — digits sort before letters in ASCII and after them in EBCDIC. The numeric key cases are unaffected, because a numeric key is compared as a number on any target.

D12. Sort work datasets and the sort product — suspected

The generated job allocates three SORTWK datasets, which is customary. Which sort product runs, and what it wants, is a site's.

D13. Reserved word lists — suspected

The two are close but not identical. BankLang mangles against the union, so a name acceptable to IBM may still be mangled here — which is safe but visible.

D23. A final line-sequential record with no delimiter — measured

GnuCOBOL 3.2.0 does not deliver the last record of a line-sequential file when that line has no trailing newline and its length exactly fills the record area. It sets file status 06 and the record is lost.

       FD  F.
       01  R PIC X(20).
$ printf 'AAAAAAAAAAAAAAAAAAAA\nBBBBBBBBBBBBBBBBBBBB' > in.txt
REC 001 [AAAAAAAAAAAAAAAAAAAA]
STATUS 06

With a trailing newline both records arrive and the loop ends on 10. With records shorter than the record area both arrive even without the final delimiter, so the condition is specifically "unterminated and exactly the record length".

Enterprise COBOL's Programming Guide describes the end-of-file case as "The remainder of the record area is filled with spaces", which reads as delivering the record. Whether it does has not been checked on the target.

The generated read loop tests the file status before using what it read, so a BankLang program skips the record rather than processing a partial one — the safe end of the difference. A program must not depend on a feed whose last line lacks a delimiter.

Found by tests/line-sequential.test.ts, which pins the measured behaviour.

D25. DISPLAY of SORT-RETURNmeasured, between the local two

The Language Reference gives the register as 01 SORT-RETURN GLOBAL PICTURE S9(4) USAGE BINARY VALUE ZERO. GnuCOBOL 3.2.0 defines it wider: DISPLAY SORT-RETURN after a failed sort prints +000000016 there and 0016 under packages/cobol-runtime, which holds the Reference's picture.

How wide an undeclared special register renders is implementation-defined and IBM's answer is a third unknown, so neither local engine is wrong. The emitter moves the value into a declared PIC 9(4) item and displays that instead — the same rule it already follows for the result of an intrinsic (D22) — so no generated program depends on it.

D26. The order of records with equal sort keys — deliberate

Language Reference, SORT format 1: "If the DUPLICATES phrase is not specified, the order of these records is undefined." A compiler whose claim is a deterministic build must not emit a statement whose output order the target leaves open, so WITH DUPLICATES IN ORDER is emitted on every SORT. GnuCOBOL 3.2.0 happens to be stable without it, which is exactly the kind of agreement that means nothing.

MERGE has no such phrase and needs none: equal keys come back in USING order, which the Reference already fixes.

D27. A sort does not set the file status under GnuCOBOL — measured

Under NOFASTSRT with a FILE STATUS clause and no ERROR declarative — which is every program this compiler emits — the Programming Guide's table 32 says to "test the SORT-RETURN special register after the format 1 SORT statement, and test the file status key". GnuCOBOL 3.2.0 does not set that key for a USING or GIVING file at all: probed directly, a successful sort leaves both keys at their VALUE SPACES, and a sort whose input dataset is missing sets SORT-RETURN to 16 and still leaves them at spaces.

Spaces are not in "00" THRU "09", so the emitted IF NOT ...-STATUS-OK fired on every successful sort: task_func_13 and task_func_38 were both recorded as benchmark passes while ending with return code 16 and printing SORT FAILED over correct output files. The check is now guarded by NOT = SPACES — the key is declared VALUE SPACES and only an I/O operation writes it, so spaces means the sort reported nothing through it. On a target that sets the key, "00" is not spaces and the guard changes nothing.


D24. Moving a blank zoned field — measured, between the local two

Where a line-sequential record is shorter than the record area, the remainder is space-filled — so a numeric field the input never supplied holds spaces, which is not a valid zoned number. cobc and packages/cobol-runtime then disagree about what a MOVE of that field does:

Result
GnuCOBOL 3.2.0 the spaces are carried through unchanged
packages/cobol-runtime the field is normalised to 000000000+

On the target this is a data exception — S0C7 — and neither answer is right; Enterprise COBOL would abend rather than produce either. It is the defect class OpenCBS records as DF12, DF19, DF28 and DF41, and the reason BankTS refuses to move a string into a decimal at all.

BankLang cannot rule it out statically, because the invalid bytes come from outside the program. What it can do is what it already does: the record layout is declared, the file status is checked, and a program reading a feed whose records may be short should test the field it depends on rather than assume it.

tests/line-sequential.test.ts asserts only the behaviour the two engines agree on, so this difference does not silently become an assertion that either is correct.


D22. DISPLAY of a bare intrinsic — measured, between the local two

DISPLAY FUNCTION ORD(X) prints 000000109 under GnuCOBOL and 109 under packages/cobol-runtime. How wide the intermediate result of an integer function is, and so how a DISPLAY of one renders, is implementation-defined: neither is wrong and IBM's width is a third unknown.

Nothing generated does this — the emitter moves a function result into a declared item before displaying it, which is defined — so no example is affected. tests/runtime-semantics.test.ts follows the same rule rather than pinning either width, and runtime/*.cbl should too.


Deliberate differences

D14. NOSSRANGE, and a generated bounds check instead

SSRANGE would range-check every subscript for free. The generated program checks its own and fails the step with a named failure instead, because SSRANGE abends rather than setting a return code the next step's COND= can read, and because it is a compile option — a program built without it silently loses the checking, where a check in the source cannot be switched off by a JCL change.

D15. No floating point

COMP-1 and COMP-2 exist in the target and are not in the language. A bank's arithmetic is decimal, and binary floating point cannot represent 0.10. The copybook and DCLGEN importers refuse a floating-point column rather than approximating it.

D16. No ALTER, no GO TO the source can write, no PERFORM THRU a range

the source chose

The one GO TO in a generated program is the failure path, and it goes to the enclosing routine's exit. ALTER is not emitted at all.

D17. Five rounding modes are generated arithmetic

Enterprise COBOL has one rounding phrase and ROUNDED is half-up away from zero. HALF_EVEN, HALF_DOWN, UP, CEILING and FLOOR are written out as a truncation, the excess that truncation discarded, and a conditional step of one unit in the last place. See numeric-model.md.

D18. FILLER is reserved <n>;

BankTS declares bytes nothing names as reserved 20;, which emits FILLER PIC X(20). The importer counts bytes rather than digits — a PIC S9(9) COMP-3 filler is nine digits and five bytes — and refuses a copybook whose fillers it cannot size, because a record one byte short moves every field after it.

Nothing can read a reserved slot, assign to it, or move a record through it. FILLER is not a name in COBOL either.

D19a. A host-variable array is passed by its first element locally

The real Db2 precompiler generates a call that passes a host-variable array by address. packages/precompiler passes NAME (1) instead, because a COBOL CALL ... USING naming an item with an OCCURS and no subscript is a compile error and the subset has no address-of.

What the local compile establishes is unchanged: that the operands resolve and are the types the statement needs, the first element being the same type as the rest. What ships to z/OS keeps the EXEC SQL FETCH NEXT ROWSET exactly as written, and DSNHPC generates the real call.

runtime/DSNHLI writes host variables. A script beside the program gives the bytes of each one, keyed by the statement number, the call, the row within that call, and the variable's position in the generated CALL; the stub moves them into the storage the caller passed and sets SQLERRD(3) to the number of rows that call delivered. Passing the array by its first element turns out not to cost anything here: CALL ... USING passes by reference, so the first element's address is the array's, and each row lands one element further along.

A rowset loop is therefore executed, including the property that makes the feature worth having — three rows over a rowset of two is one full set and one partial one, and every row is processed exactly once. tests/conformance.test.ts asserts the count under cobc and under the interpreter, and the SQLERRD(3) of the call that ends the cursor is zero rather than the previous call's count, which is what stops the last set being read twice.

This paragraph previously read "no rowset loop in this repository has been executed", on the reasoning that the local runtime could not set SQLERRD(3). It could: the SQLCA is its first parameter.

What remains true is that it is a stub. It parses no SQL, binds no plan, and knows nothing about a row beyond the bytes the script names — so what is established is that the generated loop handles the rowset protocol correctly, not that Db2 would return these rows for this query.

D19. No varying-length string

Db2's VARCHAR is a group of two level-49 items, a halfword length and the text. There is no BankTS declaration for one, so bankc dclgen import reports the column.

D20. A generated zUnit driver compiles against a stand-in EQAITERC

The driver declares its info block as 01 AZ-INFO-BLOCK. COPY EQAITERC., because that is what IBM's own generator writes and what resolves on z/OS from the IDz copybook library. That copybook is not in this repository, so a local compile has nothing to resolve — and runtime/zunit/EQAITERC.cpy declares the two fields the driver names, ITER and TC-WORK-AREA, and nothing else. Inventing the rest would be a claim about a layout nobody here has seen.

What the compile establishes is therefore narrow: the driver's syntax is accepted under both dialects and every name in it resolves. It establishes nothing about the info block's offsets, and no generated case has been run, locally or on z/OS. pnpm bankc zunit output is graded "compiled" for that reason.

The artifact that ships is unaffected: it carries COPY EQAITERC, exactly as IBM's generator writes it.

D21. noPlaybackData="true" is inferred

A generated case supplies its data in the driver rather than replaying a recording, so it writes <runner:playback moduleName="…"/> with no file and sets noPlaybackData="true" on each test.

The attribute is in the 4.0.0.0 configurations observed, and every one of them carries false — because every one of them has a recording. Nothing public carries true, so the value is read from the attribute's name and from the 3.0.0.0 case that has an empty <runner:playback> element and no such attribute at all.

If a runner refuses it, the fallback is noPlaybackData="false" with the same empty playback element, which is the 3.0.0.0 shape. One real run settles it.


What closing these looks like

zos/README.md is the kit: pnpm zos:kit writes every program, copybook and job in the member names the JCL expects, and RESULTS-TEMPLATE.md is what to fill in. A finding that contradicts something this repository claims is the most valuable thing that exercise can produce.


Read this page as Markdown on GitHub →