Assertions
Assertions specify properties the model checker should verify.
Must hold in all reachable states:
always assertion Invariant:
return counter >= 0
Use cases: Invariants, safety properties
Must hold in at least one reachable state:
exists assertion Reachability:
return counter == 10
Use cases: Reachability, possibility
Must hold for all state transitions:
transition assertion MonotonicIncrease(before, after):
if after.counter != before.counter:
return after.counter > before.counter
return True # Stutter step
Stutter Invariance (critical requirement):
Transition assertions must allow stutter steps (transitions where state doesn’t change):
# ✅ CORRECT: Allows stuttering
transition assertion NoDecrease(before, after):
if after.value != before.value:
return after.value >= before.value
return True # Stutter allowed
# ❌ WRONG: Requires change every step
transition assertion AlwaysIncreases(before, after):
return after.value > before.value
# Fails on stutter steps!
Why stutter invariance matters:
- Model checker may have steps where only thread-local variables change
- Some actions may execute without changing state variables
- FizzBee automatically passes assertion if no state variables changed
Automatic handling: If no state variables changed, assertion assumed satisfied (even if thread-local variables changed)
Pattern: Always check before != after before asserting a property about the change
Inside an always assertion, next_states() returns the list of successor
transitions from the state under check — one entry per enabled transition,
each executed to its first yield point. This expresses TLA+ ENABLED-style
safety properties with arbitrary quantifier nesting, e.g. “every item in the
trash must be restorable right now”:
always assertion EveryTrashItemRestorable:
for k in items:
if items[k] == "trash":
if len([n for n in next_states() if n.state.items[k] == "active"]) == 0:
return False
return True
Each transition is a struct:
| Field | Type | Example | Notes |
|---|---|---|---|
name |
string | "Customer#0.PlaceOrder" |
Flat label; also "thread-0", "channel-0-message-1" |
kind |
string | "action" |
"action", "thread", or "channel" |
role_ref |
string / None | "Customer#0" |
None for non-role transitions |
role_id |
id value / None | Customer0 |
Same value as the role’s __id__; compares by value |
action_name |
string / None | "PlaceOrder" |
Bare action name; None for thread/channel |
state |
struct | n.state.items |
Successor state vars — same shape as before/after in transition assertions |
(action_name/role_ref — not action/role — because action and role
are fizz keywords and cannot appear as attribute names.)
To scope a check to a specific role instance, compare ids — never role objects (roles compare by pointer and never match across states; see GOTCHAS):
always assertion EveryIdleWorkerCanWork:
for w in workers:
if w.status == "idle":
if len([n for n in next_states() if n.action_name == "Work" and n.role_id == w.__id__]) == 0:
return False
return True
Id values also expose their components: w.__id__.name → "Worker",
w.__id__.index → 0.
Semantics:
- Successors ignore exploration bounds (
max_actionsdepth,max_concurrent_actions, per-action caps). Enabledness is a property of the state, not the scheduler — so the check does not false-fail at the depth frontier or when in-flight actions exhaust the concurrency cap. - Crash variants are excluded.
- For non-atomic (serial) actions, the successor is the action’s first
yield point — effects later in the body are not yet visible. Use
effect predicates (
n.state.x == ...) with atomic witness actions; use name filtering (n.action_name == ...) to check a serial action can start. - Computed lazily, once per checked state. Works in model checking (BFS/DFS), simulation, and no-graph modes. Not available in composition specs.
- Reference examples:
09-06through09-09.
Eventually reaches a stable state:
eventually always assertion EventuallyStable:
return done == True
Requires: Fairness on relevant actions
Always makes progress:
always eventually assertion AlwaysProgresses:
return processed_count > 0
Requires: Strong fairness typically
Safety invariants that must hold in every state
Demonstrates: always assertion, safety properties
|
|
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:100 max_concurrent_actions:2}
Nodes: 6, queued: 0
Valid Nodes: 6 Unique states: 6
IsLive: true
PASSED: Model checker completed successfully
Existential properties - at least one state must satisfy
Demonstrates: exists assertion
|
|
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:100 max_concurrent_actions:2}
Nodes: 11, queued: 0
Valid Nodes: 11 Unique states: 11
IsLive: true
PASSED: Model checker completed successfully
Properties about state transitions
Demonstrates: transition assertion with before/after states
|
|
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:100 max_concurrent_actions:2}
Nodes: 8, queued: 0
Valid Nodes: 8 Unique states: 8
IsLive: true
PASSED: Model checker completed successfully
Eventually the system reaches a stable state
Demonstrates: eventually always assertion (liveness property)
|
|
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:100 max_concurrent_actions:2}
Nodes: 9, queued: 0
Valid Nodes: 9 Unique states: 9
Checking eventually always BecomesStable
IsLive: true
PASSED: Model checker completed successfully
System always makes progress eventually
Demonstrates: always eventually assertion (strong fairness)
|
|
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:100 max_concurrent_actions:2}
Nodes: 4, queued: 0
Valid Nodes: 4 Unique states: 4
Checking always eventually AlwaysMakesProgress
IsLive: true
PASSED: Model checker completed successfully
Soft-delete CRUDL: any trashed item can be restored directly. EveryTrashItemRestorable must PASS.
|
|
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:8 max_concurrent_actions:2} deadlock_detection:false
Nodes: 37, queued: 0
Valid Nodes: 37 Unique states: 15
IsLive: true
PASSED: Model checker completed successfully
Soft-delete CRUDL where the trash is a STACK: only the most recently deleted item can be restored. With two items in the trash, the bottom one is not restorable in one step, so EveryTrashItemRestorable must FAIL. This is the case a global-reachability (exists) formulation cannot catch.
|
|
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:8 max_concurrent_actions:2} deadlock_detection:false
FAILED: Model checker failed. Invariant: EveryTrashItemRestorable
------
Init
--
state: {"items":{},"next_id":0,"trash_stack":[]}
------
Create
--
state: {"items":{"0":"active"},"next_id":1,"trash_stack":[]}
------
Create
--
state: {"items":{"0":"active","1":"active"},"next_id":2,"trash_stack":[]}
------
Delete
--
state: {"items":{"0":"active","1":"active"},"next_id":2,"trash_stack":[]}
------
Any:id=0
--
state: {"items":{"0":"trash","1":"active"},"next_id":2,"trash_stack":[0]}
------
Delete
--
state: {"items":{"0":"trash","1":"active"},"next_id":2,"trash_stack":[0]}
------
Any:id=1
--
state: {"items":{"0":"trash","1":"trash"},"next_id":2,"trash_stack":[0,1]}
------
Regression guard for the concurrency-cap false positive: Churn is a serial (non-atomic) action. At its mid-action yield, the one in-flight thread exhausts max_concurrent_actions=1, so the real explorer cannot schedule Restore from that state. The next_states() probe must ignore the cap (ENABLED is a property of the state, not the scheduler), so TrashRestorable must PASS. If the probe respected the cap, this spec would FAIL at every mid-Churn yield.
|
|
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:6 max_concurrent_actions:1} deadlock_detection:false
Nodes: 55, queued: 0
Valid Nodes: 55 Unique states: 43
IsLive: true
PASSED: Model checker completed successfully
next_states() with symmetric roles: transitions carry structured fields (kind, role_ref, role_id, action_name) alongside the flat name. role_id is the role’s identity value (same as the role’s id) and compares by VALUE, so n.role_id == w.__id__ works across the assertion/probe clone boundary. Comparing Role objects directly would silently be False, roles compare by pointer. See GOTCHAS. (The fields are named action_name/role_ref because action and role are fizz keywords and cannot appear as attribute names.) Also exercises the id component attrs: id.name (“Worker”) and id.index.
|
|
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:6 max_concurrent_actions:2} deadlock_detection:false
Nodes: 3, queued: 0
Valid Nodes: 3 Unique states: 3
IsLive: true
PASSED: Model checker completed successfully
Regression pin for the no_graph exists bug: an unsatisfiable exists must FAIL under –experimental_no_graph (flags.txt) exactly as it does with the full graph. Before the fix, exists assertions were silently skipped in no_graph mode and this spec reported PASSED.
Needs command-line flags (
--experimental_no_graph); run locally withfizz.
---
deadlock_detection: false
options:
max_actions: 6
crash_on_yield: false
---
# Regression pin for the no_graph exists bug: an unsatisfiable `exists`
# must FAIL under --experimental_no_graph (flags.txt) exactly as it does
# with the full graph. Before the fix, exists assertions were silently
# skipped in no_graph mode and this spec reported PASSED.
action Init:
counter = 0
atomic action Inc:
require counter < 3
counter = counter + 1
exists assertion ImpossibleProbe:
return counter > 999
exists assertion ReachableProbe:
return counter == 2
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:6 max_concurrent_actions:2 crash_on_yield:false} deadlock_detection:false
Nodes: 4, queued: 0
FAILED: Expected states never reached
Invariant 0: ImpossibleProbe
Companion to 09-10: a satisfiable exists must PASS under –experimental_no_graph (flags.txt), the witness accumulator must actually detect witnesses, not just fail everything.
Needs command-line flags (
--experimental_no_graph); run locally withfizz.
---
deadlock_detection: false
options:
max_actions: 6
crash_on_yield: false
---
# Companion to 09-10: a satisfiable `exists` must PASS under
# --experimental_no_graph (flags.txt) — the witness accumulator must
# actually detect witnesses, not just fail everything.
action Init:
counter = 0
atomic action Inc:
require counter < 3
counter = counter + 1
exists assertion ReachableProbe:
return counter == 2
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:6 max_concurrent_actions:2 crash_on_yield:false} deadlock_detection:false
Nodes: 4, queued: 0
PASSED: Model checker completed successfully
Simulation-mode exists coverage warning: with a fixed seed (flags.txt, forces a single deterministic run), the unsatisfiable exists must produce a named WARNING, not a failure, since simulation is not exhaustive. The reachable probe must not be warned about if this seed’s trace witnesses it; the impossible one always warns.
Needs command-line flags (
-x --seed 42); run locally withfizz.
---
deadlock_detection: false
options:
max_actions: 6
---
# Simulation-mode exists coverage warning: with a fixed seed (flags.txt,
# forces a single deterministic run), the unsatisfiable exists must
# produce a named WARNING — not a failure, since simulation is not
# exhaustive. The reachable probe must not be warned about if this
# seed's trace witnesses it; the impossible one always warns.
action Init:
counter = 0
atomic action Inc:
require counter < 3
counter = counter + 1
exists assertion ImpossibleProbe:
return counter > 999
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:6 max_concurrent_actions:2} deadlock_detection:false
WARNING: 1 exists assertion(s) never satisfied in 1 simulation run(s):
exists assertion ImpossibleProbe
Simulation is not exhaustive: this may be a coverage gap rather than unreachability. Model check to verify.
- Mutex Pattern
- Read-Write Lock Pattern
- Distributed Lock Pattern
- Optimistic Locking Pattern
- Saga Pattern
- Snapshot Isolation Pattern
- Leader Election (Simplified Raft)
- Compare-And-Swap Pattern
- Producer-Consumer Pattern
- Message Queue Pattern
- Cache Coherence Pattern
- Eventual Consistency Pattern
- Idempotent Operations Pattern
- Circuit Breaker Pattern
- Rate Limiting Pattern
- Write-Ahead Log Pattern
- Event Sourcing Pattern