The block the checker stops reading¶
Nobody writes a two-hand control from contacts twice. Certified safety libraries ship it as a block, you drop the block in, wire the two palm buttons, and the anti-tie-down logic is somebody else's tested code. That is the shape safety logic actually arrives in.
Update: released ESBMC 8.6.0
The runs on this page were recorded against the builds named beside them. For comparison, in the paper's evaluation (evidence/variants_302.csv in the release):
g_two_hand_fb: the released ESBMC 8.6.0 passes the ingestion gate and returns SAFE; 8.4.0 fails the gate.- The expected verdict is unchanged in every case.
So here is lesson 7.2's press again, with the identical Type II body moved into a function block. Same machine, same properties, same expected answers. The ladder twin verifies, and has since 7.2.
The block¶
BothOff := NOT LH AND NOT RH;
Armed := (Armed OR BothOff) AND NOT Stroke;
Stroke := (Stroke OR Armed) AND LH AND RH;
BothOff and Armed are the block's own locals, which is what makes the instance
stateful and the anti-tie-down work.
| file | what it is | size | sha256 |
|---|---|---|---|
program.xml |
ladder program, PLCopen XML | 3.7 KB | d2d99912ea21 |
type_i.xml |
ladder program, PLCopen XML | 3.6 KB | fda94b735538 |
props.yaml |
property file | 0.2 KB | 01c905820f28 |
Both builds fail the gate¶
Recorded 2026-09-04T23:40:49Z. Every build ran the same command.
cp benchmarks/manufacturing/g_two_hand_fb/program.xml benchmarks/manufacturing/g_two_hand_fb/program.ld
esbmc benchmarks/manufacturing/g_two_hand_fb/program.ld --ld-props benchmarks/manufacturing/g_two_hand_fb/props.yaml --k-induction --ld-scan-watchdog --ld-scan-budget 8 --timeout 20s
Recorded 2026-09-04T23:40:49Z.
| build | ESBMC | verdict | expected | outcome | ingestion gate | cpu |
|---|---|---|---|---|---|---|
| v8.4 | 8.4.0 | SAFE |
SAFE |
correct | fail | 0.015 s |
v8.4 found its solution by the inductive step (k = 2).
What the front end encoded. The scan body as the solver receives it, branch guards kept. An assignment-only view hides the guards and makes a guarded accumulator read as a constant.
SAFE on both, and the gate fails on both, with Stroke never driven. Here is the
whole scan body, from either build:
The block is not in it. Two proofs about a press with no logic in it at all.
And the correct variant is condemned¶
Put the Type I body in the block instead, Stroke := LH AND RH, which is the program
lesson 7.2 proves safe. This time the gate passes.
Predict the verdict
The block body is now the plain conjunction, and its ladder twin satisfies the property. What happens here?
Answer
| build | verdict | expected | ingestion gate |
|---|---|---|---|
| v8.4 | VIOLATION |
SAFE |
pass |
Both builds refute a property that holds. The encoding says why:
ASSIGN THC0__LH=NONDET(_Bool);
ASSIGN THC0__RH=NONDET(_Bool);
ASSIGN THC0__Stroke=THC0__LH;
ASSIGN Stroke=THC0__Stroke;
Two separate defects, in one four-line block.
The block's inputs are havocked rather than bound. THC0__LH is a fresh unconstrained
value, not the program's LH, so the wiring from the call site is gone. Then the body
itself loses an operand: Stroke := LH AND RH arrives as Stroke := LH. We checked the
direction, and it keeps the first operand rather than a particular name: B AND A
becomes B, and A OR B becomes A. Arithmetic survives, so this looks specific to
boolean operators.
What this does not mean¶
It would be easy to read that as "function blocks do not work", and it is narrower than
that. The eight g_tank_* benchmarks in Part 5 are function-block programs, they pass
their gates, and their verdicts are right, because their bombs are constant assignments
sitting inside the block bodies rather than functions of the block inputs, which means
an unbound parameter never changes the answer they give. Going back and reading their recorded encoding, the same
NONDET on every formal parameter is sitting there and has been all along.
That is the uncomfortable part. This is not a new regression: it is behavior the corpus has been carrying since the tank programs were imported, invisible the whole time because nothing in it had ever asked a question whose answer depended on what a block does with the arguments handed to it. Two rungs of ISO 13851 logic asked. The answer came back about a different program.
The discriminator from 7.2 still runs here, for completeness, and inherits the same problem.
Recorded 2026-08-28T18:37:10Z. Every build ran the same command.
cp benchmarks/manufacturing/g_two_hand_fb/program.xml benchmarks/manufacturing/g_two_hand_fb/program.ld
esbmc benchmarks/manufacturing/g_two_hand_fb/program.ld --ld-props benchmarks/manufacturing/g_two_hand_fb/tiedown_check.props.yaml --k-induction --ld-scan-watchdog --ld-scan-budget 8 --timeout 30s
Recorded 2026-08-28T18:37:10Z.
cp benchmarks/manufacturing/g_two_hand_fb/program.xml benchmarks/manufacturing/g_two_hand_fb/program.ld
esbmc benchmarks/manufacturing/g_two_hand_fb/program.ld --ld-props benchmarks/manufacturing/g_two_hand_fb/tiedown_check.props.yaml --k-induction --ld-scan-watchdog --ld-scan-budget 8 --timeout 30s
| build | ESBMC | verdict | expected | outcome | ingestion gate | cpu |
|---|---|---|---|---|---|---|
| master | 8.4.0 0064c4ac |
VIOLATION |
VIOLATION |
correct | fail | 0.011 s |
What the front end encoded. The scan body as the solver receives it, branch guards kept. An assignment-only view hides the guards and makes a guarded accumulator read as a constant.
Counterexample. ESBMC's own output. A trace long enough to need it is elided in the middle, and the omission is marked and counted where it happens.
State 1 thread 0
----------------------------------------------------
LH = 1
State 2 thread 0
----------------------------------------------------
RH = 1
State 3 file benchmarks/manufacturing/g_two_hand_fb/program.ld thread 0
----------------------------------------------------
Violated property:
file benchmarks/manufacturing/g_two_hand_fb/program.ld
P1
!(LH && RH) || Stroke
** Results:
benchmarks/manufacturing/g_two_hand_fb/program.ld
FAILED [global.assertion.1] line 0 P1
** 1 of 1 properties failed
Recorded 2026-08-28T18:37:10Z. Every build ran the same command.
cp benchmarks/manufacturing/g_two_hand_fb/type_i.xml benchmarks/manufacturing/g_two_hand_fb/type_i.ld
esbmc benchmarks/manufacturing/g_two_hand_fb/type_i.ld --ld-props benchmarks/manufacturing/g_two_hand_fb/tiedown_check.props.yaml --k-induction --ld-scan-watchdog --ld-scan-budget 8 --timeout 30s
Recorded 2026-08-28T18:37:10Z.
| build | ESBMC | verdict | expected | outcome | ingestion gate | cpu |
|---|---|---|---|---|---|---|
| master | 8.4.0 0064c4ac |
VIOLATION |
SAFE |
wrong | pass | 0.012 s |
What the front end encoded. The scan body as the solver receives it, branch guards kept. An assignment-only view hides the guards and makes a guarded accumulator read as a constant.
Counterexample. ESBMC's own output. A trace long enough to need it is elided in the middle, and the omission is marked and counted where it happens.
State 1 thread 0
----------------------------------------------------
LH = 1
State 2 thread 0
----------------------------------------------------
RH = 1
State 3 thread 0
----------------------------------------------------
THC0__LH = 0
State 4 thread 0
----------------------------------------------------
THC0__Stroke = 0
State 5 thread 0
----------------------------------------------------
Stroke = 0
State 6 file benchmarks/manufacturing/g_two_hand_fb/type_i.ld thread 0
----------------------------------------------------
Violated property:
file benchmarks/manufacturing/g_two_hand_fb/type_i.ld
P1
!(LH && RH) || Stroke
** Results:
benchmarks/manufacturing/g_two_hand_fb/type_i.ld
FAILED [global.assertion.1] line 0 P1
** 1 of 1 properties failed
Where this leaves the part¶
Eight machines in this part carry a discriminator that separates a correct program from a plausible wrong one. This one cannot, because the checker is not reading the program. That makes it the odd lesson out here and a natural neighbour of Part 6, where the same thing happens to function block diagrams and sequential function charts.
The suite shipped it anyway, with the ladder twin as its expected verdict. A gap you can
point at is worth more than a benchmark quietly dropped for answering wrong. The gap was
fixed upstream in esbmc#8034, and the task
is now marked validated.