COBOL anti-pattern
PERFORM THRU fall-through
PERFORM A THRU B runs every paragraph that physically sits between A and B, and GO TO the exit paragraph is the early return, so the unit of behavior is a source range, not a named routine.
- FAMILY
- COBOL
- CUTOVER RISK
- High
- Hides business logic or coupling that a rewrite will drop unless it is extracted explicitly.
- MODERN MAPPING
- Single-entry function with guard clauses
- GENERATED ARTIFACT
- CLMEDIT.range-3000.json
Before and after
legacy source → modern mapping
Legacy: exhibits the pattern
1 PERFORM 3000-EDIT-CLAIM THRU 3000-EXIT.2 IF WS-CLAIM-OK3 PERFORM 4000-ADJUDICATE4 END-IF.5 ...6 3000-EDIT-CLAIM.7 SET WS-CLAIM-OK TO TRUE8 IF CLM-SERVICE-DATE > WS-RUN-DATE9 SET WS-CLAIM-REJECT TO TRUE10 MOVE 'E101' TO WS-REASON11 GO TO 3000-EXIT12 END-IF.13 3010-CHECK-ELIGIBILITY.14 PERFORM 3100-READ-MEMBER15 IF MBR-TERM-DATE < CLM-SERVICE-DATE16 SET WS-CLAIM-REJECT TO TRUE17 MOVE 'E205' TO WS-REASON18 GO TO 3000-EXIT19 END-IF.20 3020-APPLY-COPAY.21 COMPUTE CLM-MEMBER-RESP = CLM-ALLOWED * MBR-COINS22 ADD MBR-COPAY TO CLM-MEMBER-RESP.23 3000-EXIT.24 EXIT.
Modern: Single-entry function with guard clauses
1public EditResult editClaim(Claim c, Member m,2 LocalDate runDate) {3 // 3000-EDIT-CLAIM4 if (c.serviceDate().isAfter(runDate)) {5 return EditResult.reject("E101");6 }7 // 3010-CHECK-ELIGIBILITY (fall-through)8 if (m.termDate().isBefore(c.serviceDate())) {9 return EditResult.reject("E205");10 }11 // 3020-APPLY-COPAY (fall-through)12 BigDecimal resp = c.allowed()13 .multiply(m.coinsurance())14 .add(m.copay());15 return EditResult.ok(resp);16}
Why it is dangerous in modernization
A PERFORM THRU range is defined by position in the source file. Anything placed between the first and last paragraph executes as part of the range, whether or not its name suggests it belongs there. Maintenance programmers have inserted paragraphs into the middle of ranges for decades, so a range named for validation can also post a fee. Inside the range, GO TO the exit paragraph acts as an early return, and a GO TO that leaves the range abandons the return entirely: control never comes back to the statement after the PERFORM. When ranges overlap or nest incorrectly, behavior depends on which PERFORM is active at the moment control reaches a range end. A rewrite that turns each paragraph name into a method, and each PERFORM into a call, gets all of these cases wrong.
How PYD ENTERPRISES detects it
The engine builds the control-flow graph at paragraph granularity, then computes each PERFORM range as the set of paragraphs between its start and end in physical order. It records every GO TO whose target is the range exit (an early return), every GO TO whose target is outside the range (a non-returning exit), and every pair of ranges that share paragraphs. Each range is then sliced into a single-entry, single-exit region. Ranges that cannot be reduced that way, because of an outbound GO TO or an overlapping range, are flagged for review with the exact paths that break the structure.
Maps to: Single-entry function with guard clauses
Each well-formed range becomes one function whose body is the paragraphs in order, with every GO TO to the exit paragraph turned into an early return. Paragraphs that are reached only by fall-through become private steps of that function, not public methods. Non-returning exits become explicit result values or exceptions so the caller decides what happens next.
Generated specification fragment
1{2 "range": "3000-EDIT-CLAIM THRU 3000-EXIT",3 "paragraphs": [4 "3000-EDIT-CLAIM",5 "3010-CHECK-ELIGIBILITY",6 "3020-APPLY-COPAY",7 "3000-EXIT"8 ],9 "earlyReturns": [10 { "line": 11, "reason": "E101 future service date" },11 { "line": 18, "reason": "E205 member terminated" }12 ],13 "outboundGoTo": [],14 "overlapsWith": [],15 "classification": "single-entry single-exit",16 "rules": ["R-E101", "R-E205", "R-MEMBER-RESP"]17}
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.
Why the paragraph names lie
In the example, the range is named after claim edits, but its last real paragraph computes member responsibility. A reader who scans the PERFORM statement and the names 3000-EDIT-CLAIM and 3000-EXIT will not expect a calculation, and a team that splits edits and pricing into separate services will move the copay rule into the wrong one, or lose it. This is why PYD ENTERPRISES extracts rules per range and per path, not per paragraph name: the copay computation is reached only on the path where both edits pass, and that guard is part of the rule.
The cases that do not reduce cleanly
- Outbound GO TO: a GO TO from inside the range to a paragraph outside it. The PERFORM never returns on that path; in batch code this is often a jump to an abend or end-of-job paragraph.
- Overlapping ranges: two PERFORM THRU statements whose ranges share paragraphs. Which return point fires depends on which PERFORM is currently active.
- Fall-out of the range end: when control reaches the last paragraph of a range that is not currently being performed, execution simply continues with the next paragraph in the file.
Each of these is reported as a finding with the paths that trigger it, so the reviewer can decide whether it is intended behavior or a latent defect that the new service should fix on purpose rather than by accident.
Tests that protect the conversion
For each range PYD ENTERPRISES generates one fixture per exit: each early return, the normal fall-through to the exit paragraph, and any outbound GO TO. In the example that is three fixtures: a future service date, a terminated member, and a clean claim with a computed member responsibility. The new function passes when it returns the same reason codes and amounts for all three.
Related patterns
Usually found in the same codebase.
Find every PERFORM THRU fall-through 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.