Explainer 15 of 15

Answers with proofs

Read first: Fixpoints and worklists, Summarising a PERFORM

A model asked to read the source and answer gives prose: plausible, uncheckable, and sometimes drawn from what it remembers about a well-known program rather than from the program in front of it. This page keeps the model, but takes away its pen. It may only write rules. The answer is whatever those rules derive from the facts, and each row comes with the steps that derived it.

The example is a real question about CardDemo's transaction-add program, COTRN02C: before it writes a new transaction record to the transaction file, which fields typed on its screen does it check, and what kind of check is each? Count a check only if one of its outcomes can stop the write.

The pipeline

flowchart LR
    S["COBOL, JCL, CSD, BMS"] -->|parse and analyse| F["facts: CFG, byte flows, definitions, sinks, links"]
    Q["question in English"] --> A["model, blind to the source"]
    D["what each relation's columns mean"] --> A
    A -->|rules only| C["checker"]
    C -->|accepted| E["Soufflé"]
    F --> E
    E --> R["answer rows"]
    E -->|explain| P["a derivation per row"]

The rules the model wrote for this question split it into four parts, plus rules of the same shape for two more kinds of check, call and lookup:

relationthe question's words it encodes
write_node(w)"writes a new transaction record to the transaction file": a file sink whose file operand holds the grounded file name
tested(f, t)"fields typed on its screen": a value the map receive put in field f, carried through copies, reaches the bytes test t reads
kind_of(t, k)"what kind of check": a class test is numeric, a compare with spaces or low-values empty, with another constant value
stopping(t)"one of its outcomes can stop the write": one outcome can still reach the write, another is doomed, it cannot

The answer is 28 rows of (field, kind), for example (TRNAMTI, numeric), (TRNAMTI, value), (CARDNINI, lookup).

One row's derivation

Soufflé can print, for any row, the rule that produced it and the rows that rule used, down to facts. Here is (TRNAMTI, numeric), taken from the renamed run described below, with the names mapped back, node names shortened and the source lines added. The test is the second WHEN of the amount check:

           EVALUATE TRUE
               WHEN TRNAMTI OF COTRN2AI(1:1) NOT EQUAL '-' AND '+'
               WHEN TRNAMTI OF COTRN2AI(2:8) NOT NUMERIC        <- t
               ...
                   MOVE 'Y'     TO WS-ERR-FLG                  <- m
                   ...
                   PERFORM SEND-TRNADD-SCREEN
checks(TRNAMTI, numeric)
├─ tested(TRNAMTI, t)          t reads TRNAMTI's bytes,
│                              filled by the map receive
├─ kind_of(t, numeric)         t's atom: CLASS NUMERIC
└─ stopping(t)
   ├─ outcome(t, next)         edge t → next WHEN, TRUE
   ├─ write_node(w)            FILE sink, file 'TRANSACT'
   ├─ reach(next, w)
   ├─ outcome(t, m)            edge t → m, FALSE
   └─ doomed(m)
      ├─ write_node(w)
      ├─ not write_node(m)
      └─ not reach(m, w)

w is WRITE-TRANSACT-FILE/0. The facts hold the test as IS NUMERIC; the source's NOT shows up in which edge is TRUE, so TRUE (numeric) goes on to the next WHEN and FALSE goes to the error branch. The rows without a note have derivations of their own, folded here; the two not rows have none.

Read bottom-up, it is the argument a reviewer would make by hand. If the amount's digits are numeric, control goes on to the next WHEN, which can still reach the write. If not, it goes to the MOVE 'Y', from which nothing reaches the write: the error branch performs SEND-TRNADD-SCREEN, which ends with EXEC CICS RETURN, so that PERFORM never comes back (see a paragraph that always leaves some other way). Every step is either a fact or a rule applied to rows above it.

The two leaves marked not are different in kind. Every other leaf is a row the analysis wrote down. A not leaf is a fact that is absent: no derivation of reach(m, …) exists. Datalog reads a missing fact as false, so this leaf is only as good as the completeness of the control-flow edges.

What the proof proves

Soufflé does not make derivations up. A row is output only if a derivation exists, so the proof is real: given these facts, these rules yield (TRNAMTI, numeric). Nobody has to trust the model about that.

What the proof does not show is that stopping means "one of its outcomes can stop the write". That is the model's reading of the question, written as a rule. So the review moves. One no longer checks the answer row by row against the source. One checks a handful of definitions against the question's words, and the citation line above each definition says which words it claims to encode. The derivation then shows, for each row, exactly which definitions it depended on.

Checking the rules

For this question, four checks were run:

Where this shows up

What goes wrong

In cobble

The facts stage (cobble-experimental-datalog-facts --run RUN_DIR --out FACTS_DIR) writes every relation as a Soufflé facts file, with a schema that declares them. A question is a .dl file that includes that schema, adds its rules and asks for its outputs. Running it is a plain Soufflé call, and adding -t explain opens the prompt where explain checks("TRNAMTI", "numeric") prints the derivation above. The blind model, the checker and the four checks are an experiment around the stage, not part of it.