COBOL anti-pattern
OCCURS DEPENDING ON
A variable-length table whose current size is held in another field, so record length, field offsets and loop bounds all change at runtime.
- FAMILY
- COBOL
- CUTOVER RISK
- High
- Hides business logic or coupling that a rewrite will drop unless it is extracted explicitly.
- MODERN MAPPING
- Bounded array (child collection)
- GENERATED ARTIFACT
- openapi.components.yaml
Before and after
legacy source → modern mapping
Legacy: exhibits the pattern
1 01 INVOICE-REC.2 05 INV-NUMBER PIC X(10).3 05 INV-LINE-COUNT PIC 9(03) COMP.4 05 INV-LINES OCCURS 1 TO 50 TIMES5 DEPENDING ON INV-LINE-COUNT6 INDEXED BY LX.7 10 LN-SKU PIC X(12).8 10 LN-QTY PIC S9(5) COMP-3.9 10 LN-PRICE PIC S9(7)V99 COMP-3.10 ...11 PERFORM VARYING LX FROM 1 BY 112 UNTIL LX > INV-LINE-COUNT13 COMPUTE WS-EXT = LN-QTY (LX) * LN-PRICE (LX)14 ADD WS-EXT TO WS-INV-TOTAL15 END-PERFORM.
Modern: Bounded array (child collection)
1// INVOICE-REC: INV-LINES OCCURS 1 TO 502type Invoice struct {3 Number string4 Lines []Line // len(Lines) replaces INV-LINE-COUNT5}67type Line struct {8 SKU string9 Qty decimal.Decimal // S9(5) COMP-310 Price decimal.Decimal // S9(7)V99 COMP-311}1213func (inv Invoice) Total() decimal.Decimal {14 total := decimal.Zero15 for _, ln := range inv.Lines {16 total = total.Add(ln.Qty.Mul(ln.Price))17 }18 return total19}
Why it is dangerous in modernization
OCCURS DEPENDING ON (ODO) makes a table's occupied length depend on a count field, typically earlier in the same record. Group MOVEs, READ and WRITE of variable-length records, and any field placed after the table all use the count as it stands at that moment. IBM Enterprise COBOL also accepts complex ODO, where items follow the variable table in the same group and therefore change offset whenever the count changes. Converters that size the table at its maximum produce records that are too long and misaligned; converters that ignore the count export garbage entries past the occupied ones. Programs that update the count after moving data into the table, or that loop to the maximum instead of the count, contain real defects that a new service will either fix silently or copy faithfully, and someone has to decide which.
How PYD ENTERPRISES detects it
The storage-layout pass models each ODO table as a symbolic length: fixed prefix plus count times entry size. Every field that follows the table gets a symbolic offset in the same terms, which identifies complex ODO immediately. Dataflow analysis then finds every definition of the count field and every subscripted reference to the table, and checks that subscripts are bounded by the count rather than the declared maximum on all paths. Writes to the count that occur after writes to the table, and loops bounded by a literal maximum, are reported as findings with the paths that reach them.
Maps to: Bounded array (child collection)
The table becomes an array with minItems and maxItems taken from the OCCURS clause, and the count field disappears from the contract because the array length carries it. In a relational target it becomes a child table keyed by the parent and a sequence number. Fields that followed a complex ODO become ordinary properties of the parent, with their variable offsets handled once, in the converter.
Generated specification fragment
1components:2 schemas:3 Invoice:4 type: object5 required: [number, lines]6 properties:7 number:8 type: string9 maxLength: 1010 lines:11 type: array12 minItems: 113 maxItems: 5014 items:15 $ref: '#/components/schemas/InvoiceLine'16 x-pyd-source: INVOICE.cpy:1-9
Every generated element links back to the source file and line range shown in the legacy pane. Review and sign off before building against it.
Record length is a function, not a constant
Each entry in the example is 12 bytes of SKU, 3 bytes of packed quantity (S9(5) is 5 digits plus sign, so 3 bytes) and 5 bytes of packed price (S9(7)V99 is 9 digits plus sign, so 5 bytes): 20 bytes per line. The fixed prefix is 10 bytes of invoice number and 2 bytes of binary count, because a PIC 9(03) COMP field is stored in a halfword. The record is therefore 12 + 20n bytes, between 32 and 1,012 bytes depending on INV-LINE-COUNT.
File transfer and CDC tools must read the count before they know where the record ends. PYD ENTERPRISES exports that length expression with the schema, which is the piece of information most often missing when a data engineering team starts on a mainframe extract.
Complex ODO and moving offsets
If the copybook declared 05 INV-TERMS PIC X(04) after INV-LINES, IBM Enterprise COBOL would place it immediately after the last occupied entry. Its offset is 12 + 20n, and it moves whenever the count changes. Programs that change the count while INV-TERMS holds data effectively shift or overwrite it. The engine flags every complex ODO, and every write to the count that is not followed by a rebuild of the trailing fields.
Defects worth keeping or fixing
- Loops bounded by 50 instead of the count read stale entries from a previous record that still sit in storage.
- A count of zero violates
OCCURS 1 TO 50, yet many files contain such records; the legacy code may skip them or abend.
- A count above 50 overruns the table unless SSRANGE was on at compile time.
Each finding comes with an equivalence fixture so the team can see what the legacy program does with the bad record before deciding whether the new service should reject it.
Related patterns
Usually found in the same codebase.
Find every OCCURS DEPENDING ON in your codebase.
Run PYD ENTERPRISES in your private VPC and get each occurrence with file, line range, modern mapping and equivalence fixtures, ready for your architects to review.