Banking Safety Specification
1. Diagnostic philosophy
BankLang must catch banking and mainframe hazards before COBOL is generated.
Diagnostics are product features. They should be stable, documented, and useful.
Every diagnostic has:
- ID
- severity
- title
- explanation
- source span
- backend profile
- remediation hint
2. Diagnostic ID namespaces
BANK-SYN-* syntax
BANK-TYPE-* type system
BANK-DEC-* decimal/money
BANK-TXN-* transaction
BANK-LED-* ledger
BANK-AUD-* audit
BANK-SQL-* Db2/SQL
BANK-CICS-* CICS
BANK-DLI-* IMS DL/I
BANK-MQ-* IBM MQ
BANK-FILE-* VSAM/file IO
BANK-COPY-* copybook/layout
BANK-GEN-* code generation
BANK-SEC-* security
BANK-JOB-* job descriptor
Two of these are not about a BankTS program at all. BANK-COPY-008 through
BANK-COPY-012 are about a copybook or a DCLGEN member handed to the compiler,
and BANK-JOB-* is about a job.json. They carry identifiers for the same
reason everything else here does: before the 2026-08-07 audit's F3 they were
throw new Error, and a message with no identifier is one nobody can look up,
count or link to.
3. Syntax and type diagnostics
BANK-SYN-001 unexpected token
The parser expected a specific keyword, identifier, number, or punctuation.
BANK-SYN-002 unexpected construct
The parser reached a token that cannot begin a declaration, statement, type, or expression.
BANK-TYPE-000 no AST provided
Type checking ran without a parsed program. Parser errors must be fixed first.
BANK-TYPE-001 unresolved type or symbol
A type name or value symbol could not be resolved in scope.
BANK-TYPE-002 invalid type parameters
Decimal precision/scale or string length parameters are outside the supported range.
BANK-TYPE-003 type mismatch
An expression, argument, return path, or branch does not match its expected type.
BANK-TYPE-004 invalid statement position
A statement appears where the subset does not allow it, such as after a terminal statement or in a function body with no terminal statement.
BANK-TYPE-005 duplicate symbol
A parameter or local variable name is declared more than once in one scope.
BANK-TYPE-006 unknown record field
Field access names a field the record does not declare.
BANK-TYPE-007 statement not allowed in this body
A ledger or audit statement appears outside a transaction, or a return or if statement appears inside a transaction body.
BANK-TYPE-014 generic expansion does not terminate
A generic function calls itself at a type argument that keeps changing. Generics are monomorphised, so every new type argument creates another instantiation.
BANK-TYPE-015 generic function is never instantiated
Nothing is generated for an uninstantiated generic, and its body is never checked against real types.
BANK-TYPE-016 record inheritance cycle
A record extends itself, directly or through another record.
BANK-TYPE-017 inherited field redeclared
A derived record declares a field its base already declares. Both would land in one COBOL group under the same name.
BANK-TYPE-018 wrong number of type arguments
A generic record was used with the wrong number of type arguments, or with none.
BANK-TYPE-019 type arguments on a non-generic type
A type that declares no type parameters was given type arguments.
BANK-TYPE-020 type argument cannot be inferred
A type parameter appears in no parameter type, or two arguments disagree about what it stands for.
BANK-TYPE-021 record argument is not a named record
A record argument is passed by reference: the caller points the callee's
LINKAGE cell at the argument's storage. A subscripted element has no address
the caller can take without evaluating the subscript first, so it must be
assigned into a record and passed by name.
BANK-TYPE-022 two transaction parameters share one record
A transaction is a program entry point, so its record parameters live in working
storage — one COBOL group per record type. Two parameters of the same type would
be two names for one piece of storage, and writing through either would be
visible through the other. A function is unaffected: its record parameters are
LINKAGE cells the caller rebinds.
BANK-TYPE-023 invalid edited field
An edited<T, "style"> field names a style the compiler does not know, or asks
to render something with no edited form. A picture nobody checked is a report
column that silently loses digits.
BANK-TYPE-024 national layout is not locally verifiable
A warning, on every national<n> field.
national<n> emits PIC N(n) USAGE NATIONAL. Enterprise COBOL holds each
character in two bytes of UTF-16, and that is the width the layout report, the
copybook, and the copybook inspector all use. GnuCOBOL 3.2.0 allocates four
bytes per character inside a group — measured, not assumed — and warns on every
such line that its handling of USAGE NATIONAL is unfinished.
Byte-exact layout is the only thing the type promises, so this is the one place
where the compiler emits a record its own validator reads differently: under
GnuCOBOL every field after a national sits at a different offset. The warning is
there so the local evidence cannot be mistaken for a check that happened.
zos/README.md records the divergence as something to verify first.
BANK-TYPE-025 parsed document cannot be checked locally
A warning, on every json <text> into <record>.
Enterprise COBOL implements JSON PARSE. GnuCOBOL 3.2.0 compiles it, warns that
it is not implemented, and then does nothing at run time: the record is left
untouched and no exception is raised, so a program reading a payload runs
clean and processes an empty record.
The local build no longer runs that. The precompiler rewrites the statement into
calls on BANKJSON, a reference stub, so the record is populated from the
document and the JSON-STATUS test below is reached with a value it did not
invent — the same routing EXEC SQL and EXEC CICS already had. What ships to
z/OS keeps its JSON PARSE. The warning stays because the stub is a scan and
not IBM's parser: runtime/README.md lists what it does not attempt.
On Enterprise COBOL the hazard is different but has the same shape. A parse can
meet a nonexception condition, which does not terminate the statement and
"might result in the receiver being partially modified" — so on error is never
reached and the record holds some fields and not others, which is exactly what a
record that parsed cleanly looks like. Only JSON-STATUS distinguishes them, and
the compiler emits a test of it that reports the case to the job log. It reports
rather than raises, because a nonexception condition is not always an error: a
document carrying fields the record does not declare is one of them.
Verify the program on z/OS before relying on what it reads, and check the record
rather than trusting the failure path — a parse that did nothing does not report
one. zos/README.md records the divergence.
BANK-TYPE-026 invalid xml read
XML PARSE is event-driven, so xml <text> into <record> has no COBOL to
become: neither Enterprise COBOL nor GnuCOBOL has a form that fills a record.
(For what the local build does with the form that exists, see BANK-TYPE-025
above: the precompiler drives the generated handler from BANKXML.)
The form that exists is
xml message.body processing {
element "BALANCE" into account.balance;
};
and its bindings have to make sense: at least one element, each element bound
once — a second binding for the same name would never be reached — and each read
into something characters can be moved into, which is a string<n> or a number.
json <text> into <record> fills a record directly if the document is JSON.
BANK-TYPE-027 nested function is recursive
COBOL forbids LOCAL-STORAGE in a contained program, so a nested function's
locals sit in WORKING-STORAGE — one copy shared by every invocation. A
recursive one would overwrite its own locals on the way down and read the
innermost call's values on the way back out: it compiles, it runs, and it returns
the wrong number.
Drop nested. An ordinary recursive function is emitted as a sibling program
with LOCAL-STORAGE, which is what makes recursion safe.
BANK-TYPE-028 invalid sorted search
search sorted becomes COBOL SEARCH ALL, a binary search. COBOL will bisect a
table only if the declaration says it is ordered — ascending <field> — and only
on equality against that key, because anything else has no ordering to cut in
half.
This matters more than a type error usually does: a SEARCH ALL on a table that
is not actually sorted does not fall back to scanning it. It returns the
wrong row, or reports no match on a row that is there.
Use a plain search to walk a table any other way.
BANK-TYPE-029 invalid dynamic call
call <name> using <record> names its load module by a value rather than by a
literal in the source, so the name has to be text and short enough to be one:
eight characters, because a longer field is truncated to a name that does not
exist and the failure then arrives as a missing module rather than as a length.
What the compiler cannot check is whether the module is there. That is the
nature of a dynamic call, and it is why a call with no on error is warned
about: a static call that cannot be resolved fails at link time where somebody
sees it, and a dynamic one fails in the middle of a batch.
BANK-TYPE-030 a value-building call nested in an expression
concat, now, countOf and replaceChars build a value rather than name
one: each lowers to a COBOL statement — STRING, INSPECT, a CURRENT-DATE
sequence — writing into a field of its own. COBOL has no expression to nest that
in, so the call can be the whole right-hand side of an assignment, the whole
initialiser of a local, or the whole returned expression, and nothing else.
Written anywhere else there is nothing for the backend to emit. Before this
diagnostic existed it raised an internal invariant, so
toNumber(concat("0.", substring(rate, 7, 3))) — a reasonable way to read a
rate written in thousandths — reached the author as a stack trace.
let built: string<5> = concat("0.", substring(rate, 7, 3));
work.rate = toNumber(built);
4. Decimal diagnostics
BANK-DEC-001 floating-point money forbidden
Money cannot use binary floating-point representation.
BANK-DEC-002 implicit scale narrowing
Assigning decimal<18,4> to decimal<18,2> requires explicit rounding.
BANK-DEC-003 missing rounding mode
Division or scale conversion requires an explicit rounding mode.
BANK-DEC-004 possible overflow
Operation may exceed target precision.
BANK-DEC-005 currency mismatch
Different currency types cannot be added/subtracted without explicit conversion.
5. Transaction diagnostics
BANK-TXN-001 missing idempotency key
A transaction that posts financial effects must have a typed idempotency key.
BANK-TXN-002 missing rollback path
A transaction backend requires rollback representation but no rollback path can be generated.
BANK-TXN-003 unsafe non-deterministic operation
Transaction contains an operation with backend-dependent behaviour.
BANK-TXN-004 unbounded loop in transaction
Transaction contains a loop without static bound or approved termination proof.
BANK-TXN-008 invalid failure code
A raise code is empty or wider than BANK-FAILURE-CODE. A truncated code
would not match the handler that tests it.
BANK-TXN-009 failure handler raises
An on failure handler contains a raise. There is no outer handler to catch
it.
BANK-TXN-010 more than one entry transaction
COBOL enters a program at one place, so only one transaction can be the entry point.
6. Ledger diagnostics
BANK-LED-001 unbalanced posting
Debit total may not equal credit total on at least one commit path.
BANK-LED-002 missing ledger entry
Money movement occurs without ledger posting.
BANK-LED-003 inconsistent value date
Posting date and value date policy is missing or inconsistent.
BANK-LED-004 posted amount does not fit the ledger interface
BANK-LEDGER-AMOUNT is PIC S9(16)V99. A wider integer part or a finer scale
loses digits in the MOVE, and COBOL truncates silently.
7. Audit diagnostics
BANK-AUD-001 missing audit event
Financial transaction path lacks audit event.
BANK-AUD-002 restricted data reaches a log
A value marked sensitive reaches an audit event or a ledger posting. Both are
durable records that outlive the transaction and are read by people with no
business seeing a card number or a national identifier.
BANK-AUD-003 audit event name is not compile-time constant
Audit event names must be statically known.
8. SQL diagnostics
BANK-SQL-001 SQLCODE not handled
A generated SQL operation must handle success, not found, and error branches.
BANK-SQL-002 dynamic SQL disallowed
Dynamic SQL is not supported in the selected backend profile.
BANK-SQL-003 host variable layout mismatch
SQL host variable does not match expected COBOL field layout.
BANK-SQL-004 transaction commit ambiguity
A commit or rollback appears inside a CICS transaction. CICS owns the unit
of work there and commits Db2's work along with everything else, so an
EXEC SQL COMMIT is not merely redundant — Db2 rejects it at run time.
BANK-SQL-005 cursor and statement confused
A cursor was run with execute, or a sql statement was read with a cursor
loop. One lowers to a single EXEC SQL; the other to DECLARE, OPEN,
FETCH, and CLOSE.
BANK-SQL-006 cursor row binding missing
A cursor declares no result record, or no INTO clause naming where a fetched
row lands, so the generated FETCH would have nowhere to put one.
9. CICS diagnostics
BANK-CICS-001 CICS response code not handled
A CICS command must handle response code.
BANK-CICS-002 unsupported CICS operation
Selected backend profile does not support the requested operation.
BANK-CICS-003 syncpoint misuse
Transaction uses syncpoint in an invalid scope.
10. File diagnostics
BANK-FILE-001 file status not checked
File operation result is ignored.
BANK-FILE-002 record layout mismatch
File record layout differs from declared copybook.
BANK-FILE-003 unsafe restart behaviour (warning)
A transaction posts to the ledger inside a loop without both halves of checkpoint/restart. A job that dies halfway is rerun, and without a position written down the rerun starts at the beginning and posts everything twice — and a position written down but never read back leaves the rerun starting at the beginning just the same. Reported as a warning because the compiler cannot tell whether the job is rerunnable another way.
The same id covers a restart file that is not indexed update. A sequential
output file is rewritten from the start by the next OPEN, so a rerun that dies
before its own first checkpoint destroys the position it was resuming from.
BANK-FILE-004 invalid key declaration
An alternate record key names something that is not a field of the file's record, or the file has no index for one to live in. Only an indexed file has alternate keys.
BANK-FILE-005 file operation does not match the declaration
A rewrite or delete needs the file open for update, because updating a
record in place means finding it first. A start or readNext browses an
index, which a sequential file does not have.
It also reports a file given more than one error handler, which is what COBOL allows.
BANK-FILE-006 invalid sort procedure
A sort procedure works through a record variable that does not hold the record
being sorted, or release appears where no sort is running — it hands a record
to a sort in progress, so it means nothing elsewhere.
An input procedure that never reaches a release sorts an empty file, and a
merge has no input procedure at all: its premise is that the inputs already
arrive in order, and a procedure that could drop or reorder records would break
it.
BANK-FILE-007 invalid page declaration
A page depth describes a print file, so it belongs to a sequential output file,
and its footing has to be a line the page has — past the end it would never be
reached. advancing writes a report line, and on page is signalled from the
page counter, so a file with no declared depth never reaches the end of one.
It also covers a report on a file that is read rather than written, and a
report on a file that already declares a page depth: both decide where the page
ends, and COBOL rejects an FD that says so twice.
BANK-FILE-008 invalid report description
A report's names have to resolve, or the generated COBOL means nothing:
- a control field must be a field of the record the report prints;
- a
controlHeadingorcontrolFootingmust name a control the report breaks on, or none at all, which meansFINAL; sumtotals a numeric field — adecimalor a currency amount. It accumulates with COBOL'sADD, which will not take a string, and will not take an already-edited field either, that being a display form rather than a number;sumbelongs in acontrolFootingor apageFooting. This one is stricter than COBOL: Report Writer allows aSUMclause in a group of any type, including a heading, and prints whatever had accumulated when the heading was written. banklang refuses it because a total read before the details it covers is a figure no reader can check against the lines around it;- a report needs at least one
detailgroup, since a report with nothing to generate prints its headings and stops; generatenames a detail group, whileinitiateandterminatename the report itself.
10b. IBM MQ diagnostics
BANK-MQ-001 invalid queue access
An MQ statement names a queue that is not declared, or reaches one with no
status field, or the declaration names a queue manager or a queue longer than MQ
carries. MQ_Q_MGR_NAME_LENGTH and MQ_Q_NAME_LENGTH are both 48, which is what
MQOD-OBJECTNAME and MQCONN's first parameter are declared as, so a longer
name is truncated into one the queue manager does not have.
The status field matters most. MQ reports what happened in a completion code and
a reason code, and without somewhere to read the reason a getMessage that
found an empty queue is indistinguishable from one that read a message — so the
program goes on to process whatever the message area held last.
BANK-MQ-002 queue used against its direction
A queue is opened for input or for output, not both, because that is what the
MQOPEN options say: an output queue is opened MQOO-OUTPUT and an input
queue MQOO-INPUT-AS-Q-DEF. Reading a queue opened for output fails at run time
with reason 2037, MQRC-NOT-OPEN-FOR-INPUT; putting to one opened for input
fails with 2039, MQRC-NOT-OPEN-FOR-OUTPUT. Both are refused when the program is
compiled instead.
It also covers a message record of the wrong shape: the buffer is the record the queue declares, and MQ moves bytes without checking what they mean.
10a. IMS DL/I diagnostics
BANK-DLI-001 invalid DL/I access
A DL/I statement names a database that is not declared, moves a segment into a record of the wrong shape, looks for a key that is not text, or reaches a database with no status field.
The segment and key names are eight bytes each, because that is what a search argument carries — a longer one is truncated into a name matching nothing in the DBD.
The status field is the one that matters most. The two characters DL/I leaves in
the PCB are the entire error model: spaces worked, GE found nothing, GB
reached the end of the database. Without somewhere to read them, a getUnique
that found nothing is indistinguishable from one that worked, and the program
goes on to use whatever the segment area held last.
11. Copybook diagnostics
BANK-COPY-001 unsupported PIC clause
Copybook contains a PIC clause not supported by current parser.
BANK-COPY-002 unsupported REDEFINES shape
REDEFINES construct cannot be represented safely in BankTS subset.
BANK-COPY-003 incompatible layout change
New copybook layout changes field offsets or byte lengths incompatibly.
BANK-COPY-004 invalid variant record clause
A redefines names something COBOL will not let it redefine. A depending on
names something that is not a count declared before the table, which COBOL reads
to decide the record's length.
A redefinition may be longer than what it redefines. Enterprise COBOL then
extends the storage area rather than overrunning it — the Language Reference
gives 05 A PIC X(6). redefined by 05 B REDEFINES A PIC N(4)., eight bytes
over six, as a legal example — and the record runs to the end of the longest
reading of the area, so every field after it moves by the overhang. The layout
report shows this. The one case COBOL forbids is a redefined item declared as an
external data record, which this language cannot declare.
What it rejects:
- A field that is not declared before it. A redefinition re-reads storage that already exists.
- A field that is not the one immediately before it. COBOL requires the
redefinitions of an area to follow its description with nothing in between
that takes storage of its own; a further redefinition may name either the
original or the redefinition before it.
A; X; B redefines Ais rejected by Enterprise COBOL and by GnuCOBOL ("REDEFINES must follow the original definition"). - A table. A table is a repetition of an area rather than one area, so there
is no single run of bytes to alias, and COBOL forbids
OCCURSon a redefined item. Wrap the table in a record and redefine that. - A
depending onon the redefining field. Neither end of a redefinition may vary in length: the area's size has to be known to lay out what follows.
On depending on, this compiler is stricter than COBOL and says so: the
varying table has to be the last field in the record that takes storage. A field
declared after it is variably located — it sits at the start of the table plus
the count times the entry, so it moves every time the count does. IBM calls this
complex ODO and permits it. The layout report and the copybook could then only
state the offset that field has when the table is full, which is an offset no
other record has, and a copybook naming a byte position nothing is at is worse
than no copybook. GnuCOBOL refuses the shape outright ("cannot have OCCURS
DEPENDING because of ..."), so a program built this way could not be executed
locally either. The convention it stands against is ordinary: a variable-length
record ends with its table.
BANK-COPY-005 invalid field clause
justified right-aligns an alphanumeric value, so a number cannot carry it — a
number's alignment is decided by its picture. blankWhenZero prints spaces for
a zero, so there has to be a number to be zero.
BANK-COPY-006 invalid initial value
A field's initial value becomes a COBOL VALUE clause, which the compiler
evaluates when it compiles. It has to be something the compiler can see: a
written number, string, boolean, or enum member of the field's own type, short
enough to fit — a VALUE longer than its field would be truncated silently, so
it is refused instead.
A REDEFINES field cannot carry one. It has no storage of its own, only a
second reading of another field's bytes, so the value belongs on the field being
redefined.
BANK-COPY-007 two records share one copybook member
A PDS member name is one to eight characters of letters, digits, and the
national characters, with no hyphens — and that is also all the COBOL compiler
looks at when it resolves a COPY from a PDS: "only the first eight characters
of text-name are used as the identifying name".
So AccountRecord and AccountRow are both the member ACCOUNTR. One copybook
overwrites the other in the library, and every program that copies either gets
whichever was written last: a record with the name it asked for and different
fields at different offsets, which is the one thing a copybook exists to
prevent. Rename one so the two differ within those eight characters.
The same rule applies across a whole copybook library, not just within one
program, so pnpm zos:kit refuses to build a bundle whose members would
overwrite each other rather than shipping one under the other's name.
BANK-FILE-009 invalid varying record
varying <min> to <max> length <field> becomes RECORD IS VARYING IN SIZE. The
bounds have to be a range of lengths — a shortest of at least one character, and
no longer than the longest.
The file has to be sequential: an indexed or relative dataset addresses a
record by key or by position, which a varying length would move.
BANK-FILE-010 update with nothing read
rewrite and delete replace the record the last read returned, so on a file
the program accesses sequentially they need one. Without it the operation is
not performed and the file status is 92 — no abend and no exception, so a
program that does not test the status carries on believing it updated something.
Only sequential and relative files are affected. An indexed file is
ACCESS MODE IS DYNAMIC, where the record key in the record area says which
record is meant and no prior read is required.
A read in an enclosing block covers a branch inside it, but a read inside a
branch does not travel back out — the path that skipped the branch reaches the
update with nothing read. This is the same rule, and the same reasoning, as
BANK-DLI-002.
BANK-FILE-011 delete on a sequential or line-sequential file
Enterprise COBOL has no DELETE for a file with sequential organization: a
record is removed by leaving it out of the file the next program writes, not by
deleting it in place. The same holds for a line-sequential file, where the
Programming Guide is explicit that after a record is created "you cannot change
its length or its position in the file, and you cannot delete it".
GnuCOBOL compiles the statement, so local validation does not catch this one —
the program passed every check here and would have been rejected by IGYCRCTL.
BANK-FILE-013 a line-sequential file opened for update
"You can open a line-sequential file as INPUT, OUTPUT, or EXTEND. You cannot open a line-sequential file as I-O." A record ends at a newline, so rewriting one in place would move where every following record starts.
A text file is amended by reading it and writing a new one, which is what a job that rebuilds an extract already does.
BANK-FILE-014 a line-sequential record holding something unprintable
A line-sequential file is text. Enterprise COBOL requires that records "contain
only USAGE DISPLAY and DISPLAY-1 items", and BankTS's default is the thing that
is forbidden: decimal<13,2> lowers to COMP-3, two digits a byte with a sign
nibble. Written into a text file it produces bytes that are neither the number
nor readable text — and the WRITE succeeds, so nothing says so until somebody
opens the file.
Declare the number zoned if it can be negative, which emits the SIGN IS TRAILING SEPARATE the same paragraph requires of a signed zoned field, or
unsigned if it cannot. A currency amount is packed by construction, so an
interchange record uses a decimal field with a zoned usage.
BANK-FILE-015 several record layouts on a file that cannot choose between them
COBOL puts several 01 entries under one FD: a report whose heading line and
detail lines are different shapes, a feed of header, detail and trailer
records. They share one record area, and each WRITE names the layout it is
writing — so on an output file the variant is decided by the program and its
type is known where it is chosen.
A READ names nothing. Which layout arrived is decided by the data, and a value
whose type is a guess is exactly what this language exists not to hand back. The
same goes for a record key, which belongs to one layout, and for RECORD IS VARYING, which describes the length of one record rather than a choice between
several.
file bills lineSequential output record BillHeading, BillDetail status billsStatus;
For a file that is read, declare one record and interpret the bytes yourself: a
field saying which kind of record it is, and a redefines for the rest.
Note what that costs, because it is the honest half of this rule. A redefines
is COBOL's, so nothing checks that the type field was tested before the overlay
was read — the compiler refuses to hand back a value whose type is a guess, and
it cannot stop you making the guess by hand.
Whether to lift the restriction is a question about evidence, and the evidence
is measured rather than argued. evidence/horizontal/xcobol-v2/record-usage.json:
143 of X-COBOL's 6,451 file descriptions carry several records and are opened
INPUT, and those 143 are 51 distinct file contents — a corpus of 168
repositories counts a program vendored into five of them five times. Of the 51,
21 are parser, grammar, language-server and compiler-test fixtures, 16 are the
NIST CCVS85 conformance suite, and 14 are textbook and course material. None is
an application. Eleven of the fourteen are one shape: a record code in the
leading field, named by 88 levels, with variants of different lengths.
So the pattern is real and uniform where it appears, and no program in the corpus that exists to do a job reads a file this way. The restriction stands until that changes; what would change it is application code, and the measurement is rerun on every analysis pass.
BANK-FILE-016 a DD name that is also a data item
The generated SELECT reads ASSIGN TO <DD>, and both Enterprise COBOL 6 and
GnuCOBOL treat that word as a data item when one of that name exists — taking
the file name from its contents rather than from the environment. A record Feed becomes the group FEED, a file feed assigns to DD FEED, and the
program compiles. At run time the OPEN looks for a dataset named by whatever
the record happened to hold, and the job ends on file status 35 having processed
nothing.
Rename the file or the record so the two differ within the first eight characters once the hyphens are removed.
BANK-FILE-017 a file operation whose outcome was never handled
Every generated I/O statement is followed by a test of the file status, and a
status outside class 0 stops the step. That covers the failures and deliberately
does not cover the statuses a program is written to produce: end of file on a
read (10), no such record on a keyed read or a browse (23), a duplicate key
on a write to a KSDS (22). Those say the request found nothing rather than
that the file is broken.
When the program does not look, the record area still holds the record before
it. A read at end of file leaves the last transaction in place, and a program
that carries on posts it twice — with a return code of zero. It is OpenCBS
DF01.
The check is flow-sensitive. An operation that can end with one of those statuses leaves an outstanding outcome, and it is an error to
- use the record it filled,
- operate on the file again — a
closeoverwrites the status too, so a test written after one reads the close's answer, or - reach the end of the routine
with the outcome still outstanding.
read feedIn into line;
if feedInStatus == "00" {
post(line);
}
The comparison counts wherever it is written, including into a local, and a
loop whose condition reads the status discharges it — the drain loop the
language reference teaches stays legal. A log of the status does not:
printing the answer is not reading it.
Using it means every way a program can read the record, not only reading a field out of it in an expression. COBOL hands whole records to things by naming them, and each of those is the same defect:
read feedIn into line;
write trail from line; // the stale record, posted straight back out
release line; // …handed to a sort
putMessage feedQueue from line; // …put on a queue
call "BANKSUB" using line; // …passed to another program
json payload.body from line; // …published
A statement that fills the record is not a use: a second read into it, a
queue getMessage into it, a DL/I get. Those replace the stale bytes rather
than trusting them, which is the fix rather than the defect.
The walk reaches every block a statement runs, including the on page block of
a write and the bodies of a sort's input and output procedures. A transaction's
on failure handler and a file's on error handler are walked as routines of
their own: control arrives there from anywhere, so nothing the body owed is
known — but an operation the handler itself performs owes the same answer.
BANK-COPY-008 not a data description entry
A line in the copybook is not a level number followed by a name. Every entry in
a copybook is, so this is either a file that is not a copybook, or a construct
this reader does not have — a COPY of another member, a REPLACING phrase, or
a compiler directive other than the EJECT, SKIP and TITLE it skips.
It is refused rather than skipped. An entry that is passed over is a field missing from the record, and a missing field moves the offset of every field after it — which is a program reading somebody else's data at the right length in the wrong place. The message names the line.
BANK-COPY-009 copybook declares no 01-level record
A copybook describes a record, and a record begins at level 01. A file with entries but no 01 is a fragment: the subordinate half of a layout, or a copybook meant to be copied inside another record's group.
BANK-COPY-010 picture clause not understood
The layout reader cannot say how many bytes a field with this picture occupies, and every offset after it depends on that number. Guessing would describe a record that is wrong from that field onwards, which is worse than refusing because a wrong layout is one somebody acts on.
A picture the compiler emits is always understood, so this on generated output is a defect worth reporting.
BANK-COPY-011 no DECLARE TABLE block
bankc dclgen import reads what DCLGEN produces, and what makes that file a
DCLGEN output is the EXEC SQL DECLARE ... TABLE block: it names the table and
gives every column its Db2 type. Without it there is nothing to derive host
variables from.
BANK-COPY-012 not a column definition
Inside the DECLARE ... TABLE block each comma-separated part is a column name
followed by its SQL type. One of them is not, so the block has been edited by
hand or written by something other than DCLGEN.
11a. Security diagnostics
BANK-SEC-001 restricted data reclassified
A value marked sensitive is assigned to a field that is not. A field's marking
is part of its record declaration and therefore part of its copybook, so this
would reclassify the data silently and defeat the marking everywhere downstream.
12. Code generation diagnostics
These diagnostics protect the traceability claim: every BankTS symbol that reaches the backend must be locatable in the generated COBOL.
BANK-GEN-001 module missing source map entry
The generated source map has no entry for the compiled module.
BANK-GEN-002 record missing source map entry
A record reached the backend but has no source map entry.
BANK-GEN-003 field missing source map entry
A record field reached the backend but has no source map entry.
BANK-GEN-004 function missing source map entry
A function reached the backend but has no source map entry.
BANK-GEN-005 source map entry outside generated artifact
An entry targets a line range that does not exist in the generated COBOL, or an inverted range where the end line precedes the start line.
BANK-GEN-006 source map entry not anchored to generated name
An entry targets a line range that exists but does not contain the COBOL name the entry claims to describe. This catches entries that drift when the emitter changes its line layout.
BANK-GEN-007 transaction missing source map entry
A transaction reached the backend but has no source map entry. The generated COBOL has to expose the transaction boundary in the source map — see language-reference.md for what a transaction is, and verification.md for what the map is checked against.
12a. Test case diagnostics
A test declaration becomes a zUnit case rather than COBOL, and these are about
what such a case can actually observe. The driver runs in its own program, so
the program under test's WORKING-STORAGE is not reachable from it: a test that
appeared to assert on one would report a pass nobody checked.
BANK-TEST-001 a test naming something it cannot start
for names something that is not the entry transaction, or is not a transaction
at all. A zUnit case runs a load module, and a load module is entered at one
place.
BANK-TEST-002 a test on a program a batch case cannot start
A CICS transaction is started by a transaction identifier with a COMMAREA, which
is a type="CICS" case and a running region; an IMS program is entered by the
region with its PCBs. The generator writes type="BTCH" cases.
BANK-TEST-003 a given naming something the step is not started with
A batch program is entered with a PARM, which is the scalar parameters of its entry transaction. A record parameter is a buffer the program fills from a file.
BANK-TEST-004 a test value that is not a constant, or does not fit
The generated driver holds literals and evaluates nothing. A literal wider than
the field it is compared against is the same defect one step later: COBOL
truncates the MOVE that fills the interface, so the comparison would be
against a value the program could never have sent.
BANK-TEST-005 two tests with one name
Each test becomes a TEST_<NAME> entry point in one load module, and two of
them are one entry point.
BANK-TEST-006 a test name that will not survive being generated
The runner matches a test on the characters before the first space in an 80-character field, and the name also becomes part of a COBOL program-name.
BANK-TEST-007 a program asked for a case and declaring no tests
A configuration naming no test ends having done nothing, with a return code that reads as success.
12b. Job descriptor diagnostics
job.json describes a night: the programs, the sorts between them, and the
order. These are about that file, and about how the steps in it sit together.
BANK-JOB-001 job descriptor is incomplete
A job needs a name, a description and at least one entry in steps. The
first two both reach the JOB card, where the description is what an operator
watching the queue sees; a job with no steps is a stream that runs nothing.
BANK-JOB-002 step name is not a JCL name
A step name becomes the name field of an EXEC statement, which JCL limits to
one through eight alphanumeric or national characters beginning with a letter.
It is also what a restart and every COND refer to, so a name JCL will not take
is a job that cannot be restarted at a step.
BANK-JOB-003 two steps share a name
A COND and a restart both refer to a step by name. Two steps with one name
means neither can be named unambiguously, and a restart at that name is a night
rerun from a step nobody chose.
BANK-JOB-004 step is neither a program nor a sort
A step runs a BankLang project, named by project, or a sort, named by input,
output and fields.
BANK-JOB-005 two steps build the same load module
A load module member name is eight characters with the hyphens removed, and that
is all the binder and every EXEC PGM= see. Two programs in one job whose names
agree over those eight characters are one member: the second build overwrites
the first, and both steps run whichever was written last — a step that names one
program and executes another, with a return code that looks fine.
A program built on its own has nothing to collide with, so the job is where this appears.
BANK-JOB-006 sort step names a file no program declares
A sort step reads a file one program in the job wrote and writes one another program reads, and the job stream needs both datasets to give the step its SORTIN and SORTOUT. A name that belongs to no program's file declaration is a step with nothing to sort.
13. Severity levels
error compilation must stop
warning compilation may continue but risk is recorded
info useful explanation
audit included in audit report
14. Audit report integration
All warnings and errors should be available in machine-readable audit output.
Example:
{
"id": "BANK-DEC-002",
"severity": "error",
"source": "src/transfer.bank.ts",
"line": 42,
"message": "Implicit scale narrowing requires explicit rounding."
}