Looking back¶
22. What zqel is — and is not¶
It helps to know the shape of the box you are working in. zqel is a synchronous, reactive state-transition language for control logic — not a general-purpose, Turing-complete programming language. A block reads inputs, computes its next state from the current state, and writes outputs, once per scan. Computation unfolds over time (across scans), never as an unbounded loop within a scan.
The single boundary that draws the box is the Bounded-Cycle-Theorem: every valid zqel program has a statically proven, finite per-scan execution. This — not a list of banned features — is the real admission test. It is exactly why CBMC is complete on the generated code and why Z3 decides the model. Everything you have met either is, or is not, admissible by that one criterion:
- Arrays and repetition are allowed — but only at statically fixed size. An
aggregate or a
MAPover1..8is bounded repetition with a statically known trip count (§13–15). ASORT, aRULE-defined array, a recursiveFUNCTIONare all admitted because they unroll to a finite expression whose size is known now (§16–17). What is not admissible is a loop or an array whose length is a runtime value. - Unsafe operations are allowed — but only with a discharged proof. Division and a dynamic array index are partial; they compile only when the divisor / index is proven safe (§9, §14).
INThas+ - *but no division or modulo (only the real types divide, as a guarded partial operation).
These are not oversights — they are the price and the enabler of the “prove all
states” guarantee. The boundary is easiest to feel with three examples in
examples/algorithms/:
- Fibonacci (
fibonacci.zqel) fits naturally — it is a state machine: two registers,(a, b) → (b, a + b), one number per scan. (Registers have no custom init, so aseededflag setsa = 0, b = 1on the first scan;INTis 16-bit, so it wraps after F(24).) - Sorting fits at a fixed size — and §16 shows the declarative way to say
it:
SORT, aRULEspec, or a bounded recursiveisort. The earlybubble_sort5.zqelhand-unrolled a compare-swap network; its successors state the result instead. What all of them share is the fixed size — arbitrary-length sorting needs a runtime-bounded loop, which is not expressible. is_prime(n)(is_prime.zqel) fits only bounded: with no modulo or loops, compositeness is an aggregate —nis composite iffANY(a, b : a·b = n)over a bounded product range. Unbounded primality needs unbounded computation zqel deliberately cannot express.
The rule of thumb: if a construct has a statically finite execution, it fits —
whether that is a bounded state machine, a statically bounded aggregate, a synthesised network,
or a bounded recursion. If it needs runtime-unbounded data or iteration, it does
not. If you find yourself wanting a for loop whose trip count only the plant knows,
you have stepped outside what a verifiable PLC block is — and that is by design.
23. The whole language, on one page¶
- You declare the what, not the how. Outputs are defined by prioritised
rules (registers, which have memory) or by
:=(derived, combinational). The compiler synthesises the formula and prints it as ST or Rust. - Priority is explicit. The highest-priority active rule wins; the order you
wrote the rules is irrelevant; equal-priority conflicts are compile-time
errors, and dead rules are reported as warnings (or as errors in
--strictmode). - Reading time is defined. Inside a register's rules, other registers read
their old (start-of-scan) value; through a
:=definition they read their new value. All outputs update simultaneously (the_nextstaging). - Properties are proven, not tested.
INVARIANTis proven by induction over all states; timers are symbolic, so timing properties hold for all timings; a failed proof comes with a concrete counterexample. - Unsafe operations carry obligations. Division and a dynamic array index
require a proof (or a path-sensitive
IFguard) that the divisor is non-zero / the index is in range. - Repetition is bounded and static. Arrays have a compile-time size; aggregates
(
ALLANYCOUNTSUMMINMAX) and theARRAY(…)MAPemit as loops with statically known bounds, and are proven structurally (quantifier, recurrence, or witness axioms) rather than by expanding them. ASORTor apermutationRULEis synthesised into a compare-swap network and its sortedness proven; aFUNCTIONmay recurse only on a static measure, so it unrolls fully. Everything obeys the Bounded-Cycle-Theorem: a statically proven, finite per-scan execution. - Template blocks scale structure without runtime generics.
FUNCTION_BLOCK Vec<N: UINT>is a compile-time recipe: each instantiation (Vec<3>,Vec<4>) is monomorphized into a separate concrete type with concrete array bounds, its own proof, and its own generated code — the static-size invariant never bends. - Contracts scale.
ASSUMEstates caller obligations; under composition a child premise either bubbles up to the caller (wired to parent inputs) or is discharged by the parent as a proof obligation (wired to internal logic); child invariants are preserved. - Functions carry their own contracts. A
FUNCTIONwithREQUIRE/ENSUREis proved by the compiler before any call site sees it; theENSUREs become available facts at every call, and the compiler blames the call site if theREQUIREis not earned (§17). - External proofs join by contract. An
EXTERN FUNCTION … BY <artifact>lets a warrant from an external tool (exhaustive enumeration, Creusot, CBMC) flow into the model as an assumed postcondition. Every invariant that rests on it is labelled with the artifact and its warrant —auditedorproved— so nothing is silently promoted (§18). - Units and ranges are static. Dimensional analysis is checked then erased; certified ranges choose the numeric representation — all without changing behaviour.
- One source of truth. Every rule above is defined once, in the Z3 model. The ST and Rust backends are pretty-printers over it and cannot diverge from the proof or from each other.
For the normative specification of every construct above — syntax, semantics, proof obligations, and diagnostics — see the Language Reference.
24. Four mental checkpoints¶
Before you write a zqel block, ask yourself these four questions:
- What is state, and what is just a fresh calculation?
If it must remember across scans, it is a register (rules). If it is just
computed from this scan's results, it is
:=. - What happens if two conditions are true at once? Write the priorities so the conflict is explicit. Do not rely on text order.
- What must never happen?
Those are candidates for
INVARIANT. - What must the environment promise me?
Those are candidates for
ASSUME. - What can the compiler prove itself, and what needs an external warrant?
If a pure zqel
FUNCTIONcan establish the property (§17), use that. If the operation has a soundness gap zqel cannot close — overflow, a domain-specific algorithm, a Creusot/CBMC proof — declare itEXTERN … BY <artifact>(§18) and the compiler will label every invariant that rests on it with the warrant it inherits.
That is zqel: you write intent and safety requirements; a machine proves them and writes the code. Your job shifts from ”did I code it right?” to ”did I specify the right thing?” — and the compiler guarantees the rest.
Every example in the repository¶
The sections above walk through the examples that teach something in order.
This is all of them — every file under examples/, grouped by what it
carries rather than by when it was written. It is a map for when you already
know roughly what you are looking for, not a second reading of the pages
above.
Two groups repay a look before you need them. The exercises under
examples/rias/ come with their original task statement beside the model, so
you can read what the requirements said before anyone thought about proof.
And examples/algorithms/lm_step/ is the largest thing here: a numerical
kernel split into blocks that are each proven separately, with their error
envelopes composed.
Two models named in the plan for this page are not in the repository yet — the pedestrian-crossing SSFC and the badge reader. They are listed here so their absence is visible rather than silently skipped; they follow separately.
Starting points¶
The blocks the sections above are built on.
counter.zqel— A bounded up-counter. Value stays in [0, 10]; the invariant is provenconst_limit.zqel— the same counter with its bound NAMED: aCONSTis substituted before the proof, so the rule and the invariant cannot drift apartmotor.zqel— Two interlocked motors. MotorA has priority; MotorB must yield to itmotor_conflict.zqel— the symmetric interlock — it looks correct and the proof finds the cycle in which both motors startmotor_contract.zqel— the same interlock asmotor.zqel, written as aCONTRACT— same rules, different standingpressure.zqel— Pressure guard with a delayed shutdownrs_latch.zqel— a set-dominant RS latch; the priorities make the tie-break explicit instead of implying ittwo_timers.zqel— two independentHELDtimers with different durations, and why their states cannot be confused
The IEC 61131-3 standard blocks¶
Every standard function block, expressed in zqel rather than assumed. Timers carry a variable PT and report ET; the counters keep their edge state hidden, as the standard requires.
ctd.zqel— down counter — the edge state is a hiddenVAR, andQis derived, so it does not lag a scanctu.zqel— up counter, IEC-conformant: variable preset, and the load path in its standard orderctu_edge.zqel— the same counter built by composition — an edge detector instance instead of an inline previous valuectud.zqel— up/down counter; both outputs derived, so neither lagsf_trig.zqel— falling-edge detector; the memory bit is internal, not a portr_trig.zqel— rising-edge detector; the memory bit is internal, not a portrs.zqel— reset-dominant bistable — priority is what makes RESET win, and it is written downsr.zqel— set-dominant bistable — the same block with the priorities the other way roundtof.zqel— off-delay timer with variable preset and elapsed-time outputton.zqel— on-delay timer, complete: variable preset as aTIMEinput, elapsed time as an outputtp.zqel— pulse timer — the pulse starts on the rising edge and runs its preset out
Composition¶
One block built from others — and the array-of-blocks form, where N identical channels are composed and proven together.
Channel.zqel— one channel of a bank — the block an array of instances holds N ofChannelBank.zqel— an array of blocks: N identical channels composed and proven together, onceRS.zqel— a reset-dominant bistable, reused as a sub-block byStartStop.zqelStartStop.zqel— a start/stop station built from that bistable — composition at its smallest
Arrays and aggregates¶
The declarative side: what the values must satisfy, not the loop that produces them.
conditioning.zqel— eight raw ADC channels conditioned by stating what the results must satisfydecl_sort.zqel— sorting as pure constraints — the strongest declarative form, no network at allfir4.zqel— a four-tap FIR filter written as a dot product over the windowlinearize.zqel— an elementwise map: eight raw counts into engineering unitsselector.zqel— a multiplexer — pass channelselthrough, stated as a property of the resultsensor_monitor.zqel— eight temperature sensors monitored without a loop and without an index variablesort5.zqel— sorting stated as a property rather than spelled out as a networkthreshold_stats.zqel— reductions over an array, emitted as accumulator loops on the target
Algorithms¶
Where the declarative form is doing real work — recursion with a termination measure, an unrolled network, a property with no formula.
bubble_sort5.zqel— bubble sort unrolled into a fixed compare-swap network of five inputsfibonacci.zqel— a new Fibonacci number each scan — a state machine, not a loopinsertion_sort.zqel— insertion sort defined recursively, with the termination measure that makes it boundedis_prime.zqel— primality for 0..100 without modulo and without division
Code that stands in for a block¶
The transition semantics comes from a registered, proved artifact instead of from rules written in the file. What the compiler checks before it binds is the point: the artifact's sealed warrant must have been proved against this contract's canonical transition projection, and nothing else binds.
TripLatchBy.zqel—IMPLEMENTATION BY: a latch whose step function is a verified Rust artifact, registered in the manifest
A verified numerical kernel¶
One damped Gauss-Newton step, decomposed into blocks that are each proven separately and composed with their error envelopes. This is the largest thing in the repository and the one that exercises the numerical assurance chain end to end.
LMFit.zqel— the program root: an online calibration fit driven by one verified stepLM_AdaptLambda.zqel— LM Trust Region: accept/reject step and adapt damping parameter λLM_CgConverged.zqel— CG convergence check: converged = (rr ≤ tol)LM_CgDotProduct.zqel— CG helper: dot product s = aᵀb = Σ_k a[k]·b[k]LM_CgNegateVec.zqel— CG init helper: negate vector neg_g[k] = -g[k]LM_CgScalarDiv.zqel— CG step-size computation: out = numerator / denominatorLM_CgUpdateD.zqel— CG iteration: solution update d_new[k] = d[k] + alpha·p[k]LM_CgUpdateP.zqel— CG iteration: search-direction update p_new[k] = r_new[k] + beta·p[k]LM_CgUpdateR.zqel— CG iteration: residual update r_new[k] = r[k] - alpha·Ap[k]LM_Cramer2x2.zqel— 2×2 direct linear solve via Cramer's rule: Ad · d_sol = -gLM_DiagDamp.zqel— LM Step 2: Diagonal damping Ad = JᵀJ + λILM_GainRatio.zqel— LM Trust Region: gain ratio ρ = actual_reduction / predicted_reductionLM_Gradient.zqel— LM Step 3: Gradient g = Jᵀ rLM_GradientLinearStep.zqel— LM Step 4b: Gradient step toward solution of Ad · d = −gLM_GramMatrix.zqel— LM Step 1: Gram matrix JᵀJ (2×2, stored flat row-major)LM_MatVec.zqel— LM Step 4a: Matvec Ad_d = Ad · dLM_QuadModel.zqel— Residual + Jacobian of the quadratic calibration modelLM_Step.zqel— the composition root: one damped Gauss-Newton step over two parametersLM_Update.zqel— LM Step 5: Parameter update x_new = x + d
Analog signal processing¶
Continuous quantities on a discrete scan: integrators, a differentiator, and PID with anti-windup.
differentiator.zqel— A discrete differentiator: d = (x - x_prev) / DTflow_totalizer.zqel— a totalizer with physical units: volumetric flow integrated into a volumeintegrator.zqel— A clamped integrator: y accumulates x over time (DT = elapsed seconds from thepid.zqel— A PID controller with output saturation and integral anti-windup
Three of those state the mathematics for any input, which is what makes them
reference examples — and exactly what a machine representation cannot promise.
Choosing Float64 adds an obligation ℝ does not have: every value the programme
evaluates has to fit in a binary64, and y + x * DT is evaluated before
CLAMP ever sees it. So each has a sibling that declares the caller's operating
envelope, and that is the one a deployment emits:
integrator_bounded.zqel— the clamped integrator under a declared input span and band ceilingdifferentiator_bounded.zqel— the differentiator under an input span; note that the span alone is not enough, becauseIF DT <> 0.0proves the division happens, not that its result is representable — the profile has to state a minimum scan interval toopid_bounded.zqel— the same controller under a declared envelope on gains, error and bandpid_system.zqel— a concrete instance with its gains fixedassurance_policy.zqel— anASSURANCEbound: the programme is right in ℝ, and this says how far the machine may be from it. Compiled under a representation, because an error envelope only exists where one is chosen
PID as a composition¶
The same controller again, this time split into blocks and closed around a plant — the pair to read when composition is the question.
PID.zqel— A PID controller with output saturation and integral anti-windup. Thepid_loop.zqel— Closed-loop: first-order plant + inline PID (flat, no composition)pid_system.zqel— a concrete instance with its gains fixedplant.zqel— First-order plant: ẏ = -2y + 2u (pole at -2 rad/s, T = 0.5 s)
Control loops with a certificate¶
A closed loop whose stability is proven, not simulated.
Loop.zqel— the closed loop as a composition —PlantandPControllerwired togetherPController.zqel— Proportional controller with actuator saturation: u = clamp(-K*x, -umax, umax)Plant.zqel— PT1 plant as its own FB. It guarantees its state stays in [-10,10] PROVIDEDclosed_loop.zqel— A closed control loop in a single block, with a proven Lyapunov certificate
Discretisation¶
A continuous reference (CONTINUOUS_REFERENCE) beside its discrete implementation, so the gap between them can be bounded.
external_pid.zqel— a PID whose continuous reference is declared beside it, so the discretisation gap can be boundedlowpass.zqel— a first-order lowpass derived from its own ODE by forward Euler
Sequential function charts¶
SSFC in its own right: a minimal chart, a chart with parallel regions and a JOIN, and a full batch process.
arm_calibration.zqel— Two-arm robot calibration: parallel regions with JOINbatch_pasteurizer.zqel— Batch Pasteuriser SSFCtank_fill.zqel— Minimal SSFC example: Fill -> Drain -> Done
A complete plant¶
The batch pasteuriser as a PROGRAM: an SSFC for the sequence, a PI controller for the temperature, composed into one root.
BatchPasteurizer.zqel— Batch Pasteuriser SSFCPasteurizer.zqel— Batch Pasteuriser PROGRAM — composition rootTemperaturePID.zqel— Temperature PI controller with enable gating and integral anti-windup
Counting and access control¶
A works car park, first written straight and then as a composition — the pair that shows what composition buys.
Belegungszaehler.zqel— the shared 0..20 occupancy counter; simultaneous entry and exit hold itEinbahnSchranke.zqel— a one-way barrier: one direction only, and the pair of sensors that decides whichParkplatz2.zqel— the same car park as a composition — the pair that shows what composition buysWerksparkplatz.zqel— a works car park, written straightZufahrtsfreigabe.zqel— access on request: the button arms exactly one entry
Industrial exercises, as they were set¶
Teaching exercises modelled unchanged, each with its original task statement beside it. Their value is that the requirements were written before anyone thought about proof.
AxleCounter.zqel— axle counting in a track section — the difference between entering and leaving axles is the occupancyAxleGate.zqel— one sensor pair of that axle counter, on its ownConveyorInterlock.zqel— two conveyors with an interlock between themStirredReactor.zqel— a batch reactor as a sequence — the one exercise in this set that is a chartTunnelCount.zqel— a single-bore tunnel: count the cars inside and drive the signals from that countTwoHandPress.zqel— a press with two-hand interlocking — and the proof that found a real fault in the first draft
EXTERN — operations zqel does not implement¶
A contract without a body, discharged by a registered artifact.
sat_add_u8.zqel— a typed external contract over unsigned parameterssaturating_abs.zqel— an external contract by example — an operation zqel does not implement itselfsaturating_add.zqel— a second external operation, added entirely from the user sidesaturating_sub.zqel— saturating subtraction, the complement to the addition abovescale.zqel— saturating multiply-divide, the shape analog I/O scaling actually needs
Realization: contracts without an implementation¶
RULE-only blocks whose implementation is supplied afterwards and then proved against them.
affine_real_gs.zqel— an exact-real contract with a proved binary64 error envelope beside itarg_max4_gs.zqel— arg-max over four values as threeRULEs — the indexed one is admitted only after its own index bounds are provedclamp_gs.zqel— clamp, stated once as a conditional RULE underASSUME lo <= hidebounce_gs.zqel— a stateful debounce: the candidate receives the pre-state and returns output plus next statehysteresis_int_gs.zqel— integer hysteresis, the second stateful slicesat_add_gs.zqel— saturating addition as a single RULE over the exact sumsort4_semantic_gs.zqel— sorting stated as meaning, not as a network:SORTED(s)andPERMUTATION_OF(s, a)
The integer and bit-string types¶
The IEC types beyond INT, and what their ranges mean for a proof.
lword_flags.zqel— sixty-four independent boolean signals packed into one wordsint_saturate.zqel— the signed byte type, and what saturating at its range means for a proofunsigned_counters.zqel— the unsigned integer family, and where their ranges differ from the signed ones
Bit strings as status words¶
BYTE, WORD, DWORD as what they are on a real bus.
quality_byte.zqel—BYTEas an eight-bit status word, the way a real bus carries itstatus_word.zqel—WORDandDWORDas status words of sixteen and thirty-two bits
Strings¶
WSTRING as a carrier — compared and passed, not computed on.
device_tag.zqel—WSTRINGas a carrier — compared and passed on, not computed with
Wider intermediate arithmetic¶
Scaling that would overflow in INT, done in DINT and proven not to.
dint_scale.zqel— ADC scaling that would overflow inINT, done inDINTand proven not to
Interoperability¶
A block transferred from an MTP standard project, kept as a compatibility reference.
anaview.zqel— a block transferred from an MTP standard project, kept as a compatibility reference
A test holds this list complete: every .zqel file under examples/ must
appear above, or stand in an explicit exclusion list with a reason. That is
the only way a page like this stays true — the previous version of it named
21 of the 106.
Where to go next
| You want to… | Go to |
|---|---|
| Look up a keyword, type, or operator | Language Reference |
| Look up SSFC syntax and semantics | Language Reference §21 — SSFC |
| Look up ownership and recovery claim definitions and EvidenceStrength | Assurance Reference |
| See all CLI options and output formats | CLI Reference |
Configure MATHREAL representation or sampling |
Project Configuration Reference |
| Model a sequential process with SSFC | How-to: Model a Sequential Machine |
| Model concurrent sequences with JOIN | How-to: Model Parallel Regions |
| Compose an SSFC with FB or SSFC child components | How-to: Compose Components |
| Check which state owns each output | How-to: Audit Signal Ownership |
| Prove safe recovery within N scans | How-to: Prove Safe Recovery |
| Add an externally-verified function | How-to: Verified Functions |
Configure MATHREAL / Float64 representation |
How-to: Configure MATHREAL Representation |
| Diagnose a proof failure | How-to: Diagnose Proof Failures |
| Understand pre-state semantics and priorities | Concepts: Synchronous semantics |
| Understand SSFC scan model and assurance scope | Concepts: SSFC Semantics |
| Understand FB/SSFC composition and WIRING | Concepts: Component Model |
| Understand signal ownership analysis | Concepts: Ownership and Effects |
| Understand recovery and progress analysis | Concepts: Recovery and Progress |
Understand MATHREAL = ℝ and the numerical assurance chain |
Concepts: Numerical semantics |
| Understand what "proved" guarantees | Concepts: Assurance model |
| Understand EXTERN warrant inheritance | Concepts: EXTERN trust model |
| Understand guided synthesis and its relationship to EXTERN | Concepts: EXTERN trust model §guided-synthesis |
| Accept and use a guided synthesis candidate | How-to: Realize a contract |
| Look up GuidedSynthesisWarrant fields | Reference: GuidedSynthesisWarrant |
| Read the compiler internals | Architecture |