Explainer 5 of 11

Value sets and dynamic calls

Resolving CALL WS-PGM without running the program: track every value a field can hold, and say so when the list may be short.

Where this shows up in reverse engineering

Any call graph for a program with dynamic dispatch has this hole in it until something fills it in — the question is always the same: what values can actually reach the thing deciding where to go next.

The problem

CALL 'PROGB' names its target in the source; a call graph can draw that edge by reading it. CALL WS-PGM calls whatever program name is in WS-PGM when the statement runs. Leave those edges out and the call graph has holes exactly where a program dispatches on data — menus, routers, transaction handlers. Guess and it draws edges that do not exist.

The intuition

For every field, carry the set of byte values it may hold at each statement. A MOVE of a literal sets it to exactly that value. A MOVE from another field copies that field's set. Where paths meet, take the union. At a CALL through a field, the field's set is the list of programs that call could reach.

The set can be incomplete: the value may come from a file, from a caller, or from nowhere the analysis can see. So each set also carries reasons it may be missing values. An empty set of reasons means "these and no others"; any reason means "at least these".

The example

Two branches put different program names into WS-PGM before calling through it. A second call goes through WS-OTHER, which nothing assigns.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. VALSETS.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-FLAG              PIC X.
       01  WS-PGM               PIC X(8).
       01  WS-OTHER             PIC X(8).
       PROCEDURE DIVISION.
       MAIN-PARA.
           IF WS-FLAG = 'Y'
               MOVE 'PROGB' TO WS-PGM
           ELSE
               MOVE 'PROGC' TO WS-PGM
           END-IF.
           CALL WS-PGM.
           CALL WS-OTHER.
           STOP RUN.

Step by step

Values are shown as stored: PIC X(8) pads 'PROGB' with three spaces, written ␣. A call trims the trailing spaces off a value to get a program name.

The value sets, one statement at a time (6 steps)
  1. IF WS-FLAG = 'Y'. On entry, nothing has been written: every field is empty and uninitialised.

    Value sets after this statement (amber: reasons the set may be short)

    fieldvalues
    WS-FLAG{} + uninitialised
    WS-PGM{} + uninitialised
    WS-OTHER{} + uninitialised
  2. MOVE 'PROGB' TO WS-PGM replaces WS-PGM's set with exactly one value, and no reasons.

    Value sets after this statement (amber: reasons the set may be short)

    fieldvalues
    WS-FLAG{} + uninitialised
    WS-PGM{'PROGB␣␣␣'}
    WS-OTHER{} + uninitialised
  3. MOVE 'PROGC' TO WS-PGM replaces WS-PGM's set with exactly one value, and no reasons.

    Value sets after this statement (amber: reasons the set may be short)

    fieldvalues
    WS-FLAG{} + uninitialised
    WS-PGM{'PROGC␣␣␣'}
    WS-OTHER{} + uninitialised
  4. Two paths meet: each field's values and reasons are the union of both arms'. CALL WS-PGM: candidates: 2 possible edges, to PROGB and PROGC.

    Value sets after this statement (amber: reasons the set may be short)

    fieldvalues
    WS-FLAG{} + uninitialised
    WS-PGM{'PROGB␣␣␣', 'PROGC␣␣␣'}
    WS-OTHER{} + uninitialised

    Row written for this call

    candidatesPROGB, PROGC
    reasonsnone
    in the graphcandidates: 2 possible edges, to PROGB and PROGC
  5. CALL WS-OTHER: unresolved: a dynamic-target node for WS-OTHER, because uninitialised.

    Value sets after this statement (amber: reasons the set may be short)

    fieldvalues
    WS-FLAG{} + uninitialised
    WS-PGM{'PROGB␣␣␣', 'PROGC␣␣␣'}
    WS-OTHER{} + uninitialised

    Row written for this call

    candidatesnone
    reasonsuninitialised
    in the graphunresolved: a dynamic-target node for WS-OTHER, because uninitialised
  6. STOP RUN.

    Value sets after this statement (amber: reasons the set may be short)

    fieldvalues
    WS-FLAG{} + uninitialised
    WS-PGM{'PROGB␣␣␣', 'PROGC␣␣␣'}
    WS-OTHER{} + uninitialised

The rule

value set = (values, reasons)
MOVE literal TO f : f := ({literal padded to f}, {})
MOVE g TO f : f := g's set
merge : (values₁ ∪ values₂, reasons₁ ∪ reasons₂), at most 32 values
never written : ({}, {uninitialised})
call through f : candidates = trimmed values of f, reasons = f's reasons

A call site then becomes one of three things in the graph:

What goes wrong

In cobble

The fixpoint stage (cobble-dataflow-fixpoint) builds the value sets, applies the budget and records the reasons. It writes each dynamic call site as a targets.jsonl row. The graph build (cobble-build) reads those rows and applies the three-way rule.