The properties we prove¶
A benchmark is a program and a question. The rest of this site is full of programs. This page is about the questions, because a verdict is only ever an answer to the one that was actually asked, and every failure recorded here has been a failure of the question rather than of the solver.
Each task in the catalog carries a property file beside its program. That file is the whole specification: what must never happen, written precisely enough for a checker and plainly enough for a reviewer to argue with.
format_version: "0.1"
# Applies to both variants (clean.st -> SAFE, bomb.st -> VIOLATION of P1 via P2 witness).
properties:
- id: P1
kind: mutual_exclusion
variables: [Motor_A, Motor_B]
justification: "Forward/reverse contactors must never be energised together (contactor short / mechanical damage)."
- id: P2
kind: reachability
expression: "Motor_A && Motor_B"
justification: "Trigger synthesis: a witness recovers the secret-knock input combination that violates the interlock."
Three fields are required of every property. A stable id, because published results
cite it and a renumbered property invalidates them. A kind, drawn from a closed
vocabulary. And a justification in prose, which is the field a control engineer reads
first and no tool reads at all. schema/properties.schema.json is the machine-readable
form, and validate checks every file in the tree against it on every commit.
What the catalog asks¶
| kind | properties | tasks carrying one |
|---|---|---|
invariant |
98 | 87 |
reachability |
21 | 21 |
mutual_exclusion |
16 | 16 |
termination |
14 | 14 |
| total | 149 | 117 |
Counted from the property files when the site builds. A task appears in two rows whenever it pairs a safety claim with a witness, which is why the task column adds up to more than the 117 tasks in the catalog. Thirteen further properties live in the tree without belonging to a task: they exist to teach a lesson, and the lessons that use them say so.
invariant¶
Does this hold in every reachable state of the scan model?
The workhorse, and ninety-eight of the 149 properties here. The expression is a formula over the program's own variables, and the encoder splices it into the body of the scan loop, so it is checked once per scan rather than once per program.
- id: P1
kind: invariant
expression: "!(feed_valve && overpressure)"
justification: "The feed valve must never open while the reactor is over-pressure."
A violation gives you a counterexample: the input sequence, scan by scan, that reaches the
state you said could not happen. A SAFE gives you rather less than it appears to, and
what it gives you depends on the mode. Under --incremental-bmc --unwind N it means no
counterexample exists within N scans. Under --k-induction it means the property holds
for every scan, which is the claim most people think they are reading either way.
mutual_exclusion¶
Are these outputs ever true together?
An invariant of one fixed shape, spelled over a variable list instead of an expression:
- id: P1
kind: mutual_exclusion
variables: [Motor_A, Motor_B]
justification: "Forward/reverse contactors must never be energised together."
It becomes assert(!(Motor_A && Motor_B)), so you could write it as an invariant and get
the same verdict. Naming the kind buys something the expression does not: a tool can
enumerate every exclusion pair in the suite without parsing anybody's expression syntax,
which is worth having on a checker that rejects one of the two spellings of AND. Lesson 1.3
verifies exactly this pair on a reversing starter, and shows what the two spellings cost.
reachability¶
Can this state be reached, and by what input?
This one inverts. The property is written so that reaching the state fails it:
if (expr) assert(false) on the ladder route, assert(!expr) through C. A VIOLATION is
therefore the result you want, and ESBMC's counterexample is the answer.
- id: P2
kind: reachability
expression: "Motor_A && Motor_B"
justification: "Recover the input combination that energises both contactors, if one exists."
That is trigger synthesis. Point it at a program you suspect carries a logic-level bomb,
ask whether the forbidden state is reachable, and the trace names the input combination
that arms it. Lesson 3.5 recovers
fwd AND rev AND maint from a bombed interlock without being told a bomb was there. The
suite uses the kind twenty-one times, always beside the safety
property whose violation it explains.
termination¶
Does the scan complete?
The other three kinds are blind to a controller that has stopped producing output at all.
A program stuck in a WHILE loop holds its outputs in a perfectly consistent state for
ever, and every safety property you would naturally write is satisfied.
- id: P1
kind: termination
justification: "Every PLC scan cycle must complete (no non-terminating loop)."
No expression, because there is nothing to say about the variables. The fourteen termination properties in the catalog all guard against the third payload class in the logic-level bomb taxonomy: denial of control.
Neither route checks this with an assertion over your program. The ladder front end
rejects the kind outright and the scan watchdog carries it instead, instrumenting WHILE
loops in function block bodies with a budget. Through C, the unwinding assertion carries
it: a loop ESBMC cannot close within the bound fails the run. That second one is evidence
rather than proof, because a merely deep loop fails the same way, which is why the record
of every such run states the bound it used. Lesson 3.4
runs both instruments against a hung tank controller, including the flag combination that
turns a provably hanging program into a SUCCESSFUL in under a second.
Which route can express which kind¶
Two routes reach the same engine, described on How ESBMC-PLC works, and they do not accept the same vocabulary.
| kind | ladder front end, --ld-props |
through Beremiz and MatIEC to C |
|---|---|---|
invariant |
assert(expr) inside the scan loop |
__ESBMC_assert(expr) inside the harness loop |
mutual_exclusion |
assert(!(A && B && …)) |
the same conjunction over the generated variables |
absence |
assert(!expr) |
__ESBMC_assert(!(expr)) |
reachability |
if (expr) assert(false) |
__ESBMC_assert(!(expr)) |
termination |
rejected; --ld-scan-watchdog carries it |
no assertion; the unwinding assertion carries it |
assertion |
rejected | not expressible |
ERROR: Unknown property kind: 'termination' is what a rejection looks like, which is at
least a diagnostic. Compare that with an <FBD> body, which the same front end accepts
and then discards in silence.
Two kinds the vocabulary declares and nothing uses¶
| kind in the schema | properties in the catalog |
|---|---|
invariant |
98 |
mutual_exclusion |
16 |
absence |
none |
reachability |
21 |
reachable |
none |
assertion |
none |
termination |
14 |
absence asks whether a runtime error of a given subtype is unreachable: overflow,
division by zero, an array read past its end. The front end supports it, the probe
confirmed as much, and no benchmark here needs it yet, because the catalog's programs are
Boolean interlocks and small counters rather than the arithmetic where those errors live.
assertion would check an inline assertion at a named location. The schema declares it
and docs/format_spec.md describes it; no front end on either route accepts it. Writing
one today gets you a rejected file rather than a checked program.
Neither gap is hidden by the tally above, which is the point of generating it.
Write the expression in C¶
The property parser reads expressions as C, so !(A && B) is accepted and the IEC
spelling of the same formula, NOT (A AND B), is rejected. The diagnostic names the
entire expression as an undeclared variable, which is filed upstream as
esbmc/esbmc#7371.
Every expression in the catalog turns out to be Boolean, built from !, &&, || and a
single equality. The file format allows comparisons and arithmetic as well, and no
benchmark here exercises them yet. Values are the ones sampled at the top of the scan, so
an expression says something about this scan and nothing about the last one.
What none of these can say¶
The vocabulary has no temporal operators. Nothing here expresses eventually, until, or within 200 ms, and that shapes what the suite can honestly claim:
- No liveness. Lesson 2.3 proves a junction never shows two conflicting greens. A stuck detector that starves one arm for ever satisfies every property in that file.
- No real time. Timers count scans, not milliseconds. A
PTof 5 means five scans, and several timer benchmarks are markedcandidatefor exactly that reason. - No sequence beyond what the program remembers. A property is a formula over the current scan. Anything about the previous scan has to be a variable the program itself latches.
Saying so is not a hedge. A suite whose properties quietly imply a completeness they never had is worse than useless to somebody evaluating a tool against it.
Then check that the tool read your program¶
A property holds vacuously over a program that was never ingested. Every run recorded on
the ladder route therefore reports an ingestion gate beside its verdict, asking whether
the property's variables are assigned anywhere inside the scan loop.
Twenty-five of those 154 runs fail it.
Most are files the front end now rejects by name rather than emptying in silence. The
eighteen that remain still carry a verdict, one SAFE and one
VIOLATION, and neither is an answer about the program in the file.
Lesson 3.6 turns all of this into a checklist for writing your own: name a consequence rather than restating the code, add a discriminator that fails when the feature you are guarding is absent, refuse the convenient exception, and carry the bound with the answer.