@state
class S(MachineStateBase):
N: Param[int]
pc: StateVar[dict[int, PC]]
flag: StateVar[dict[int, bool]]
num: StateVar[dict[int, int]]
For multiple symmetric processes, store the program counter and process-local variables as map-valued StateVar fields indexed by process id. This is the style used by examples/bakery.py.
def procs(s): return Set(Val(1), ..., s.N)
@action(init=True)
def Init(c):
s, ps = c.state, procs(c.state)
s.pc = ps.map_to(lambda _: Val(PC.NCS))
s.flag = ps.map_to(lambda _: Val(False))
Initialize every process id explicitly with map_to. There is no generated process initialization helper.
@action(inline=True)
def p(c, q):
s = c.state
alts = iter(c.alternatives(*(pc.name for pc in PROCESS_PCS)))
Write one ordinary action helper for a single process id. Choose among its possible program-counter cases with c.alternatives(...).
alts = iter(c.alternatives("NCS", "E1"))
with next(alts):
c.assume(s.pc[q] == PC.NCS)
s.pc[q] = Val(PC.E1)
Each branch starts with a guard on the current program counter. Assign the next counter explicitly instead of using goto or fallthrough.
with c.one_of(s.unchecked[q], "i") as i:
s.nxt[q] = i
c.assume(~s.flag[s.nxt[q]])
s.pc[q] = Val(PC.W2)
Use c.one_of(...) directly in the branch for nondeterministic values. Parameters and variables are read from c.state.
with next(alts):
c.assume(s.pc[q] == PC.W2)
s.unchecked[q] -= Set(s.nxt[q])
s.pc[q] = Val(PC.W1)
A branch can contain multiple assignments. Set pc[q] to the next control state when the atomic step is done.
@action
def Next(c):
with c.one_of(procs(c.state), "q") as q:
p(c, q)
Choose the process id explicitly in the top-level step. The worker step is just another action helper.
@action(inline=True)
def kv_step(c):
s = c.state
c.assume(s.pc == PC.Wait)
s.pc = Val(PC.Sync)
For a single-process machine, use the same pattern with a scalar pc: StateVar[PC].
examples/bakery.py
examples/dekker.py
Reference examples for explicit program-counter state machines. Prefer this pattern over generator-based process specifications.