20 min read

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...

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 while and do-while)
  • A counting loop (like for (i = start; i < end; i += step))
  • A nested loop construct (via the AFTER clause)

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:

  1. The current execution position is saved (pushed onto the PERFORM stack).
  2. Control transfers to the first statement of CALCULATE-TAX.
  3. All statements in CALCULATE-TAX are executed.
  4. 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.cob for 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.cob for 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.cob for 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

  1. No automatic counter. Unlike for loops in most languages, PERFORM TIMES does 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
  1. 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
  1. 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.

  2. 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.cob for 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.cob for 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:

  1. Set identifier-1 to the FROM value.
  2. Evaluate the UNTIL condition. If true, exit the loop.
  3. Execute the loop body.
  4. Add the BY value to identifier-1.
  5. 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.cob for 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.cob for 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:

  1. GO TO that skips forward: Replace with IF/ELSE.
  2. GO TO that loops backward: Replace with PERFORM UNTIL.
  3. GO TO to an exit paragraph: Replace with EXIT PARAGRAPH or restructure with IF.
  4. GO TO in the middle of a paragraph: Split into multiple paragraphs connected by PERFORM.

See: case-study-02.md for 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.cob for 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 PERFORM and EXIT PERFORM CYCLE only work with inline PERFORM (the form with END-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:

  1. The return address (the address of the statement following the PERFORM) is pushed onto an internal stack.
  2. Control transfers to the target paragraph.
  3. 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:

  1. Basic PERFORM: Executing a named paragraph and returning -- COBOL's subroutine mechanism.
  2. PERFORM THRU: Executing a range of paragraphs, with its benefits (structured exit points) and risks (scope creep, ordering dependency).
  3. Inline vs. Out-of-Line: How to choose between placing code in a separate paragraph or inline within END-PERFORM.
  4. PERFORM TIMES: Fixed-count iteration without an automatic counter.
  5. PERFORM UNTIL: Conditional iteration with TEST BEFORE (while-loop) and TEST AFTER (do-while loop) semantics, and the use of 88-level condition names.
  6. PERFORM VARYING: Counting loops with FROM/BY/UNTIL, including nested loops via the AFTER clause, and the use of indexes vs. subscripts.
  7. EXIT PARAGRAPH, EXIT PERFORM, EXIT PERFORM CYCLE: Modern flow-control additions from COBOL 2002+.
  8. The PERFORM stack: How COBOL tracks return addresses and why paragraph fall-through is dangerous.
  9. Structured programming patterns: IPT, priming read, control break, accumulation, search.
  10. 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.