How to specify an output relationally¶
Audience: engineer who knows what the plant must never do, and wants the
compiler to hold them to it.\
Prerequisites: RULE, INVARIANT and priorities from the
Introduction by Example.\
Language Reference:
§8.6a Typed relational clauses.
Most specification clauses are implications. Written as raw disjunctions they all look alike, and one crucial distinction disappears: whether a condition permits an output or forces it. The typed clauses keep that distinction — and the compiler can then tell you things it could not see before.
The four forms¶
RULE <literal> ONLY_WHEN <condition> ; (* necessary: literal → condition *)
RULE <condition> IMPLIES <literal> ; (* sufficient: condition → literal *)
RULE <output> IFF <condition> ; (* definition: both directions *)
RULE <source> EXCLUDES <source> ; (* exclusion: NOT (a AND b) *)
<literal> is one declared BOOL output, bare (Release) or negated
(NOT Release). The condition may not mention that output.
1. Say what an output requires — ONLY_WHEN¶
A two-hand press must never release with one hand:
RULE Release ONLY_WHEN (Left AND Right);
Read it as: Release may only be true when both buttons are pressed. It
does not say the press releases whenever both are held — that would be the
sufficient direction, and mixing the two up is exactly the mistake this syntax
prevents.
2. Say what forces an output — IMPLIES¶
The forbidding direction is the same clause with the sides swapped and the output negated:
RULE (Left AND NOT Right) IMPLIES NOT Release;
3. Say that an output is a condition — IFF¶
When an output is nothing but an abbreviation, say so:
FUNCTION_BLOCK Actuator
VAR_INPUT Release : BOOL; Enabled : BOOL; END_VAR
VAR_OUTPUT Press : BOOL; END_VAR
IMPLEMENTATION
RULE Press IFF (Release AND Enabled);
END_IMPLEMENTATION
END_FUNCTION_BLOCK
Press needs no separate definition: the compiler materialises it from the
clause and records the provenance as defined, not searched. If any other
clause contradicts the definition, the compile fails closed
(ns1-iff-conflict) — nothing is materialised on a contradiction.
4. Say that two outputs are mutually exclusive — EXCLUDES¶
FUNCTION_BLOCK Drive
VAR_INPUT Fwd : BOOL; Rev : BOOL; END_VAR
VAR_OUTPUT MotorFwd : BOOL; MotorRev : BOOL; END_VAR
IMPLEMENTATION
MotorFwd := Fwd AND NOT Rev;
MotorRev := Rev AND NOT Fwd;
RULE MotorFwd EXCLUDES MotorRev;
END_IMPLEMENTATION
END_FUNCTION_BLOCK
EXCLUDES normalises to NOT (a AND b) and is symmetric. It is the readable
form of a motor interlock — and unlike a comment, it is proved.
The whole example¶
FUNCTION_BLOCK TwoHandPress
VAR_INPUT
Left : BOOL;
Right : BOOL;
END_VAR
VAR_OUTPUT
Release : BOOL;
END_VAR
IMPLEMENTATION
Release := Left AND Right;
RULE Release ONLY_WHEN (Left AND Right);
RULE (Left AND NOT Right) IMPLIES NOT Release;
RULE (Right AND NOT Left) IMPLIES NOT Release;
END_IMPLEMENTATION
END_FUNCTION_BLOCK
Reading the balance¶
Compiling prints one INFO line per output:
Release: 1 ON-necessary, 0 OFF-necessary, 0 ON-sufficient, 2 OFF-sufficient
The four columns are the four roles: what must hold for the output to be on (ON-necessary), what must hold for it to be off, what forces it on, what forces it off. The line is information, not a verdict — but read it, because an output with no necessary conditions is exactly the shape that lets a synthesiser answer "always on unless forbidden" and still satisfy every clause.
defined (IFF) appears instead for an output defined by a biconditional, and
legacy RULE clauses that touch an output are counted as unclassified — a
half-migrated block cannot present a balance that looks complete and is not.
When clauses collide¶
Two clauses that cannot both hold produce an explanation:
warning: [ns1-pair-conflict] clauses at lines 10 and 11 drive 'Release' to
both values: their antecedents demand and fault can hold at the same time
A necessary clause colliding with a sufficient one reads slightly differently, because what fails is a different thing:
warning: [ns1-pair-conflict] sufficient clause (line 10) and necessary clause
(line 11) on 'Release' collide: the antecedent pressed can hold while the
necessary condition (NOT locked) fails
Both name the conditions from your block. Until 2026-09-04 they ended in a
fixed (c AND d is satisfiable) no matter what the signals were called, so an
explanation meant to point at two clauses named variables that were not in the
program.
The verdict still comes from the ordinary proof over the whole clause set — the pair analysis only explains which two clauses collide and why, which a single unsatisfiable-core message rarely does:
verification failed:
[synthesis-contract-unrealizable] no output satisfies all RULEs for the input
assignment [demand=True, fault=True]; colliding clauses: RULE #1, RULE #2
It is deliberately incomplete: conflicts among three or more clauses escape it, and the verdict fires anyway. A missing warning is therefore not a promise that the clauses agree.
Timers on the condition side — not yet¶
A clause such as
RULE HELD(Trip, T#30s) IMPLIES NOT Enable;
reads naturally, but the compiler refuses it today:
[sema-error] HELD/ELAPSED may not appear inside an INVARIANT: the timers' own
laws are not yet part of the step relation (ADR TMR-1). Mirror the condition
into a register and state the claim over that (ADR TX-1.2), or use them in
rule guards / definitions.
The reason is not the depth of the induction. This page used to say the
statement "needs k-induction, which the proof core does not do yet"; ADR TX-2.2
withdraws that. A RULE is checked as an invariant over the transition, and
HELD is already a state bit — one step would be enough. What is missing is
that the timers' own laws (reset, run, clock, threshold) are not yet in the step
relation, so the encoding would be unsound in one direction and incomplete in
the other. Until that lands, HELD/ELAPSED live where they already work — in
rule guards and definitions:
FUNCTION_BLOCK Watchdog
VAR_INPUT Trip : BOOL; END_VAR
VAR_OUTPUT Enable : BOOL; TooLong : BOOL; END_VAR
IMPLEMENTATION
TooLong := HELD(Trip, T#30s);
Enable := NOT TooLong AND NOT Trip;
RULE TooLong IMPLIES NOT Enable; (* the mirrored signal is ordinary BOOL *)
END_IMPLEMENTATION
END_FUNCTION_BLOCK
The mirror pattern keeps the timing in a definition, where it is supported, and lets the relational clause speak about an ordinary boolean. Direct temporal antecedents are tracked as future work (ADR NS-1.9 and the temporal track).
A register name means the previous value in a clause
Writing Enable as a register instead — Enable: OFF WHEN TooLong
PRIO 20; HOLD OTHERWISE; — makes the same clause fail, with a
counterexample where Enable is still on while TooLong has just
become true. That is not a bug: inside a clause a register name
denotes the value entering the scan, while a definition (:=)
denotes the value leaving it. A clause about what an output must be
after this scan therefore belongs on a defined output — or must
name the previous value explicitly with PRE.
Related¶
- §8.6a Typed relational clauses — the normative grammar, literal rules and every diagnostic code.
- Synthesis — why an unbalanced specification produces degenerate implementations.