Functions are reusable code blocks that can be called from actions or other functions.
atomicfuncadd(x,y):returnx+yserialfuncprocess_items():foriteminitems:# Process each itemresult=transform(item)
Characteristics
Parameters: Functions can take parameters
Return values: Can return values
Flow modifiers: Can be atomic or serial (default)
Call contexts: Can be called from atomic, serial, parallel, or oneof blocks, at the top level or within roles
Top-level or role-scoped: Can be global or part of a role
Atomic vs Serial Functions
Atomic functions:
Execute without yielding
All statements run as one indivisible step
Faster, smaller state space
Serial functions (default):
Yield between statements
Allows interleaving with other actions
More realistic for modeling sequential operations
Calling Contexts
Functions can be called from atomic, serial, parallel, and oneof blocks:
# ✅ Calling from atomic actionatomicactionFromAtomic:result=some_function()# ✅ Calling from serial action (default flow)actionFromSerial:result=some_function()# function body yields between statements# ✅ Calling from a parallel block — each call is one parallel statement;# the function bodies interleave with the other parallel statementsactionFromParallel:parallel:some_function()other_function()# ✅ Calling from a oneof blockactionFromOneof:oneof:some_function()other_function()# ✅ Calling from role actionsroleMyRole:actionDoWork:result=some_function()
Function Call Syntax Limitations
Fizz functions have restricted call syntax. They must follow this pattern:
[variable =] [role.]function([parameters])
Cannot use fizz functions in complex expressions:
# ❌ WRONG: Function call in conditionalifreplica.get_state()==State.ACTIVE:process()# ✅ CORRECT: Extract to variable firststate=replica.get_state()ifstate==State.ACTIVE:process()# ❌ WRONG: Function call in arithmeticresult=compute()+10# ✅ CORRECT: Extract firstvalue=compute()result=value+10
Reason: Fizz function calls create yield points and must be tracked separately by the model checker.
Note: Standard Python/Starlark functions (built-ins like len(), str(), etc.) have no restrictions.
# Example 0022: Function with Parameters# Functions with multiple parameters# Demonstrates: multiple parameters, parameter orderaction Init:
x=0y=0func add(a, b):
return a + b
func multiply(a, b):
return a * b
atomic action Compute:
require x < 2x= x + 1# Functions with multiple parametersy= add(x, 2)y= multiply(y, 2)# After first call: x=1, y=add(1,2)*2=6# After second call: x=2, y=add(2,2)*2=8atomic action NoOp:
if x >= 2:
pass
# Example 0023: Function Return Values# Functions with conditional returns# Demonstrates: early return, conditional logic in functionsaction Init:
value=0is_valid= False
func validate(x):
if x < 0:
return False
if x > 10:
return False
return True
atomic action CheckValue:
if value >= 12:
returnvalue= value + 3is_valid= validate(value)# value: 0->3->6->9->12# is_valid: True, True, True, Falseatomic action NoOp:
if value >= 12:
pass
# Example 0024: Atomic Function# Atomic functions execute without yielding# Demonstrates: atomic func modifieraction Init:
items=[]total=0atomic func process_batch():
# Atomic function - all statements execute togetherfor i in range(3):
items.append(i)total= total + i
return total
atomic action ProcessData:
require len(items)==0# Call atomic functionresult= process_batch()# After ProcessData: items=[0,1,2], total=3, result=3always assertion ValidState:
# Atomic function ensures we never see partial statereturn len(items)==0 or len(items)==3atomic action NoOp:
if len(items) > 0:
pass
# Example 0025: Function with Serial For Loop# Functions can contain serial blocks/loops that yield# Demonstrates: atomic function with serial for loop inside# NOTE: Functions can be called from any flow context (atomic, serial,# parallel, oneof), at the top level or within rolesMAX=2action Init:
items=[]total=0# Atomic function with serial for loop inside# Function itself is atomic, but for loop yields between iterationsatomic func process_items():
serial for i in range(MAX):
items.append(i)total= total + i
return total
atomic action ProcessData:
require len(items) < MAX
# Call function from atomic actionresult= process_items()# Function is atomic overall, but serial for yields internally# Creates intermediate states during loop executionatomic action NoOp:
if len(items) >= MAX:
pass
# Example: Top-level Function Called from a Serial Action# Top-level (non-role) functions can be called from serial actions.# The function body yields between statements like any serial code,# so other actions (and crashes) interleave with it.MAX=2action Init:
counter=0last=0func Incr(amount):
counter= counter + amount
return counter
atomic action Reset:
counter=0last=0serial action Add:
require counter < MAX
last= Incr(1)
# Example: Functions Called from a Parallel Block# Function calls are supported inside parallel blocks. Each call is one# parallel statement; the function body executes with its own flow# (serial by default) and interleaves with the other parallel statements.# The reachable states are identical to writing the function bodies inline.# MOD keeps the counters bounded without require (a `require` directly# inside a parallel block is a separate known issue).MOD=3action Init:
a=0b=0action NoOp:
pass
func IncrTwiceA():
a=(a + 1) % MOD
a=(a + 1) % MOD
func IncrB():
b=(b + 1) % MOD
parallel action Both:
IncrTwiceA() IncrB()
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:100 max_concurrent_actions:2}
Nodes: 252, queued: 0
Skipping dotfile generation. Too many Nodes: 252
Valid Nodes: 252 Unique states: 207
IsLive: true
PASSED: Model checker completed successfully
# Example: Role Functions Called from a Parallel Block# Role methods can be called from parallel blocks too. (Before the# executeEndOfBlock fork-propagation fix this crashed the model checker# even though role calls passed the old call-site guard.)MOD=3role Node:
action Init:
self.a =0 self.b =0 func IncrA():
self.a =(self.a + 1) % MOD
func IncrB():
self.b =(self.b + 1) % MOD
parallel action Both:
self.IncrA() self.IncrB()action Init:
n= Node()action NoOp:
pass
# Example: Function Calls Wrapped in Nested Blocks inside a Parallel Block# A function call nested inside an atomic: or serial: block within a# parallel block works: when the call returns, the block ends advance# through the parallel scope and fork the remaining parallel statements# (the executeEndOfBlock scope-climbing path).# MOD keeps the counters bounded without require (a `require` directly# inside a parallel block is a separate known issue).MOD=3action Init:
a=0b=0action NoOp:
pass
func IncrA():
a=(a + 1) % MOD
func IncrB():
b=(b + 1) % MOD
parallel action Both:
atomic:
IncrA() serial:
IncrB()
# Example: Function Calls inside Nested Parallel Blocks# A parallel block nested within a parallel block, with function calls# at both levels. A function return inside the inner block must fork the# inner block's remaining statements; the inner block's end must advance# through the outer parallel scope (recursive executeEndOfBlock climbing).# MOD keeps the counters bounded without require (a `require` directly# inside a parallel block is a separate known issue).MOD=2action Init:
a=0b=0c=0action NoOp:
pass
func IncrA():
a=(a + 1) % MOD
func IncrB():
b=(b + 1) % MOD
func IncrC():
c=(c + 1) % MOD
parallel action Trio:
IncrA() parallel:
IncrB() IncrC()
Expected result (from fizz):
StateSpaceOptions: options:{max_actions:100 max_concurrent_actions:2}
Nodes: 560, queued: 0
Skipping dotfile generation. Too many Nodes: 560
Valid Nodes: 560 Unique states: 392
IsLive: true
PASSED: Model checker completed successfully