Explainer 7 of 11

Resolving a dynamic call site inside its own program

Read first: Value sets and dynamic calls, Area need

Area need charges a dynamic call site whose callees it does not know the union of every member's entry need. Where some programs need thousands of bytes, that union is thousands of bytes, and it spreads from the site's program to its callers.

PROGA has three dynamic XCTLs that hand over its commarea. Two take their names from PROGA's own literals, so a walk over PROGA alone resolves them: they are charged 8 and 16 bytes, their callees' rows. The third copies its name out of the commarea and is charged the union of all four members' rows, 32 bytes.

Each site gets one forward reaching-definitions walk over its operand's value set. Terms are in the COBOL/CICS primer.

The example

PROGA's sites are on lines 24, 27 and 30, each handing over its 100-byte commarea. PROGB, PROGC and PROGD each XCTL on to a name in their own commarea: PROGB needs bytes [20, 28) on entry, PROGC [40, 48), PROGD [60, 68). Nothing calls PROGD.

flowchart LR
    S24["PROGA line 24
XCTL WS-FIRST-PGM"] --> B["PROGB
needs [20, 28)"] S27["PROGA line 27
XCTL WS-MENU-PGM"] --> B S27 --> C["PROGC
needs [40, 48)"] S30["PROGA line 30
XCTL WS-NEXT-PGM"] -.->|"callee unknown"| U["every member's row
for the commarea"] D["PROGD
needs [60, 68)
nothing calls it"]

Solid arrows end at programs found inside PROGA; the dashed one has no known end. That is the whole answer for this example, what each site is charged:

line 24 → PROGB: 8 bytesline 27 → PROGB, PROGC: 16 bytesline 30 unresolved: 32 bytes

The lines that matter: each site's operand, and every write to it. LK-NEXT is commarea bytes [0, 8).

* PROGA
01  WS-FIRST-PGM  PIC X(8) VALUE 'PROGB'.
01  WS-MENU-PGM   PIC X(8).
01  WS-NEXT-PGM   PIC X(8).
    IF WS-FLAG = 'Y'
        MOVE 'PROGB' TO WS-MENU-PGM
    ELSE
        MOVE 'PROGC' TO WS-MENU-PGM
    END-IF.
    MOVE LK-NEXT TO WS-NEXT-PGM.
* line 24, WHEN '1'
    EXEC CICS XCTL PROGRAM(WS-FIRST-PGM)
        COMMAREA(DFHCOMMAREA) END-EXEC
* line 27, WHEN '2'
    EXEC CICS XCTL PROGRAM(WS-MENU-PGM)
        COMMAREA(DFHCOMMAREA) END-EXEC
* line 30, WHEN OTHER
    EXEC CICS XCTL PROGRAM(WS-NEXT-PGM)
        COMMAREA(DFHCOMMAREA) END-EXEC
* PROGB
01  DFHCOMMAREA.
    05  LK-PAD   PIC X(20).
    05  LK-TGT   PIC X(8).
    05  LK-TAIL  PIC X(72).
    EXEC CICS XCTL PROGRAM(LK-TGT) END-EXEC.

PROGC and PROGD are the same with LK-PAD 40 and 60 bytes long (and LK-TAIL shortened to keep 100 bytes). Their own XCTLs hand over no area, so area need has nothing to charge them and does not list them among its dynamic sites.

Full listings (PROGA, PROGB; line numbers count from PROGA's first line)
       IDENTIFICATION DIVISION.
       PROGRAM-ID. PROGA.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-FIRST-PGM         PIC X(8) VALUE 'PROGB'.
       01  WS-MENU-PGM          PIC X(8).
       01  WS-NEXT-PGM          PIC X(8).
       01  WS-FLAG              PIC X.
       01  WS-MODE              PIC X.
       LINKAGE SECTION.
       01  DFHCOMMAREA.
           05  LK-NEXT          PIC X(8).
           05  LK-REST          PIC X(92).
       PROCEDURE DIVISION.
       MAIN-PARA.
           IF WS-FLAG = 'Y'
               MOVE 'PROGB' TO WS-MENU-PGM
           ELSE
               MOVE 'PROGC' TO WS-MENU-PGM
           END-IF.
           MOVE LK-NEXT TO WS-NEXT-PGM.
           EVALUATE WS-MODE
               WHEN '1'
                   EXEC CICS XCTL PROGRAM(WS-FIRST-PGM)
                       COMMAREA(DFHCOMMAREA) END-EXEC
               WHEN '2'
                   EXEC CICS XCTL PROGRAM(WS-MENU-PGM)
                       COMMAREA(DFHCOMMAREA) END-EXEC
               WHEN OTHER
                   EXEC CICS XCTL PROGRAM(WS-NEXT-PGM)
                       COMMAREA(DFHCOMMAREA) END-EXEC
           END-EVALUATE.
       PROGRAM-ID. PROGB.
       ...
       LINKAGE SECTION.
       01  DFHCOMMAREA.
           05  LK-PAD           PIC X(20).
           05  LK-TGT           PIC X(8).
           05  LK-TAIL          PIC X(72).
       PROCEDURE DIVISION.
       MAIN-PARA.
           EXEC CICS XCTL PROGRAM(LK-TGT) END-EXEC.

In cobble's node names the three sites are MAIN-PARA/2.when.0.0, MAIN-PARA/2.when.1.0 and MAIN-PARA/2.when.2.0. The operands are WS-FIRST-PGM@working_storage[0:8], WS-MENU-PGM@working_storage[8:16] and WS-NEXT-PGM@working_storage[16:24].

Step by step

Each site gets its own walk, over its own operand. The three walks are shown side by side because they visit the same nodes. Each row is the set arriving at that node, which is what the walk reports for a site. Values are shown as stored: PIC X(8) pads 'PROGB' with three spaces, written ␣. The program has no loop, so the worklist visits each node once, in order.

Three forward walks over PROGA (7 steps)
  1. Entry. Each operand starts at what the program holds before any statement runs. WS-FIRST-PGM is covered by a VALUE, so it starts at that literal. The other two have no VALUE: uninitialised. An operand in LINKAGE would start as inbound-linkage.

    operandarriving
    WS-FIRST-PGM{'PROGB␣␣␣'}
    WS-MENU-PGM{} + uninitialised
    WS-NEXT-PGM{} + uninitialised
  2. IF WS-FLAG = 'Y' writes nothing. Both arms start from the same sets.

    operandarriving at each arm
    WS-FIRST-PGM{'PROGB␣␣␣'}
    WS-MENU-PGM{} + uninitialised
    WS-NEXT-PGM{} + uninitialised
  3. Each arm moves a literal into all eight bytes of WS-MENU-PGM. The write is exact, not weak, covers the whole operand, and its values are complete: it replaces. The then arm leaves {'PROGB␣␣␣'}, the else arm {'PROGC␣␣␣'}. The uninitialised doubt is gone on both: the write killed it.

    operandleaving thenleaving else
    WS-FIRST-PGM{'PROGB␣␣␣'}{'PROGB␣␣␣'}
    WS-MENU-PGM{'PROGB␣␣␣'}{'PROGC␣␣␣'}
    WS-NEXT-PGM{} + uninitialised{} + uninitialised
  4. The arms meet at MOVE LK-NEXT TO WS-NEXT-PGM (MAIN-PARA/1), so its arriving set is the union of both. Then the move: it covers all of WS-NEXT-PGM, but it copies rather than storing a literal, so it does not replace. Its source is in LINKAGE, and the write carries inbound-linkage, which is kept.

    operandarrivingleaving
    WS-FIRST-PGM{'PROGB␣␣␣'}{'PROGB␣␣␣'}
    WS-MENU-PGM{'PROGB␣␣␣', 'PROGC␣␣␣'}{'PROGB␣␣␣', 'PROGC␣␣␣'}
    WS-NEXT-PGM{} + uninitialised{} + inbound-linkage
  5. Line 24: XCTL PROGRAM(WS-FIRST-PGM). Its walk arrives with {'PROGB␣␣␣'} and no doubt. Trimmed, that spells PROGB. Resolved to PROGB. The XCTL writes nothing in PROGA and never returns, so it changes none of the three sets.

    line 24 → PROGB
  6. Line 27: XCTL PROGRAM(WS-MENU-PGM). Arrives with {'PROGB␣␣␣', 'PROGC␣␣␣'} and no doubt. Resolved to PROGB and PROGC.

    line 24 → PROGBline 27 → PROGB, PROGC
  7. Line 30: XCTL PROGRAM(WS-NEXT-PGM). Arrives with {} + inbound-linkage. That doubt is not uninitialised, so the site is unresolved: a caller supplies the name. The site goes to the cross-program walk later.

    line 24 → PROGBline 27 → PROGB, PROGCline 30 unresolved: inbound-linkage
What each site is charged, and PROGA's entry row (3 steps)
  1. The entry rows of every member, on the commarea slot. Their union is what an unresolved commarea site is charged. PROGA's own row is in it too, because PROGA reads LK-NEXT (bytes [0, 8)) to pick its target.

    memberentry, commarea
    PROGA[0, 8)
    PROGB[20, 28)
    PROGC[40, 48)
    PROGD[60, 68)
    union[0, 8) [20, 28) [40, 48) [60, 68) = 32 bytes
  2. The charge at each site, as --explain PROGA reports it. A resolved site pays only for the programs it reaches. The unresolved one pays the union, including PROGD, which nothing can reach from here.

    siteresolutioncalleescharge
    line 24resolvedPROGB8 bytes
    line 27resolvedPROGB, PROGC16 bytes
    line 30unresolvednone32 bytes
  3. PROGA's entry row across rounds. In round 1 no row is known yet, so only its own target read counts: 8 bytes. In round 2 the charges arrive. The 32 bytes come from line 30 alone; lines 24 and 27 add nothing it does not already hold.

    roundPROGA on entry
    1[0, 8), 8 bytes
    2[0, 8) [20, 28) [40, 48) [60, 68), 32 bytes

The --sites file for the same run holds one row per dynamic site, with the operand, the result, the reasons, and every write in the program that touches the operand. The VALUE clause is not a write, so line 24 lists none.

{"member":"PROGA","resolved":true,"place":"MAIN-PARA line 24","transfer":"XCTL",
 "operand":"WS-FIRST-PGM@working_storage[0:8]","callees":["PROGB"],"reasons":[],
 "definitions":[]}
{"member":"PROGA","resolved":true,"place":"MAIN-PARA line 27","transfer":"XCTL",
 "operand":"WS-MENU-PGM@working_storage[8:16]","callees":["PROGB","PROGC"],"reasons":[],
 "definitions":[
  {"place":"MAIN-PARA line 17","kind":"literal","values":["PROGB   "],"sources":[],"reasons":[]},
  {"place":"MAIN-PARA line 19","kind":"literal","values":["PROGC   "],"sources":[],"reasons":[]}]}
{"member":"PROGA","resolved":false,"place":"MAIN-PARA line 30","transfer":"XCTL",
 "operand":"WS-NEXT-PGM@working_storage[16:24]","callees":[],"reasons":["inbound-linkage"],
 "definitions":[
  {"place":"MAIN-PARA line 21","kind":"copy","values":[],"sources":["LK-NEXT@linkage[0:8]"],
   "reasons":["inbound-linkage"]}]}

Other writes, one program each

Small programs, each ending in XCTL PROGRAM(WS-PGM) COMMAREA(WS-AREA) unless noted, run through the same stage. The reasons column is the site's; for the copy, the write's own reasons say something different, which is explained under the rule.

what reaches the operandresolvedreasons
IF ... MOVE 'PROGB' TO WS-PGM END-IF, no ELSE, no VALUEyes, PROGBuninitialised
MOVE 'PROGB' TO WS-OTHER, MOVE WS-OTHER TO WS-PGMnonot-a-literal (write: copy, uninitialised)
MOVE 'PROGB' TO WS-PGM, then LINK PROGRAM('PROGC') COMMAREA(WS-PGM)nofrom-callee
EXEC CICS RETRIEVE INTO(WS-PGM)nonot-a-literal
RETURN TRANSID(WS-TRAN), VALUE 'TRNB', CSD maps TRNB to PROGByes, PROGBnone
START TRANSID(WS-TRAN) FROM(WS-AREA), same VALUE and CSDyes, PROGBnone
RETURN TRANSID(WS-TRAN), VALUE 'ZZZZ', not in the CSDyes, nothingtransaction-not-in-csd
either TRANSID case, run without --confignono-csd

Why literals settle it

Steps 3 and 4 of the first walk decided line 27. Here is the part of PROGA's graph they covered, with the set of WS-MENU-PGM, line 27's operand, on each edge:

flowchart TD
    E["entry"] -->|"{} + uninitialised"| I["line 16
IF WS-FLAG = 'Y'"] I -->|then| T["line 17
MOVE 'PROGB'"] I -->|else| F["line 19
MOVE 'PROGC'"] T -->|"{'PROGB␣␣␣'}"| J["line 21
MOVE LK-NEXT TO WS-NEXT-PGM"] F -->|"{'PROGC␣␣␣'}"| J J -->|"{'PROGB␣␣␣', 'PROGC␣␣␣'}"| V["line 22
EVALUATE WS-MODE"] V --> S24["line 24 site"] V --> S27["line 27 site
resolved: PROGB, PROGC"] V --> S30["line 30 site"]

In step 3 each arm overwrites all eight bytes with a literal, so it replaces what arrived, the uninitialised doubt included. In step 4 the arms meet at line 21, so the set is their union; line 21 writes WS-NEXT-PGM, not WS-MENU-PGM. Every value reaching line 27 is a literal, so the site is resolved. Line 21's copy is also why step 7 leaves line 30 unresolved: it gives WS-NEXT-PGM inbound-linkage.

That is the whole test. If every value reaching a site is a literal this program wrote, the site is resolved here. If a copy reaches it, the name may have come from a caller, a callee or a file, which this program alone cannot see, and the site goes to the cross-program walk.

One doubt is let through: uninitialised. In the fragment below there is no ELSE, so the uninitialised path reaches the site. That storage belongs to this program, and no other program can have put a name in it. The site can reach only the names its literals spell, as cobble reports for it:

       01  WS-PGM               PIC X(8).
       ...
           IF WS-FLAG = 'Y'
               MOVE 'PROGB' TO WS-PGM
           END-IF.
           EXEC CICS XCTL PROGRAM(WS-PGM)
               COMMAREA(DFHCOMMAREA) END-EXEC.
arriving: {'PROGB␣␣␣'} + uninitialisedresolved → PROGBreasons: uninitialisedcharged 8 bytes

What one unresolved site costs

Area need works out which bytes of a handed-over area each program must keep alive for the fixpoint: at a call site that hands over an area, whatever the callee needs on entry. Step 2 of the second walk charged lines 24 and 27 only their callees' rows. Line 30's dashed arrow has no known end, so the only safe charge is the union of every member's entry row for the commarea, PROGD's included, though nothing reaches PROGD. Step 3 then shows all 32 bytes in PROGA's own entry row, from line 30 alone.

Without local resolution, all three of PROGA's arrows would be dashed, and every program with even one dashed arrow pays the union. So: for each dynamic site, can its solid arrows be drawn from its own program alone? If yes, charge only the programs at their ends.

Where this shows up in reverse engineering

On a large CICS corpus (about 3,800 programs and 19,000 jumps), the only goal was a call graph with the dynamic targets filled in. Those targets came in three shapes, and PROGA has one of each; the line numbers are those of the full listing.

shapein PROGAsiteneeds other programs?
a literal VALUE, never writtenWS-FIRST-PGM ... VALUE 'PROGB' (line 5)line 24no
a MOVE 'LITERAL' on each branchMOVE 'PROGB' / MOVE 'PROGC' TO WS-MENU-PGM (lines 17, 19)line 27no
a name read out of DFHCOMMAREAMOVE LK-NEXT TO WS-NEXT-PGM (line 21)line 30yes

Only the third row, about 5% of the corpus's sites, needs the cross-program walk: a caller put the next program's name in the commarea. The area-need stage was charging every dynamic site as if it were the third row. Answering rows 1 and 2 locally stops that, so a program's row reflects the programs it really reaches.

The rule

initial(o) = inbound-linkage, if o is in LINKAGE
           = the VALUE bytes covering o, if a VALUE covers it
           = uninitialised, otherwise
in(entry) = initial(o);  in(n) = ⋃ out(p) for every predecessor p
out(n) = in(n) after each write w at n:
  w misses o's bytes                                    → unchanged
  w replaces o (strong, exact, covers o, literal values) → w's values, sliced to o
  otherwise                                             → w's reasons − {uninitialised}, or not-a-literal if none remain

resolved(site) ⇔ o is one exact extent ∧ reasons(in(site)) ⊆ {uninitialised}
callees(site)  = program names among values(in(site)); none → resolved to nothing, no-program-name

transaction site: literal TRANSID → resolved to it; then through the CSD
  no CSD              → unresolved, no-csd
  id not in the CSD → that id reaches nothing; transaction-not-in-csd; still resolved

charge(site, slot) = ⋃ entry(c)[slot] for c in callees(site), if resolved
                       = ⋃ entry(m)[slot] for every member m, otherwise

Each line of the box has already run once in the walks above. initial(o) is step 1 of the first walk: WS-FIRST-PGM starts at its VALUE, the other two at uninitialised. in(n) as a union is step 4, where the two arms meet at line 21. The replaces line is step 3 (lines 17 and 19); the otherwise line is step 4's copy, which leaves WS-NEXT-PGM with inbound-linkage. resolved(site) is steps 5 to 7, and charge is step 2 of the second walk.

The last line has two edges the walks do not reach. Two one-site variants of PROGA, run through cobble with PROGB, PROGC and PROGD beside them, show them; each ends in XCTL PROGRAM(WS-PGM) COMMAREA(DFHCOMMAREA), and DFHCOMMAREA is PROGA's 100 bytes.

WS-PGMresolved toreasonscharge
VALUE 'PROGX', not in the corpusPROGXnone[0, 100), every byte
VALUE SPACESnothingno-program-name0 bytes

Row 1 is a callee outside the corpus, which is charged every byte of the area, as is one that does not receive that slot. Row 2 is a site resolved to nothing, which is charged nothing.

Why uninitialised is accepted, and a copy is not

Uninitialised means one thing here: on some path, no write reached the operand and no VALUE covers it. In the IF-without-ELSE fragment under why literals settle it, that path is the one where WS-FLAG is not 'Y'. That storage is this program's own working storage. No caller, callee or file put anything in it. So the only names the site can reach are the literals on the other paths, there PROGB. If that path really runs, the operand holds whatever the storage started as, not a name some other program chose.

A copy is different. This fragment is plainly PROGB:

       01  WS-PGM               PIC X(8).
       01  WS-OTHER             PIC X(8).
       ...
           MOVE 'PROGB' TO WS-OTHER.
           MOVE WS-OTHER TO WS-PGM.
           EXEC CICS XCTL PROGRAM(WS-PGM)
               COMMAREA(DFHCOMMAREA) END-EXEC.

The writes are computed once per program, without a state, so the second MOVE above looks up WS-OTHER in an empty state and finds it "unwritten". That is a fact about the empty state, not about the program: WS-OTHER holds 'PROGB' by then here, and elsewhere might hold a name read from a file. The walk does not follow the copy to find out.

cobble's --sites row for it is below; its line numbers are those of the full variant program. The copy's own reasons still show uninitialised, but the walk drops that reason, so the write is not-a-literal and so is the site. Accepting it would resolve the site to nothing and charge nothing, while the combine, following the copy, links it to PROGB.

{"member":"PROGA","resolved":false,"place":"MAIN-PARA line 15","transfer":"XCTL",
 "operand":"WS-PGM@working_storage[0:8]","callees":[],"reasons":["not-a-literal"],
 "definitions":[
  {"place":"MAIN-PARA line 14","kind":"copy","values":[],
   "sources":["WS-OTHER@working_storage[8:16]"],"reasons":["uninitialised"]}]}

A copy whose source carries a real reason keeps it. The table lists them with where on this page each shows up.

what writes the operandreason keptseen on this page
a copy out of LINKAGEinbound-linkagePROGA line 21, the third --sites row
a LINK or CALL BY REFERENCE handing the operand overfrom-calleerow 3 of the "other writes" table
a READ into itfrom-filenot shown
a RETRIEVE INTO, modelled as CICS state with no literalnot-a-literalrow 4 of the "other writes" table

What goes wrong

Each case below is a small variant of PROGA, run through cobble beside PROGB, PROGC and PROGD. Where PROGA itself reads none of its commarea, the union an unresolved site pays is their three rows, [20, 28) [40, 48) [60, 68): 24 bytes.

The two CSD cases, as one site: RETURN TRANSID(WS-TRAN) COMMAREA(DFHCOMMAREA), with a CSD whose only line is DEFINE TRANSACTION(TRNB) PROGRAM(PROGB) GROUP(TEST).

WS-TRANrunresolved toreasonscharge
VALUE 'TRNB'with --configPROGBnone8 bytes
VALUE 'TRNB'withoutunresolvedno-csd24 bytes, the union
VALUE 'ZZZZ'with --confignothingtransaction-not-in-csd0 bytes

In cobble

The resolution runs inside cobble-dataflow-area-need, once for each member before the rounds. One graph is built per program, and each dynamic site gets its own walk over it, as PROGA's three sites do in the first walk above. The answers split into resolved sites and unresolved ones, the solid and dashed arrows of the graph under the example. A resolved site is charged its callees' rows; an unresolved one, the union of every member's entry need. Transaction ids from RETURN TRANSID and START are mapped to programs through the CSD files of the corpus config named by --config.

PROGA's --sites rows and charges above came from these commands, run over PROGA to PROGD with the bridge environment sourced and the corpus config listing the four programs:

cobble-parse-corpus --config corpus.toml
cobble-extents --asg-dir asg --out extents.jsonl
cobble-dataflow-area-need --asg-dir asg --extents extents.jsonl \
    --out need.jsonl --sites sites.jsonl --explain PROGA

PROGA's row in need.jsonl is below. entry holds the four ranges of the second walk's last step as flat start, end pairs. sites lists only the two resolved sites, by node name, with their callees; line 30 is not there.

{"member":"PROGA","receives":[["commarea",0]],
 "entry":[[["commarea",0],[0,8,20,28,40,48,60,68]]],"back":[],
 "sites":[["MAIN-PARA/2.when.0.0",["PROGB"]],["MAIN-PARA/2.when.1.0",["PROGB","PROGC"]]]}

--sites writes one row per dynamic site, like the three under step by step, with each write that touches the operand marked as a literal, a copy or other. Sites that a finished fixpoint and combine resolved are added from --fixpoint-dir and --combined-dir. The command is documented under cobble-dataflow-area-need in the field dataflow reference.

Next: rounds across programs, and walking only what changed.