BankLang

Records

Records and their layout, inheritance, generics, variant records, bounded arrays, and copybooks brought in from outside.

Part of the BankTS language reference.

Records

Records map to COBOL group items and copybook structures.

record TransferRequest {
  debitAccount: string<16>;
  creditAccount: string<16>;
  amount: decimal<18, 2>;
  idempotencyKey: string<36>;
}

Rules:

Space nothing names

record CustomerParty {
  personName: string<30>;
  reserved 20;
}

FILLER PIC X(20). Every copybook on an estate has them — a field that was removed, a gap left for one that is coming, an area another program's copybook overlays — and a record language without a way to say so cannot describe the records it has to interoperate with. The importer used to refuse such a copybook outright, which is the right answer to "can this be laid out short?" and a useless answer to "can this copybook be imported?"

A reserved slot has no name and there is deliberately no way to give it one: nothing can read it, assign to it, or move a record through it. FILLER is not a name in COBOL either, and a slot a program could write to would be writing into space the layout says belongs to nobody.

The number is bytes, not digits. PIC S9(9) COMP-3 is nine digits and five bytes, and reserved 9 where the copybook had five would move every field after it four bytes along.

A field really can be called reserved; what settles it is the token after. A slot is followed by its byte count, a field by the : before its type.

5c. Variant records and variable-length tables

Two clauses a real copybook is built on:

record LegacyRecord {
  recordType: string<2>;
  personalName: string<40>;
  companyName: string<40> redefines personalName;

  lineCount: binary<4>;
  lines: Entry[100] depending on lineCount;
}

redefines is a second reading of storage another field already occupies — how a legacy layout says "this area means different things depending on the record type". The layout report shows it at the offset it shares.

It must name the field immediately before it, or a redefinition of that same area written since; COBOL requires the readings of an area to follow its description with nothing in between that takes storage of its own. It cannot redefine a table, and it cannot carry a depending on (BANK-COPY-004).

It may be longer than what it redefines. COBOL then extends the storage area, so the record runs to the end of the longest reading of it and every field after it moves by the overhang — a: string<6> redefined by national<4> is the Language Reference's own example, and leaves the next field at byte 8.

depending on names the field holding how much of a table this record uses, which is what makes a variable-length record variable. The fixed bound stays as the maximum — the storage still has to be reserved — so the emitted clause is OCCURS 1 TO 100 TIMES DEPENDING ON LINE-COUNT OF BATCH. The count must be a whole number declared before the table, because COBOL reads it to decide the record's length and cannot read a field it has not reached.

The count is qualified by its group because the same record is laid out in working storage and again inside every FD that holds it, so the bare name exists twice and both compilers reject it as ambiguous. The Language Reference allows the qualification: "All data-names used in the OCCURS clause can be qualified; they cannot be subscripted or indexed."

The count is checked before anything uses it. The Language Reference says "the behavior is undefined if the value of the object is outside of the range", and the object decides how long the record containing the table is — a count of 500 on a table declared to hold 100 describes a record longer than the storage allocated for it, so every group move, write, or call using that record runs off the end. The check matters most where the value is least controlled: the count usually arrives in the record read from the file, so it holds whatever was in the dataset. Out of range, the count is named in the job log and the step fails with a return code of 12. A read also copies only the occurrences the count says are there, rather than the declared maximum, which would read past the data the read delivered.

The varying table has to be the last field 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, and no copybook can give it an offset. IBM calls this complex ODO and permits it; this compiler is stricter and says why (BANK-COPY-004). The layout report would otherwise state the offset the field has when the table is full, which is an offset no other record has — and a copybook that names a byte position nothing is at is worse than no copybook at all. GnuCOBOL refuses the shape outright, so such a program could not be executed locally either.

5a. Inheritance

A record may extend another. The base fields are laid out first, so the derived record's leading storage is the base record's storage byte for byte:

record CurrentAccount {
  accountId: string<16>;
  balance: currency<"BDT", 18, 2>;
}

record SavingsAccount extends CurrentAccount {
  minimumBalance: currency<"BDT", 18, 2>;
}

That layout guarantee is the point: an existing copybook for the base still reads a derived record correctly.

It is also what makes a derived record substitutable for its base at a call site. A function's record parameter is a LINKAGE cell rather than a group item in working storage, and the caller points the cell at the argument before performing the paragraph:

           SET ADDRESS OF LEDGER-BALANCE-OF-P1 TO ADDRESS OF SAVINGS-ACCOUNT
           PERFORM LEDGER-BALANCE-OF

The cell describes the declared parameter's fields, and extends guarantees those fields sit at the same offsets in the derived record, so the callee reads the caller's storage correctly. An argument whose address the caller cannot take without evaluating a subscript first is BANK-TYPE-021.

