Exact field classes across programs
Read first: From byte provenance to a field graph
A caller and a callee often declare one datum under two names, and only the bytes say so. This page finds such pairs from the field graph's byte placements alone, and joins them into classes.
The test
Field A (program P) and field B (program Q, not P) are one class when:
- they are the same size;
- byte
iof A lands on byteiof B, for everyi; - no byte of A lands anywhere else in B.
Classes are the connected components of the pairs that pass. Copies inside one program never join: a program copying a field to a work area is not evidence of a shared datum.
The example
Program R hands a commarea to program S. S receives it into a linkage table of 1-byte occurrences T(1) to T(4) and moves bytes out into its own fields.
flowchart LR
subgraph R
A["CA.A (2)"]
C["CA.C (2)"]
E["CA.E (4)"]
H["CA.H (2)"]
end
subgraph S
T1["T(1)"]
T2["T(2)"]
T3["T(3)"]
T4["T(4)"]
B["WS.B (2)"]
D["WS.D (2)"]
F["WS.F (2)"]
G["WS.G (2)"]
J["WS.J (2)"]
K["WS.K (2)"]
end
A -->|"byte 0"| T1 -->|"to byte 0"| B
A -->|"byte 1"| T2 -->|"to byte 1"| B
C -->|"byte 0"| T3 -->|"to byte 1"| D
C -->|"byte 1"| T4 -->|"to byte 0"| D
E -->|"bytes 0-1"| F
B --> G
H --> J
K -.->|"no placement"| J
Relays
The table occurrences T(1) to T(4) are not fields anyone would call a datum. They are just how S declared the bytes it received. So they relay: a byte that lands on a relay keeps going to wherever the relay sends it, and only the far end counts.
- Relays are subscripted fields and dataset slots.
- Following a byte through relays is reachability in a graph of byte-to-byte moves.
Each pair, judged
| pair | bytes land | verdict |
|---|---|---|
| R:CA.A, S:WS.B | 0→0, 1→1, via T(1), T(2) | joined |
| R:CA.C, S:WS.D | 0→1, 1→0 | no: out of place |
| R:CA.E, S:WS.F | 0→0, 1→1, but sizes 4 and 2 | no: sizes differ |
| S:WS.B, S:WS.G | in place | no: same program |
| R:CA.H, S:WS.J | in place | no: J is reached by an edge with no placement |
The result is one class: {R:CA.A, S:WS.B}.
The last row is the strict rule. K → J has no known placement, so nobody can say which of J's bytes it overwrote. A field reached by such an edge neither relays nor joins.
One kind of edge without placement does not count. An edge is rebuilt when every statement behind it built the target's value in place, by arithmetic or STRING: ADD Y TO X gives Y → X no placement, but X is not a copy of anything, so it keeps its class.
Through a file
Dataset slots relay the same way, over exact dataset edges. W writes an 11-byte ID; R reads it back as one 11-byte field; the record is cut into slots [0:5] and [5:11]:
| edge | alignment |
|---|---|
| W:R.ID → DS[0:5] | bytes 0–4 to 0–4 |
| W:R.ID → DS[5:11] | bytes 5–10 to 0–5 |
| DS[0:5] → R:R.ID | 0–4 to 0–4 |
| DS[5:11] → R:R.ID | 0–5 to 5–10 |
| DS[0:5] → R:R.GRP | 0–4 to 0–4 |
| DS[0:5] → Q:R.ID | possible, no placement |
| W:R.ID → Q:R.ID | 0–10 to 0–10 |
The class is {Q:R.ID, R:R.ID, W:R.ID}. R:R.GRP is five bytes, so it does not join an 11-byte field. The possible edge into Q is ignored, and does not stop Q joining through its direct edge.
The rule
onward(b) = {b} if b is not on a relay; else b and everything reachable from it
landings(a, i) = { onward(m) | m ∈ moves((a, i)) } restricted to non-relay fields
unplaced = targets of a field-to-field edge with no placement that is not rebuilt
exact(a, b) = program(a) ≠ program(b) ∧ size(a) = size(b)
∧ ∀ i: { offsets of b in landings(a, i) } = { i }
classes = connected components of exact pairs, unplaced fields left out
Loosening it
Each condition costs recall. On CardDemo the strict rule gives 111 classes of 510 fields. Ignoring the unplaced rule:
| run | classes | fields | what is added |
|---|---|---|---|
| strict | 111 | 510 | |
| ignore the unplaced rule | 113 | 583 | 73 fields, 70 of them commarea fields |
The other three loosenings were measured on 2026-10-26, when the strict rule gave 90 classes of 394 fields. They are not offered, so they have not been measured since:
| loosened | classes | fields | precision, by sample |
|---|---|---|---|
| allow joins in one program | 441 | 1,811 | mixed: one value fanned out to 42 fields becomes one class |
| accept 75% of bytes in place | 90 | 394 | no effect |
| a field inside a bigger one | 45 | 438 | collapses: chains merge distinct fields |
Only ignoring the unplaced rule is offered, as --join-unplaced.
On CardDemo
Through a file. The card number in the cross-reference record is one class of 14 fields over ten programs, though no program writes the file:
All 14 are 16 bytes: CARD-XREF-RECORD.XREF-CARD-NUM in nine of the programs, and the file's own record field FD-XREF-CARD-NUM in the five batch readers that declare one (CBACT03C, CBACT04C, CBSTM03B, CBTRN02C, CBTRN03C). The batch readers and COTRN02C meet in the cross-reference file's slot [0:16]. COACTUPC, COACTVWC and COBIL00C read its alternate-index path, a dataset of its own, and meet in that dataset's slot [0:16]. COTRN02C reads both, which joins the two groups. Two readers of the same bytes stay out:
CBSTM03B's 1,000-byte I/O area. Its bytes 0–15 do land on slot [0:16], but a 1,000-byte field is not the same size as a 16-byte one.CBSTM03Bjoins through itsFD-XREF-CARD-NUMinstead.CBTRN01C's read never resolved to the dataset (no JCL step runs it), so it has a slot of its own.
Through a commarea, and the strict rule. CDEMO-USER-ID in the shared commarea copybook forms two classes under the strict rule, of three programs and two, and two with --join-unplaced, of six and three:
| run | CDEMO-USER-ID's classes |
|---|---|
| strict | COCRDLIC, COCRDSLC, COCRDUPC COTRTLIC, COTRTUPC |
| --join-unplaced | COACTUPC, COACTVWC, COCRDLIC, COCRDSLC, COCRDUPC, COMEN01C COADM01C, COTRTLIC, COTRTUPC |
The flag changes only one thing, the unplaced rule, so in the four added programs the commarea field is also reached by a move with no known placement, and the strict rule leaves it out. CDEMO-ACCT-ID forms only the COTRTLIC, COTRTUPC class under the strict rule, and with the flag the same two classes as CDEMO-USER-ID, in the same programs.