Explainer 5 of 11

Value sets and dynamic calls

Read first: Fixpoints and worklists, Gen/kill and reaching definitions

VALSETS moves 'PROGB' into WS-PGM on one arm of an IF and 'PROGC' on the other, then does CALL WS-PGM; it also does CALL WS-OTHER, which nothing assigns. Value sets track every value a field may hold, and why that list may be short: the first call gets candidate edges to PROGB and PROGC, the second a visible unresolved node, reason uninitialised.

New to CALL, MOVE or PIC X(8)? The COBOL/CICS primer has the vocabulary.

The example

Read alone, the CALL statements of VALSETS give a call graph with no edges (box read alone); value sets give the box with value sets.

flowchart LR
    subgraph vs["with value sets"]
        b0["VALSETS"] -.->|CALL_CANDIDATE| b1["PROGB"]
        b0 -.->|CALL_CANDIDATE| b2["PROGC"]
        b0 -->|CALL| b3["VALSETS.WS-OTHER
dynamic target: uninitialised"] end subgraph read["read alone"] a0["VALSETS"] end

That is the whole answer for this example, one row per call:

callvaluesreasonsreads as
CALL WS-PGMPROGB, PROGCnonethese and no others
CALL WS-OTHERnoneuninitialisedat least these (none known)

The lines that matter. Neither field has a VALUE clause:

01  WS-PGM     PIC X(8).
01  WS-OTHER   PIC X(8).
    IF WS-FLAG = 'Y'
        MOVE 'PROGB' TO WS-PGM
    ELSE
        MOVE 'PROGC' TO WS-PGM
    END-IF.
    CALL WS-PGM.
    CALL WS-OTHER.
Full listing (VALSETS)
       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. The strip below is WS-PGM, a PIC X(8), after MOVE 'PROGB' TO WS-PGM (step 2 of the walk): the literal fills bytes [0, 5) and the field is padded with three spaces in bytes [5, 8), written ␣. That is why the set in step 2 reads 'PROGB␣␣␣'. A call trims the trailing spaces off a value to get a program name: PROGB.

WS-PGM after step 2 (amber: bytes from the literal; blue: padding)

0
1
2
3
4
5
6
7
WS-PGM
P
R
O
G
B
␣
␣
␣
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

Sets, and reasons they may be short

The walk carried, for every field, the set of byte values it may hold after each statement. Follow WS-PGM through it: a MOVE of a literal sets the set to exactly that value (rows 1 and 2, steps 2 and 3 of the walk); where the two arms of the IF meet, take the union (row 3, step 4); at a CALL through the field, the set is the list of programs that call could reach (row 4, also step 4). A MOVE from another field, which the example does not have, copies that field's set.

#statementWS-PGM's set after it
1MOVE 'PROGB' TO WS-PGM (then-arm){'PROGB␣␣␣'}
2MOVE 'PROGC' TO WS-PGM (else-arm){'PROGC␣␣␣'}
3END-IF (arms meet){'PROGB␣␣␣', 'PROGC␣␣␣'}
4CALL WS-PGMcan reach PROGB, PROGC

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, and the answer table under the example shows both readings. WS-PGM ends with two values and no reasons: "these and no others". WS-OTHER is never assigned, so from step 1 to the call in step 5 it carries no values and the reason uninitialised: "at least these" — here, at least nothing.

Why a call through a field needs this

CALL 'PROGB'.   target in the source:
                edge to PROGB
CALL WS-PGM.    target is whatever
                WS-PGM holds

The first call names its target; a call graph can draw that edge by reading it. The second calls whatever is in WS-PGM when it runs, which in VALSETS is PROGB on one arm of the IF and PROGC on the other. Reading statements alone gives the example's edgeless read alone box; the walk fills in with value sets.

Where this shows up

The rule

value set = (values, reasons)
entry:
  f := ({f's VALUE bytes}, {})
    if f has a VALUE clause,
    else ({}, {uninitialised})
MOVE literal TO f:
  f := ({literal padded or truncated
    to f}, {})
MOVE g TO f:
  f := g's set
merge:
  (values₁ ∪ values₂, reasons₁ ∪ reasons₂);
  if more than 32 values, keep the first
  32 in sorted order and add over-budget
call through f:
  candidates = trimmed values of f,
  reasons = f's reasons

Each line of the box is one the walk above has already used. The table points every line back at the step that applied it:

rule linewhere the walk applied it
entrystep 1: none of the three fields has a VALUE clause, so each starts as {} + uninitialised
MOVE literal TO fsteps 2 and 3: WS-PGM becomes {'PROGB␣␣␣'}, then {'PROGC␣␣␣'} on the other arm
MOVE g TO fnot used: the example has no field-to-field MOVE (but see the fragment just below)
mergestep 4: {'PROGB␣␣␣'} ∪ {'PROGC␣␣␣'} is two values, well under 32, so nothing is cut and no over-budget
call through fstep 4 (WS-PGM: PROGB, PROGC, no reasons) and step 5 (WS-OTHER: no candidates, uninitialised)

cobble departs from the MOVE g TO f line in one case: when g holds no known value. Change the example's else-arm to MOVE WS-OTHER TO WS-PGM, as below. By the box, that arm gives WS-PGM the set of WS-OTHER, {} + uninitialised. cobble stores no value for such a move, only that WS-PGM was written, so the call reads that arm as not-a-literal instead. Both reasons say the same thing to the graph: the list may be short.

ELSE
    MOVE WS-OTHER TO WS-PGM
END-IF.
CALL WS-PGM.   candidates PROGB
               reasons    not-a-literal

The entry line plays the part of the teaching-only start definition on the fixpoints page (cobble itself has no such definition): a VALUE clause supplies the field's starting value, and without one the starting value is unknown. Change one line of the example to give WS-OTHER a VALUE, and step 1 starts it as ({'PROGD␣␣␣'}, {}) instead of {} + uninitialised; nothing else writes it, so CALL WS-OTHER gets candidate PROGD with no reasons.

01  WS-OTHER  PIC X(8) VALUE 'PROGD'.

The two MOVE lines gloss over size: when source and target differ in length, an alphanumeric MOVE pads the target with spaces, as in the strip under step by step, or cuts the value off on the right. If the else-arm were MOVE 'PROGRAMNAME' TO WS-PGM, the eleven-byte literal would not fit: its bytes [0, 8) land in WS-PGM, shown below, and AME, its bytes [8, 11), is cut off. The set after the arms meet would be {'PROGB␣␣␣', 'PROGRAMN'}. Read both MOVE lines as applying this to each value in the set.

WS-PGM after MOVE 'PROGRAMNAME' TO WS-PGM (amber: bytes from the literal)

0
1
2
3
4
5
6
7
WS-PGM
P
R
O
G
R
A
M
N

A call site then becomes one of three things in the graph. The table after the list has one site of each kind: the two calls of VALSETS, and CALL WS-OTHER in the VALUE 'PROGD' variant above (call that variant VALSETV).

#sitecandidatesreasonsin the graph
1VALSETS: CALL WS-PGMPROGB, PROGCnonecandidates: two CALL_CANDIDATE edges
2VALSETS: CALL WS-OTHERnoneuninitialisedunresolved: a CALL edge to node VALSETS.WS-OTHER
3VALSETV: CALL WS-OTHERPROGDnoneproven: a CALL edge to PROGD

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. These are the two rows it writes for VALSETS; MAIN-PARA/1 and MAIN-PARA/2 are the two CALL statements, and the columns are rows 1 and 2 of the three-way table under the rule.

{"caller": "VALSETS", "node": "MAIN-PARA/1",
 "transfer": "CALL", "expression": "WS-PGM",
 "candidates": ["PROGB", "PROGC"],
 "reasons": [], "dropped": []}
{"caller": "VALSETS", "node": "MAIN-PARA/2",
 "transfer": "CALL", "expression": "WS-OTHER",
 "candidates": [], "reasons": ["uninitialised"],
 "dropped": []}

The graph build (cobble-build) reads those rows and applies the three-way rule. From these two it draws the with value sets box in the graph under the example: two CALL_CANDIDATE edges from the first row, and from the second a CALL edge to a DYNAMIC_TARGET node with id VALSETS.WS-OTHER and reason uninitialised.