If there is one statement that defines COBOL programming, it is PERFORM. No other COBOL verb is as versatile, as frequently used, or as essential to understanding how COBOL programs work. In most modern languages, you have separate constructs for...
In This Chapter
- Introduction
- 8.1 The Basic PERFORM Statement
- 8.2 PERFORM THRU: Executing a Range of Paragraphs
- 8.3 Out-of-Line vs. Inline PERFORM
- 8.4 PERFORM n TIMES: Fixed Iteration
- 8.5 PERFORM UNTIL: Conditional Iteration
- 8.6 PERFORM VARYING: Counting Loops
- 8.7 Inline PERFORM with END-PERFORM
- 8.8 Nested PERFORM: Paragraphs Calling Other Paragraphs
- 8.9 The GO TO Controversy: Structured Programming Without GO TO
- 8.10 PERFORM and Structured Programming: Top-Down Design
- 8.11 EXIT PARAGRAPH and EXIT SECTION (COBOL 2002+)
- 8.12 EXIT PERFORM and EXIT PERFORM CYCLE (COBOL 2002+)
- 8.13 Performance Considerations
- 8.14 The PERFORM Stack: How COBOL Tracks Return Addresses
- 8.15 Common PERFORM Patterns
- 8.16 Common Mistakes and Pitfalls
- 8.17 Quick Reference: All PERFORM Forms
- 8.18 Free-Format Examples (COBOL 2002+)
- 8.19 Chapter Summary
Chapter 8: Iteration -- The PERFORM Statement in All Its Forms
Introduction
If there is one statement that defines COBOL programming, it is PERFORM. No other COBOL verb is as versatile, as frequently used, or as essential to understanding how COBOL programs work. In most modern languages, you have separate constructs for calling functions, running for loops, executing while loops, and implementing do-while loops. In COBOL, the PERFORM statement does all of these things -- and more.
The PERFORM statement is the cornerstone of structured COBOL programming. It serves simultaneously as:
- A subroutine call mechanism (like calling a function or method)
- A fixed-count loop (like
for i = 1 to n) - A conditional loop (like
whileanddo-while) - A counting loop (like
for (i = start; i < end; i += step)) - A nested loop construct (via the
AFTERclause)
Understanding PERFORM in all its forms is not optional -- it is the single most important skill for any COBOL programmer. Every COBOL program of any significance uses PERFORM extensively, and the quality of a COBOL program can often be judged by how well its PERFORM structure is organized.
In this chapter, we will work through every form of the PERFORM statement, from the simplest paragraph invocation to complex multi-level VARYING loops with AFTER clauses. We will also explore the structured programming paradigm that PERFORM enables and examine common patterns that have been used in COBOL shops for decades.
8.1 The Basic PERFORM Statement
Executing a Paragraph
The simplest form of PERFORM transfers control to a named paragraph, executes all statements in that paragraph, and then returns control to the statement immediately following the PERFORM. This is analogous to calling a function or subroutine in other languages, but with an important difference: COBOL paragraphs do not have formal parameters or return values. They communicate through shared WORKING-STORAGE variables.
Fixed-format syntax:
PROCEDURE DIVISION.
MAIN-PROGRAM.
DISPLAY "Before PERFORM"
PERFORM CALCULATE-TAX
DISPLAY "After PERFORM"
STOP RUN.
CALCULATE-TAX.
MULTIPLY WS-GROSS-PAY BY WS-TAX-RATE
GIVING WS-TAX-AMOUNT
SUBTRACT WS-TAX-AMOUNT FROM WS-GROSS-PAY
GIVING WS-NET-PAY.
Free-format syntax (COBOL 2002+):
procedure division.
main-program.
display "Before PERFORM"
perform calculate-tax
display "After PERFORM"
stop run.
calculate-tax.
multiply ws-gross-pay by ws-tax-rate
giving ws-tax-amount
subtract ws-tax-amount from ws-gross-pay
giving ws-net-pay.
When the PERFORM CALCULATE-TAX statement executes, the following happens:
- The current execution position is saved (pushed onto the PERFORM stack).
- Control transfers to the first statement of
CALCULATE-TAX. - All statements in
CALCULATE-TAXare executed. - When the paragraph ends (either at the next paragraph name or at a period), control returns to the statement after the
PERFORM.
This is fundamentally different from GO TO, which transfers control without saving a return point. PERFORM always comes back.
Executing a Section
You can also PERFORM an entire section. A section contains one or more paragraphs, and performing a section executes all paragraphs within it:
PROCEDURE DIVISION.
MAIN-PROCESS SECTION.
MAIN-PARAGRAPH.
PERFORM VALIDATION-SECTION
STOP RUN.
VALIDATION-SECTION SECTION.
VALIDATE-ACCOUNT.
IF WS-ACCOUNT-NUM = ZEROS
DISPLAY "Invalid account"
END-IF.
VALIDATE-BALANCE.
IF WS-BALANCE < ZEROS
DISPLAY "Negative balance"
END-IF.
When PERFORM VALIDATION-SECTION executes, both VALIDATE-ACCOUNT and VALIDATE-BALANCE are executed before control returns.
See:
code/example-01-basic-perform.cobfor complete working examples of basic PERFORM with multiple paragraphs.
8.2 PERFORM THRU: Executing a Range of Paragraphs
The PERFORM ... THRU ... (or THROUGH, which is identical) form executes all paragraphs from the first named paragraph through the last named paragraph:
PERFORM VALIDATE-START THRU VALIDATE-END
This executes VALIDATE-START, then every paragraph that physically follows it in the source code, until VALIDATE-END is completed. The paragraphs are executed in their source-code order.
The EXIT Paragraph Convention
A common coding convention pairs PERFORM THRU with an EXIT paragraph:
PERFORM PROCESS-RECORD THRU PROCESS-RECORD-EXIT.
PROCESS-RECORD.
IF WS-RECORD-TYPE = "H"
DISPLAY "Header record"
END-IF
IF WS-RECORD-TYPE = "D"
PERFORM PROCESS-DETAIL
END-IF.
PROCESS-RECORD-EXIT.
EXIT.
The EXIT statement does nothing -- it is a no-operation placeholder that serves as a clean endpoint for the THRU range. This convention was especially important in COBOL-74 and COBOL-85, where EXIT was the only way to provide a clear range boundary.
Why PERFORM THRU Is Controversial
PERFORM THRU is one of the most debated features in COBOL. Many modern coding standards prohibit or strongly discourage its use. Here is why:
The insertion problem: If a programmer adds a new paragraph between the THRU range endpoints, that paragraph becomes part of the THRU range -- even if that was not intended. This is a maintenance hazard:
* Original code - THRU range is A through C
PERFORM PARA-A THRU PARA-C.
PARA-A.
DISPLAY "A".
PARA-B.
DISPLAY "B".
PARA-C.
EXIT.
* A maintenance programmer adds PARA-NEW between B and C:
PARA-A.
DISPLAY "A".
PARA-B.
DISPLAY "B".
PARA-NEW.
DISPLAY "New" . *> Now unexpectedly executed!
PARA-C.
EXIT.
The coupling problem: PERFORM THRU creates an implicit dependency on the physical ordering of paragraphs in the source file. Rearranging paragraphs can silently break the program.
Industry guidance: The IBM Enterprise COBOL Programming Guide recommends avoiding PERFORM THRU in new code. Most modern COBOL style guides agree. However, you will encounter it extensively in legacy code, so you must understand it even if you never write it yourself.
See:
code/example-01-basic-perform.cobfor a PERFORM THRU demonstration with the EXIT paragraph convention.
8.3 Out-of-Line vs. Inline PERFORM
Prior to the COBOL-85 standard, all PERFORM statements were "out-of-line" -- they named a paragraph or section to execute. COBOL-85 introduced the inline PERFORM, where the code to be executed is placed directly between PERFORM and END-PERFORM.
Out-of-Line PERFORM (All COBOL Versions)
PERFORM CALCULATE-TAX.
CALCULATE-TAX.
COMPUTE WS-TAX = WS-INCOME * WS-TAX-RATE.
The code to execute is in a separate paragraph. The reader must scroll or navigate to find the paragraph body.
Inline PERFORM (COBOL-85 and Later)
PERFORM
COMPUTE WS-TAX = WS-INCOME * WS-TAX-RATE
END-PERFORM
The code to execute is right there, between PERFORM and END-PERFORM. No paragraph is needed. The code reads top-to-bottom without jumping around.
When to Use Each Form
| Consideration | Out-of-Line | Inline |
|---|---|---|
| Code is reused from multiple places | Preferred | Not possible |
| Code is short (1-5 lines) | Either | Preferred |
| Code is long (20+ lines) | Preferred | Gets unwieldy |
| Readability of calling code | Paragraph name documents intent | Logic is immediately visible |
| Debugging | Easy to set breakpoint on paragraph | Must set breakpoint on specific line |
| Testing | Paragraph can be tested independently | Cannot be tested in isolation |
General rule of thumb: Use inline PERFORM for short, single-use loops. Use out-of-line PERFORM for longer logic blocks and for code that will be called from multiple places.
See:
code/example-05-inline-perform.cobfor side-by-side comparisons of inline and out-of-line forms.
8.4 PERFORM n TIMES: Fixed Iteration
The PERFORM ... TIMES form executes a paragraph or inline block a fixed number of times:
* Out-of-line: execute paragraph 5 times
PERFORM PRINT-ASTERISKS 5 TIMES
* Inline: execute block n times (n is a variable)
PERFORM WS-NUM-ITERATIONS TIMES
ADD 1 TO WS-COUNTER
DISPLAY "Iteration: " WS-COUNTER
END-PERFORM
Key Points About PERFORM TIMES
- No automatic counter. Unlike
forloops in most languages,PERFORM TIMESdoes not provide a loop counter variable. If you need to know which iteration you are on, you must maintain your own counter:
MOVE ZEROS TO WS-COUNTER
PERFORM 10 TIMES
ADD 1 TO WS-COUNTER
DISPLAY "Iteration " WS-COUNTER " of 10"
END-PERFORM
- The count is evaluated once. If you use a variable for the count, its value is captured at the start of the loop. Changing the variable inside the loop does not affect the number of iterations:
MOVE 5 TO WS-LIMIT
PERFORM WS-LIMIT TIMES
DISPLAY "Hello"
MOVE 100 TO WS-LIMIT *> Does NOT cause 100 iterations
END-PERFORM
-
Zero or negative counts. If the TIMES value is zero or negative, the paragraph is never executed. The test is performed before the first iteration.
-
Practical uses. PERFORM TIMES is ideal when you know exactly how many iterations you need: printing a fixed number of blank lines, processing a fixed-size array, repeating an operation a specified number of times.
See:
code/example-02-perform-times.cobfor demonstrations including compound interest calculation, multiplication tables, and pattern generation.
8.5 PERFORM UNTIL: Conditional Iteration
The PERFORM UNTIL form repeats execution until a specified condition becomes true. This is COBOL's general-purpose conditional loop.
Critical distinction: The UNTIL condition specifies when to stop, not when to continue. This is the opposite of C's while loop. In C, the loop continues while the condition is true. In COBOL, the loop continues until the condition becomes true.
* C equivalent: while (counter <= 10)
* COBOL:
PERFORM UNTIL WS-COUNTER > 10
ADD 1 TO WS-COUNTER
END-PERFORM
WITH TEST BEFORE (Default)
WITH TEST BEFORE tests the condition before each iteration. If the condition is true initially, the loop body never executes. This is equivalent to a while loop:
* Explicit TEST BEFORE (same as default)
PERFORM WITH TEST BEFORE
UNTIL WS-COUNTER > 10
ADD 1 TO WS-COUNTER
END-PERFORM
* Implicit TEST BEFORE (identical behavior)
PERFORM UNTIL WS-COUNTER > 10
ADD 1 TO WS-COUNTER
END-PERFORM
WITH TEST AFTER
WITH TEST AFTER tests the condition after each iteration. The loop body always executes at least once, even if the condition is already true. This is equivalent to a do-while loop:
* Always executes at least once
PERFORM WITH TEST AFTER
UNTIL WS-COUNTER > 10
DISPLAY "Counter: " WS-COUNTER
ADD 1 TO WS-COUNTER
END-PERFORM
This is particularly useful for: - Menu loops: The menu should display at least once before checking if the user wants to quit. - Input validation: You must read input at least once before you can validate it. - Convergence algorithms: You must compute at least one iteration before checking for convergence.
Using Condition Names (88-Level Items)
The idiomatic COBOL way to control loops uses condition names (88-level items) rather than relational conditions. This makes the code more readable:
01 WS-EOF-FLAG PIC X(1) VALUE 'N'.
88 END-OF-FILE VALUE 'Y'.
88 NOT-END-OF-FILE VALUE 'N'.
PROCEDURE DIVISION.
MAIN-PROCESS.
SET NOT-END-OF-FILE TO TRUE
PERFORM READ-RECORD
PERFORM UNTIL END-OF-FILE
PERFORM PROCESS-RECORD
PERFORM READ-RECORD
END-PERFORM.
This PERFORM UNTIL END-OF-FILE reads almost like English. It is clearer than PERFORM UNTIL WS-EOF-FLAG = 'Y'. The SET statement combined with 88-level items is the standard COBOL approach for flag management.
Compound Conditions
You can use compound conditions with AND and OR:
PERFORM UNTIL END-OF-FILE
OR WS-ERROR-COUNT > 100
PERFORM PROCESS-RECORD
PERFORM READ-RECORD
END-PERFORM
See:
code/example-03-perform-until.cobfor comprehensive examples including menu loops, sequential search, convergence algorithms, and the standard file-processing idiom.
8.6 PERFORM VARYING: Counting Loops
The PERFORM VARYING statement is COBOL's counting loop. It automatically manages a loop variable, incrementing (or decrementing) it on each iteration:
PERFORM VARYING WS-INDEX FROM 1 BY 1
UNTIL WS-INDEX > 100
DISPLAY WS-INDEX
END-PERFORM
The FROM / BY / UNTIL Syntax
The full syntax is:
PERFORM [WITH TEST {BEFORE|AFTER}]
VARYING identifier-1 FROM {value-1|identifier-2}
BY {value-2|identifier-3}
UNTIL condition-1
[AFTER identifier-4 FROM {value-3|identifier-5}
BY {value-4|identifier-6}
UNTIL condition-2]
...
The execution sequence for PERFORM VARYING with TEST BEFORE (default) is:
- Set identifier-1 to the FROM value.
- Evaluate the UNTIL condition. If true, exit the loop.
- Execute the loop body.
- Add the BY value to identifier-1.
- Go to step 2.
Important: After the loop ends, the VARYING variable contains a value one step past the termination condition:
PERFORM VARYING WS-I FROM 1 BY 1
UNTIL WS-I > 5
DISPLAY WS-I *> Displays 1, 2, 3, 4, 5
END-PERFORM
DISPLAY WS-I *> Displays 6 (one past the limit)
Counting Down with Negative BY Values
You can count downward by using a negative BY value:
PERFORM VARYING WS-COUNTDOWN FROM 10 BY -1
UNTIL WS-COUNTDOWN < 1
DISPLAY WS-COUNTDOWN "..."
END-PERFORM
DISPLAY "LIFTOFF!"
Fractional BY Values
The BY value can be fractional:
01 WS-ANGLE PIC 9(3)V99 VALUE ZEROS.
PERFORM VARYING WS-ANGLE FROM 0.00 BY 0.25
UNTIL WS-ANGLE > 3.14
DISPLAY "Angle: " WS-ANGLE
END-PERFORM
The AFTER Clause: Nested Loops
The AFTER clause creates nested loops within a single PERFORM statement. The VARYING variable is the outer loop, and each AFTER variable is an inner loop:
* Two-dimensional iteration (like nested for loops)
PERFORM VARYING WS-ROW FROM 1 BY 1
UNTIL WS-ROW > 4
AFTER WS-COL FROM 1 BY 1
UNTIL WS-COL > 5
MOVE WS-ROW TO WS-MATRIX(WS-ROW, WS-COL)
END-PERFORM
This is equivalent to:
for (row = 1; row <= 4; row++)
for (col = 1; col <= 5; col++)
matrix[row][col] = row;
You can have multiple AFTER clauses for three or more levels of nesting:
* Three-dimensional iteration
PERFORM VARYING WS-REGION FROM 1 BY 1
UNTIL WS-REGION > 3
AFTER WS-QUARTER FROM 1 BY 1
UNTIL WS-QUARTER > 4
AFTER WS-MONTH FROM 1 BY 1
UNTIL WS-MONTH > 3
ADD WS-SALES(WS-REGION, WS-QUARTER, WS-MONTH)
TO WS-TOTAL
END-PERFORM
Execution order of AFTER clauses: The rightmost (innermost) AFTER variable changes fastest. When it exhausts its range, the next AFTER variable steps, and the innermost resets to its FROM value. This continues up through the VARYING variable.
Using Indexes vs. Subscripts
When working with tables that have INDEXED BY clauses, you use the SET statement and index names instead of subscripts:
01 WS-PRODUCT-TABLE.
05 WS-PRODUCT OCCURS 100 TIMES
INDEXED BY WS-PROD-IDX.
10 WS-PROD-NAME PIC X(30).
10 WS-PROD-PRICE PIC 9(5)V99.
PERFORM VARYING WS-PROD-IDX FROM 1 BY 1
UNTIL WS-PROD-IDX > 100
DISPLAY WS-PROD-NAME(WS-PROD-IDX)
END-PERFORM
Indexes are generally more efficient than subscripts because the compiler can often optimize index arithmetic into direct displacement calculations, avoiding the multiplication that subscript access requires at runtime.
See:
code/example-04-perform-varying.cobfor examples including temperature conversion tables, factorials, matrix operations, 3D array processing, and indexed table access.
8.7 Inline PERFORM with END-PERFORM
Every form of PERFORM can be used inline with END-PERFORM:
* Inline basic (rarely useful but legal)
PERFORM
COMPUTE WS-X = WS-Y + WS-Z
END-PERFORM
* Inline TIMES
PERFORM 5 TIMES
DISPLAY "Hello"
END-PERFORM
* Inline UNTIL
PERFORM UNTIL WS-COUNT > 10
ADD 1 TO WS-COUNT
END-PERFORM
* Inline VARYING
PERFORM VARYING WS-I FROM 1 BY 1
UNTIL WS-I > 100
DISPLAY WS-I
END-PERFORM
END-PERFORM is a scope terminator introduced in COBOL-85. It explicitly marks the end of the inline PERFORM block. Without END-PERFORM, you cannot use inline PERFORM -- you must name a paragraph.
Nesting Inline PERFORMs
Inline PERFORMs can be nested within each other:
PERFORM VARYING WS-ROW FROM 1 BY 1
UNTIL WS-ROW > 10
PERFORM VARYING WS-COL FROM 1 BY 1
UNTIL WS-COL > 10
COMPUTE WS-CELL(WS-ROW, WS-COL) =
WS-ROW * WS-COL
END-PERFORM
END-PERFORM
Each END-PERFORM matches the nearest unmatched PERFORM. Proper indentation is essential for readability. Most shops limit nesting to two or three levels -- deeper nesting should be refactored into out-of-line paragraphs.
See:
code/example-05-inline-perform.cobfor bubble sort, Fibonacci sequence, character analysis, and other inline PERFORM demonstrations.
8.8 Nested PERFORM: Paragraphs Calling Other Paragraphs
A PERFORMed paragraph can itself contain PERFORM statements, creating a hierarchy of calls:
MAIN-PROGRAM.
PERFORM PROCESS-ORDER.
PROCESS-ORDER.
PERFORM VALIDATE-ORDER
PERFORM CALCULATE-TOTAL
PERFORM APPLY-DISCOUNT
PERFORM GENERATE-INVOICE.
VALIDATE-ORDER.
PERFORM CHECK-CUSTOMER-STATUS
PERFORM CHECK-INVENTORY.
CHECK-CUSTOMER-STATUS.
...
This creates a call tree:
MAIN-PROGRAM
└── PROCESS-ORDER
├── VALIDATE-ORDER
│ ├── CHECK-CUSTOMER-STATUS
│ └── CHECK-INVENTORY
├── CALCULATE-TOTAL
├── APPLY-DISCOUNT
└── GENERATE-INVOICE
There is no technical limit on nesting depth, but practical programs typically have three to five levels. Deeper nesting suggests the program may benefit from being split into separate programs called via the CALL statement.
The PERFORM Stack
COBOL maintains a PERFORM stack (also called a return address stack) to track where to return after each PERFORM completes. When you PERFORM PROCESS-ORDER, and PROCESS-ORDER then PERFORMs VALIDATE-ORDER, both return addresses are on the stack. When VALIDATE-ORDER completes, control returns to the statement after the PERFORM in PROCESS-ORDER. When PROCESS-ORDER completes, control returns to the statement after the PERFORM in MAIN-PROGRAM.
This is exactly how function call stacks work in other languages, but with a critical difference: COBOL paragraphs are not truly separate scopes. They share all WORKING-STORAGE variables. A deeply nested paragraph can modify any variable that a higher-level paragraph depends on. This makes discipline and naming conventions essential.
Recursive PERFORM
Traditional COBOL does not support recursion -- a paragraph cannot PERFORM itself (directly or indirectly). Attempting to do so causes undefined behavior in most compilers. However, COBOL 2002 introduced the RECURSIVE attribute for programs, allowing recursive CALL statements (though not recursive PERFORM).
8.9 The GO TO Controversy: Structured Programming Without GO TO
In the early days of COBOL (the 1960s and 1970s), GO TO was heavily used for all flow control. Programs were littered with GO TO statements that transferred control forward and backward through the source code, creating what Edsger Dijkstra famously called "spaghetti code."
The structured programming movement of the 1970s advocated replacing GO TO with structured constructs. In COBOL, this means using PERFORM for all flow control:
| GO TO Pattern | Structured PERFORM Replacement |
|---|---|
| GO TO loop-start | PERFORM UNTIL / PERFORM VARYING |
| GO TO skip-section | IF/ELSE with PERFORM |
| GO TO error-handler | EVALUATE with PERFORM |
| GO TO end-of-paragraph | EXIT PARAGRAPH (COBOL 2002+) |
GO TO Still Has Defenders
While most modern standards discourage GO TO, some experienced COBOL programmers argue that a limited, disciplined use of GO TO can actually improve readability in certain cases -- specifically, GO TO the EXIT paragraph at the end of a PERFORM THRU range:
PROCESS-RECORD.
IF WS-RECORD-TYPE = "X"
GO TO PROCESS-RECORD-EXIT
END-IF
... process the record ...
PROCESS-RECORD-EXIT.
EXIT.
This is effectively an early return from a paragraph, which is a common and accepted pattern in other languages. The COBOL 2002 standard addressed this need directly with EXIT PARAGRAPH (see Section 8.11).
Converting GO TO to PERFORM
When maintaining legacy code, you may need to convert GO TO-based logic to PERFORM-based logic. The key techniques are:
- GO TO that skips forward: Replace with IF/ELSE.
- GO TO that loops backward: Replace with PERFORM UNTIL.
- GO TO to an exit paragraph: Replace with EXIT PARAGRAPH or restructure with IF.
- GO TO in the middle of a paragraph: Split into multiple paragraphs connected by PERFORM.
See:
case-study-02.mdfor a detailed example of converting a GO TO-heavy program to structured PERFORM-based code.
8.10 PERFORM and Structured Programming: Top-Down Design
The PERFORM statement enables the top-down design pattern that has been the standard for COBOL development since the 1980s. In this pattern, the program is organized as a hierarchy of paragraphs, each performing a well-defined function.
The Init-Process-Terminate (IPT) Pattern
Nearly every batch COBOL program follows this fundamental structure:
MAIN-PROCESS.
PERFORM INITIALIZATION
PERFORM PROCESS-RECORDS
PERFORM WRAP-UP
STOP RUN.
INITIALIZATION opens files, initializes working storage, and prints report headers.
PROCESS-RECORDS contains the main processing loop, reading and processing records until end-of-file.
WRAP-UP prints summary totals, closes files, and performs final cleanup.
The Priming Read Pattern
Within PROCESS-RECORDS, the standard COBOL file-processing idiom uses a priming read -- reading the first record before entering the loop:
PROCESS-RECORDS.
PERFORM READ-NEXT-RECORD *> Priming read
PERFORM UNTIL END-OF-FILE
PERFORM PROCESS-ONE-RECORD
PERFORM READ-NEXT-RECORD *> Read next
END-PERFORM.
The priming read is necessary because COBOL's PERFORM UNTIL with TEST BEFORE (the default) tests the condition before executing the body. Without the priming read, the program would try to process a record before one had been read.
An alternative using WITH TEST AFTER eliminates the priming read:
PROCESS-RECORDS.
PERFORM WITH TEST AFTER
UNTIL END-OF-FILE
PERFORM READ-NEXT-RECORD
IF NOT END-OF-FILE
PERFORM PROCESS-ONE-RECORD
END-IF
END-PERFORM.
However, the priming read pattern with TEST BEFORE is more traditional and widely used.
The Processing Hierarchy
A well-structured COBOL program has a clear paragraph hierarchy:
Level 0: MAIN-PROCESS
Level 1: INITIALIZATION, PROCESS-RECORDS, WRAP-UP
Level 2: READ-RECORD, VALIDATE-RECORD, PROCESS-VALID, WRITE-OUTPUT
Level 3: CALCULATE-AMOUNTS, UPDATE-TOTALS, FORMAT-OUTPUT
Each paragraph should:
- Have a single, well-defined purpose (the Single Responsibility Principle)
- Be named descriptively (verb-noun format is standard: CALCULATE-TAX, VALIDATE-ACCOUNT)
- Be short enough to fit on one screen (roughly 30-50 lines)
- Call lower-level paragraphs via PERFORM for detailed work
Jackson Structured Programming (JSP)
Michael Jackson's Structured Programming method, widely used in COBOL shops in the 1970s and 1980s, maps data structures to program structures. In JSP, the structure of the program mirrors the structure of the data it processes:
- A sequence of data items maps to a sequence of PERFORM statements
- A selection (record type A or B) maps to IF/EVALUATE with PERFORM
- An iteration (repeating records) maps to PERFORM UNTIL
JSP produces a particular style of COBOL program where the paragraph hierarchy directly reflects the data hierarchy. For example, processing a file of orders containing header records and detail records:
PROCESS-ALL-ORDERS.
PERFORM UNTIL END-OF-FILE
PERFORM PROCESS-ONE-ORDER
END-PERFORM.
PROCESS-ONE-ORDER.
PERFORM PROCESS-ORDER-HEADER
PERFORM UNTIL ORDER-BREAK OR END-OF-FILE
PERFORM PROCESS-ORDER-DETAIL
END-PERFORM
PERFORM PRINT-ORDER-TOTAL.
Warnier-Orr Methodology
The Warnier-Orr method is similar to JSP but uses a different diagramming notation (Warnier-Orr diagrams or bracket diagrams). It also maps data structures to program structures and produces well-organized PERFORM hierarchies. Both JSP and Warnier-Orr are still taught in some COBOL training programs and remain relevant for understanding legacy program structures.
See:
code/example-06-structured-program.cobfor a complete structured program implementing the IPT pattern with a full paragraph hierarchy.
8.11 EXIT PARAGRAPH and EXIT SECTION (COBOL 2002+)
COBOL 2002 introduced EXIT PARAGRAPH and EXIT SECTION to provide an early exit mechanism -- similar to return in other languages:
VALIDATE-RECORD.
IF WS-RECORD-TYPE = SPACES
DISPLAY "Empty record type"
EXIT PARAGRAPH
END-IF
IF WS-ACCOUNT-NUM = ZEROS
DISPLAY "Invalid account"
EXIT PARAGRAPH
END-IF
SET RECORD-IS-VALID TO TRUE.
When EXIT PARAGRAPH is executed, control immediately transfers to the end of the current paragraph, as if the paragraph's terminating period had been reached. This eliminates the need for deeply nested IF statements:
Without EXIT PARAGRAPH (deeply nested):
VALIDATE-RECORD.
IF WS-RECORD-TYPE NOT = SPACES
IF WS-ACCOUNT-NUM NOT = ZEROS
IF WS-AMOUNT > ZEROS
SET RECORD-IS-VALID TO TRUE
ELSE
DISPLAY "Invalid amount"
END-IF
ELSE
DISPLAY "Invalid account"
END-IF
ELSE
DISPLAY "Empty record type"
END-IF.
With EXIT PARAGRAPH (flat, guard-clause style):
VALIDATE-RECORD.
IF WS-RECORD-TYPE = SPACES
DISPLAY "Empty record type"
EXIT PARAGRAPH
END-IF
IF WS-ACCOUNT-NUM = ZEROS
DISPLAY "Invalid account"
EXIT PARAGRAPH
END-IF
IF WS-AMOUNT NOT > ZEROS
DISPLAY "Invalid amount"
EXIT PARAGRAPH
END-IF
SET RECORD-IS-VALID TO TRUE.
The flat structure with guard clauses is easier to read, easier to maintain, and easier to extend with additional validations.
EXIT SECTION works identically but exits the current section rather than just the current paragraph.
Note
EXIT PARAGRAPH and EXIT SECTION require COBOL 2002 or later. If you are targeting COBOL-85, the equivalent is GO TO the EXIT paragraph at the end of a PERFORM THRU range.
8.12 EXIT PERFORM and EXIT PERFORM CYCLE (COBOL 2002+)
COBOL 2002 also introduced EXIT PERFORM and EXIT PERFORM CYCLE, which are the COBOL equivalents of break and continue from C/Java/Python.
EXIT PERFORM (Like break)
EXIT PERFORM immediately terminates the innermost enclosing inline PERFORM loop and transfers control to the statement following END-PERFORM:
PERFORM VARYING WS-I FROM 1 BY 1
UNTIL WS-I > 1000
IF WS-TABLE(WS-I) = WS-SEARCH-KEY
SET ITEM-FOUND TO TRUE
EXIT PERFORM
END-IF
END-PERFORM
EXIT PERFORM CYCLE (Like continue)
EXIT PERFORM CYCLE skips the remaining statements in the current iteration and proceeds to the next iteration (after incrementing the VARYING variable, if applicable):
PERFORM VARYING WS-I FROM 1 BY 1
UNTIL WS-I > 100
IF WS-RECORD-TYPE(WS-I) = "H"
EXIT PERFORM CYCLE *> Skip headers
END-IF
PERFORM PROCESS-DETAIL-RECORD
END-PERFORM
Important Limitations
EXIT PERFORMandEXIT PERFORM CYCLEonly work with inline PERFORM (the form withEND-PERFORM). They cannot be used to exit an out-of-line PERFORM of a paragraph.- They exit only the innermost enclosing PERFORM. To break out of nested loops, you typically need to set a flag that the outer loop checks.
8.13 Performance Considerations
Out-of-Line vs. Inline Performance
In general, the performance difference between inline and out-of-line PERFORM is negligible on modern hardware. However, there are some considerations:
- Out-of-line PERFORM involves saving and restoring a return address, which adds a tiny overhead per call. For tight loops executed millions of times, this can accumulate.
- Inline PERFORM eliminates the call/return overhead since the code is in-place. However, if the same logic is duplicated in multiple inline blocks, the program becomes larger, potentially affecting instruction cache behavior.
- Compiler optimization: Modern COBOL compilers (IBM Enterprise COBOL, Micro Focus Visual COBOL, GnuCOBOL) often optimize out-of-line PERFORM calls, especially for small paragraphs. The compiler may effectively inline the paragraph.
Practical advice: Write for clarity first. Optimize only if profiling reveals a specific PERFORM as a bottleneck.
PERFORM VARYING vs. Manual Counter Management
PERFORM VARYING is generally as efficient as managing a counter manually with PERFORM UNTIL, because the compiler generates similar code for both. Use PERFORM VARYING when you have a counting loop -- it is clearer and less error-prone.
Index vs. Subscript Performance
Index-based table access (using INDEXED BY) can be significantly faster than subscript-based access (using a numeric data item). Indexes are stored as byte displacements, so accessing a table element requires only an addition. Subscript access requires a multiplication (subscript value times element size) followed by an addition. On IBM mainframes, this difference can matter in tight loops over large tables.
8.14 The PERFORM Stack: How COBOL Tracks Return Addresses
Understanding the PERFORM stack is important for avoiding subtle bugs. When a PERFORM statement executes:
- The return address (the address of the statement following the PERFORM) is pushed onto an internal stack.
- Control transfers to the target paragraph.
- When the paragraph ends, the return address is popped from the stack, and control resumes there.
For nested PERFORMs, multiple return addresses are on the stack simultaneously:
MAIN-PROGRAM ──PERFORM──> PROCESS-ORDER ──PERFORM──> VALIDATE
[return-1 pushed] [return-2 pushed]
... executes ...
[return-2 popped] <──returns──
[return-1 popped] <──returns──
The "Falling Through" Danger
One classic COBOL pitfall involves a paragraph that is both PERFORMed and "fallen through to" in the normal flow of execution. Consider:
PARA-A.
PERFORM PARA-B
DISPLAY "Back in A".
PARA-B.
DISPLAY "In B".
PARA-C.
DISPLAY "In C".
If PARA-A is being executed via a PERFORM, and PARA-B is also PERFORMed from within PARA-A, the execution is straightforward. But if the code is arranged so that PARA-B can be reached both by PERFORM and by falling through from PARA-A (without a PERFORM), the PERFORM stack can get confused. This is why structured programming discipline -- where every paragraph is only reached via PERFORM, never by falling through -- is so important.
8.15 Common PERFORM Patterns
Pattern 1: Read-Process-Write Loop
The most fundamental COBOL pattern:
PROCESS-FILE.
PERFORM READ-INPUT
PERFORM UNTIL END-OF-FILE
PERFORM PROCESS-RECORD
PERFORM WRITE-OUTPUT
PERFORM READ-INPUT
END-PERFORM.
Pattern 2: Control Break Processing
Processing sorted records and printing subtotals when a key field changes:
PROCESS-RECORDS.
PERFORM READ-RECORD
MOVE WS-CURRENT-KEY TO WS-PREVIOUS-KEY
PERFORM UNTIL END-OF-FILE
IF WS-CURRENT-KEY NOT = WS-PREVIOUS-KEY
PERFORM PRINT-SUBTOTAL
MOVE ZEROS TO WS-SUBTOTAL
MOVE WS-CURRENT-KEY TO WS-PREVIOUS-KEY
END-IF
ADD WS-AMOUNT TO WS-SUBTOTAL
ADD WS-AMOUNT TO WS-GRAND-TOTAL
PERFORM READ-RECORD
END-PERFORM
PERFORM PRINT-SUBTOTAL
PERFORM PRINT-GRAND-TOTAL.
Pattern 3: Multi-Level Control Break
Extending the control break pattern for multiple levels (e.g., region, district, office):
PROCESS-RECORDS.
PERFORM READ-RECORD
PERFORM INITIALIZE-ALL-BREAKS
PERFORM UNTIL END-OF-FILE
IF WS-REGION NOT = WS-PREV-REGION
PERFORM OFFICE-BREAK
PERFORM DISTRICT-BREAK
PERFORM REGION-BREAK
ELSE IF WS-DISTRICT NOT = WS-PREV-DISTRICT
PERFORM OFFICE-BREAK
PERFORM DISTRICT-BREAK
ELSE IF WS-OFFICE NOT = WS-PREV-OFFICE
PERFORM OFFICE-BREAK
END-IF
PERFORM ACCUMULATE-DETAIL
PERFORM READ-RECORD
END-PERFORM
PERFORM OFFICE-BREAK
PERFORM DISTRICT-BREAK
PERFORM REGION-BREAK
PERFORM PRINT-GRAND-TOTAL.
Pattern 4: Accumulation Loop
Iterating through a table to compute totals, averages, min, max:
MOVE ZEROS TO WS-TOTAL
MOVE ZEROS TO WS-MAX-VAL
MOVE 999999 TO WS-MIN-VAL
PERFORM VARYING WS-I FROM 1 BY 1
UNTIL WS-I > WS-TABLE-SIZE
ADD WS-VALUE(WS-I) TO WS-TOTAL
IF WS-VALUE(WS-I) > WS-MAX-VAL
MOVE WS-VALUE(WS-I) TO WS-MAX-VAL
END-IF
IF WS-VALUE(WS-I) < WS-MIN-VAL
MOVE WS-VALUE(WS-I) TO WS-MIN-VAL
END-IF
END-PERFORM
DIVIDE WS-TOTAL BY WS-TABLE-SIZE
GIVING WS-AVERAGE.
Pattern 5: Search Loop
Searching a table with early exit:
MOVE 1 TO WS-INDEX
SET NOT-FOUND TO TRUE
PERFORM UNTIL FOUND OR WS-INDEX > WS-TABLE-SIZE
IF WS-KEY(WS-INDEX) = WS-SEARCH-VALUE
SET FOUND TO TRUE
ELSE
ADD 1 TO WS-INDEX
END-IF
END-PERFORM
8.16 Common Mistakes and Pitfalls
Mistake 1: Infinite Loops
Forgetting to modify the loop variable or condition:
* BUG: WS-COUNTER is never incremented
PERFORM UNTIL WS-COUNTER > 10
DISPLAY WS-COUNTER
END-PERFORM
* FIX:
PERFORM UNTIL WS-COUNTER > 10
DISPLAY WS-COUNTER
ADD 1 TO WS-COUNTER
END-PERFORM
Mistake 2: Off-by-One Errors
Using the wrong comparison operator:
* Iterates 11 times (0 through 10), not 10
PERFORM VARYING WS-I FROM 0 BY 1
UNTIL WS-I > 10
...
END-PERFORM
* Iterates 10 times (0 through 9) -- probably intended
PERFORM VARYING WS-I FROM 0 BY 1
UNTIL WS-I >= 10
...
END-PERFORM
Mistake 3: Modifying the VARYING Variable Inside the Loop
The COBOL standard allows you to modify the VARYING variable inside the loop, but doing so leads to confusing, error-prone code:
* DANGEROUS: Modifying WS-I inside a VARYING loop
PERFORM VARYING WS-I FROM 1 BY 1
UNTIL WS-I > 10
IF WS-SKIP-FLAG = 'Y'
ADD 1 TO WS-I *> Skips an element, but confusing
END-IF
END-PERFORM
The problem is that the VARYING mechanism also adds BY 1 at the end of each iteration. So if you add 1 inside the loop, the effective step is 2 for that iteration. This is a common source of subtle bugs. Use EXIT PERFORM CYCLE instead if your compiler supports it, or restructure with PERFORM UNTIL.
Mistake 4: PERFORM THRU Scope Creep
As discussed earlier, inserting paragraphs within a THRU range inadvertently includes them in the execution. Always use explicit EXIT paragraphs as endpoints if you must use THRU.
Mistake 5: Forgetting the Priming Read
Entering a PERFORM UNTIL END-OF-FILE loop without first reading a record causes the program to process whatever happens to be in the record area -- typically spaces or zeros from initialization:
* BUG: No priming read -- WS-RECORD is spaces
PERFORM UNTIL END-OF-FILE
PERFORM PROCESS-RECORD *> Processes garbage!
PERFORM READ-RECORD
END-PERFORM
* FIX: Priming read before the loop
PERFORM READ-RECORD
PERFORM UNTIL END-OF-FILE
PERFORM PROCESS-RECORD
PERFORM READ-RECORD
END-PERFORM
Mistake 6: Assuming PERFORM TIMES Provides a Counter
* BUG: Trying to use the TIMES count as a counter
PERFORM DISPLAY-ITEM 10 TIMES
...
DISPLAY-ITEM.
* There is no implicit counter here!
* WS-ITEM-NUMBER is not automatically incremented.
DISPLAY "Item " WS-ITEM-NUMBER. *> Always shows same value
8.17 Quick Reference: All PERFORM Forms
| Form | Syntax | Equivalent In Other Languages |
|---|---|---|
| Basic | PERFORM para-name |
Function call |
| THRU | PERFORM para-a THRU para-z |
Calling a sequence of functions |
| TIMES | PERFORM para-name n TIMES |
for (i=0; i<n; i++) (no counter) |
| UNTIL (TEST BEFORE) | PERFORM UNTIL cond |
while (!cond) |
| UNTIL (TEST AFTER) | PERFORM WITH TEST AFTER UNTIL cond |
do { ... } while (!cond) |
| VARYING | PERFORM VARYING i FROM a BY b UNTIL cond |
for (i=a; !cond; i+=b) |
| VARYING with AFTER | PERFORM VARYING i ... AFTER j ... |
Nested for loops |
| Inline | PERFORM ... END-PERFORM |
Block of code (no function) |
8.18 Free-Format Examples (COBOL 2002+)
For completeness, here are key examples in free format. Note that in free format, the column restrictions of fixed format do not apply -- code can start in any column, and there is no area A/area B distinction:
identification division.
program-id. free-format-perform.
data division.
working-storage section.
01 ws-index pic 9(3) value zeros.
01 ws-total pic 9(7)v99 value zeros.
01 ws-eof-flag pic x value 'N'.
88 end-of-file value 'Y'.
procedure division.
main-process.
perform initialization
perform process-records
perform wrap-up
stop run.
initialization.
display "Starting..."
move zeros to ws-total
set end-of-file to false.
process-records.
perform read-record
perform until end-of-file
perform varying ws-index from 1 by 1
until ws-index > 10
add ws-amount(ws-index) to ws-total
end-perform
perform read-record
end-perform.
wrap-up.
display "Total: " ws-total
display "Complete.".
Free format is increasingly used in modern COBOL development, particularly with Micro Focus Visual COBOL and GnuCOBOL. However, the vast majority of existing COBOL code is in fixed format, so both formats must be understood.
8.19 Chapter Summary
The PERFORM statement is the most important and versatile statement in COBOL. In this chapter, we have covered:
- Basic PERFORM: Executing a named paragraph and returning -- COBOL's subroutine mechanism.
- PERFORM THRU: Executing a range of paragraphs, with its benefits (structured exit points) and risks (scope creep, ordering dependency).
- Inline vs. Out-of-Line: How to choose between placing code in a separate paragraph or inline within
END-PERFORM. - PERFORM TIMES: Fixed-count iteration without an automatic counter.
- PERFORM UNTIL: Conditional iteration with TEST BEFORE (while-loop) and TEST AFTER (do-while loop) semantics, and the use of 88-level condition names.
- PERFORM VARYING: Counting loops with FROM/BY/UNTIL, including nested loops via the AFTER clause, and the use of indexes vs. subscripts.
- EXIT PARAGRAPH, EXIT PERFORM, EXIT PERFORM CYCLE: Modern flow-control additions from COBOL 2002+.
- The PERFORM stack: How COBOL tracks return addresses and why paragraph fall-through is dangerous.
- Structured programming patterns: IPT, priming read, control break, accumulation, search.
- Common mistakes: Infinite loops, off-by-one errors, modifying VARYING variables, forgotten priming reads.
The PERFORM statement is the glue that holds COBOL programs together. Master it, and you have mastered the most fundamental aspect of COBOL programming.
Next chapter: We will explore the EVALUATE statement, COBOL's multi-way branching construct, which replaces nested IF statements and provides COBOL's equivalent of the switch/case statement found in other languages.