Best Practices
Always bound your state:
# ❌ BAD: Unbounded
action Increment:
counter = counter + 1 # Grows forever!
# ✅ GOOD: Bounded
action Increment:
if counter >= MAX_VALUE:
return
counter = counter + 1
Use modulo for cyclic behavior:
counter = (counter + 1) % MAX_VALUE
Use early returns for enabling conditions:
action Process:
if not ready:
return
if items_empty:
return
# Main logic here
Use tuple assignment to update multiple variables atomically:
# ❌ Creates 3 yield points in serial context
action Update:
a = 1 # Yield point
b = 2 # Yield point
c = 3 # Yield point
# Other actions can run between these
# ✅ Single statement = atomic in serial context
action Update:
a, b, c = 1, 2, 3 # Single yield point after
# All three updated together
Use cases:
- Swapping values:
x, y = y, x - Coordinated updates:
min_val, max_val = 0, 100 - Avoiding intermediate states visible to other actions
Note: This applies to serial contexts. In atomic blocks, all statements are atomic anyway.
Extracting local variables can alter execution order and atomicity:
# Original: Function called every iteration
action Process:
for i in range(get_count()): # get_count() called each iteration
process(i)
# Extracted: Function called once
action Process:
count = get_count() # Called once, creates yield point
for i in range(count):
process(i)
Impact:
- Changes when function is evaluated
- Creates additional yield points
- May change behavior if state changes between calls
Best practice: Be deliberate about extraction for clarity, but understand the semantic difference.
Prefer atomic when:
- Operations must complete together
- Modeling transactions
- Reducing state space
Use serial when:
- Modeling real sequencing
- Testing crash scenarios
- Exploring interleavings
Remember restrictions:
# Functions must be called from:
# 1. Atomic context
atomic action CallFunc:
result = my_function() # OK
# 2. Roles
role MyRole:
action DoWork:
result = my_function() # OK
# NOT from serial actions
action BadExample:
result = my_function() # ERROR!
Encapsulate related state:
# ✅ GOOD: Cohesive role
role Account:
action Init:
self.balance = 0
self.transactions = []
func deposit(amount):
self.balance += amount
Mark ephemeral state explicitly:
@state(ephemeral=['cache', 'temp_results'])
role Worker:
# Durable state persists across crashes
# Ephemeral state is lost
Write clear invariants:
# ✅ GOOD: Clear condition
always assertion BalanceNonNegative:
return account.balance >= 0
# ❌ BAD: Complex, unclear
always assertion ComplexCheck:
return (a > 0 and b < 10) or (c == d and e != f)
Use appropriate assertion types:
always: Safety (invariants)eventually always: Liveness (termination)exists: Reachabilitytransition: State transition properties
Add fairness for liveness:
# Without fairness - may starve
action Progress:
counter = counter + 1
# With fairness - will execute
fair action Progress:
counter = counter + 1
eventually always assertion EventuallyDone:
return counter >= 10 # Requires fairness
Use selectively:
# Enable for fault-sensitive paths
options:
crash_on_yield: true
# Disable for performance modeling
action_options:
ComputeMetrics:
crash_on_yield: false
Always bound your state variables to prevent explosion
Demonstrates: modulo arithmetic, guard clauses, bounded ranges
|
|
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:100 max_concurrent_actions:2}
Nodes: 660, queued: 0
Skipping dotfile generation. Too many Nodes: 660
Valid Nodes: 660 Unique states: 660
IsLive: true
PASSED: Model checker completed successfully
Model errors and recovery explicitly
Demonstrates: oneof for success/failure, error states
|
|
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:100 max_concurrent_actions:2}
Nodes: 240, queued: 0
Valid Nodes: 240 Unique states: 213
IsLive: true
PASSED: Model checker completed successfully