A transaction is a program entry point rather than something called with varying arguments, so its record parameters stay in working storage and take no part in this.

Redeclaring an inherited field is BANK-TYPE-017; a cycle is BANK-TYPE-016.

5b. Generics

Records and functions may take type parameters. There is no runtime polymorphism in COBOL and no boxing, so every instantiation is expanded into a concrete declaration at compile time:

record Slot<T> {
  value: T;
  present: bool;
}

record Holder {
  money: Slot<currency<"BDT", 18, 2>>;
  count: Slot<decimal<9, 0>>;
}

That emits two records, SLOT-CUR-BDT18-2 and SLOT-DEC9-0, each with its own storage. Two instantiations of one generic cost two copies of the layout and, for a function, two copies of the code. Type arguments are normalised through the resolver first, so Slot<Money> and Slot<currency<"BDT", 18, 2>> are one record rather than two identical ones.

Generic functions take their type arguments by inference from the argument types, never explicitly:

function larger<T>(left: T, right: T): T {
  if left >= right {
    return left;
  } else {
    return right;
  }
}

larger(a, b) instantiates from the types of a and b. There is no larger<Money>(a, b) syntax, because f<T>(x) cannot be told apart from two comparisons without unbounded lookahead. A type parameter that appears in no parameter type therefore cannot be inferred, and is BANK-TYPE-020.

A generic that is never instantiated generates nothing and is never checked against real types, which is reported as BANK-TYPE-015.

Two instantiations of a generic function that lower to identical COBOL share one paragraph. larger<currency<"BDT", 18, 2>> and larger<currency<"USD", 18, 2>> both emit PIC S9(16)V99 COMP-3, so emitting both is the same paragraph and the same storage twice. Sharing follows the callee: two instantiations that differ only in which instantiation they call merge once those callees have merged. The surviving name is the alphabetically first of the group, so the choice does not depend on the order the instantiations were created in.

This is a decision about emitted COBOL and nothing else. Currency stays nominally typed, so larger(bdtAmount, usdAmount) is still BANK-TYPE-020. A function you wrote yourself keeps its own paragraph even if another one happens to match it exactly, because that name appears in the source, the source map, and the audit record.

Instantiated records are never shared: two instantiations are two variables holding different values, not two copies of one routine.

Bounded arrays

record Statement {
  entries: LedgerEntry[1000];
}

Arrays must be statically bounded unless mapped to a known COBOL OCCURS DEPENDING ON construct.

Tables of tables

record RateBook {
  rates: decimal<9, 4>[3][4];
}

Nested OCCURS, three rows of four. The bounds read outermost-first, so the 3 becomes the outer table and the 4 the inner one — a rate matrix by term and band, which is how a bank holds one.

book.rates[1][1] = 0.05;

COBOL subscripts the innermost data name with every dimension at once, so that becomes RATES-ITEM OF BOOK (1, 1) rather than RATES (1) (1). The inner dimension is named for you, since nothing in the source names it and COBOL needs something to subscript.

Each dimension gets its own INDEXED BY.

Copybooks in the program

bankc emits a .cpy file for every record. Whether the generated program writes those layouts out or copies them in is a project setting:

{ "copybookMode": "inline" }
Mode Generated program For
inline 01 TRANSFER-REQUEST. + fields a self-contained artifact, reviewable alone
copy COPY TRANSFER. a shop with a shared copybook library

inline is the default: it is what the playground shows and what a reviewer reads on its own.

copy is the shape a real shop expects, where the copybook is the contract between programs rather than a document that can drift from them. The generated job then carries a SYSLIB for the copybook library — without it the copy statements resolve to nothing and every data name is undefined — and local GnuCOBOL validation puts the copybook directory on the compiler's search path, so the mode that ships is the mode that is checked.

The record stays traceable in copy mode: the source map carries one entry for the record and none for its fields, because the fields are in the copybook and have a layout report of their own.

A copybook is named for its member, not for its record. 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 compiler looks at: "only the first eight characters of text-name are used as the identifying name" when it searches a PDS or PDSE. So TransferRequest is the member TRANSFER, and the COPY names that. COPY TRANSFER-REQUEST would have the compiler look for a member called TRANSFER-, which no library can hold.

The consequence is that two records agreeing within those eight characters cannot share a copybook library: one overwrites the other, and every program that copies either gets a record with the name it asked for and different fields at different offsets. BANK-COPY-007 reports that within a program, and pnpm zos:kit refuses to build a bundle whose members would overwrite each other rather than shipping one under the other's name.


Read this page as Markdown on GitHub →