[statemachine] initial steps for declarative transitions

# Conflicts:
#	codex/utils/asyncstatemachine.nim
This commit is contained in:
Eric Mastro 2023-02-15 14:35:44 +11:00
parent 2035568b88
commit 127adc0b8a
No known key found for this signature in database
GPG Key ID: AD065ECE27A873B9
2 changed files with 63 additions and 17 deletions

View File

@ -2,36 +2,55 @@ import pkg/questionable
import pkg/chronos
import pkg/upraises
push: {.upraises:[].}
type
Machine* = ref object of RootObj
# TODO: context?
state: State
running: Future[void]
scheduled: AsyncQueue[Event]
scheduling: Future[void]
transitions: seq[Transition]
State* = ref object of RootObj
Event* = proc(state: State): ?State {.gcsafe, upraises:[].}
Event = proc(state: State): ?State {.gcsafe, upraises:[].}
TransitionCondition* = proc(machine: Machine, state: State): bool {.gcsafe, upraises:[].}
Transition* = object of RootObj
prevState: State
nextState: State
condition: TransitionCondition
TransitionProperty*[T] = ref object of RootObj
machine: Machine
value*: T
proc new*(T: type Transition,
prev, next: State,
condition: TransitionCondition): T =
Transition(prevState: prev, nextState: next, condition: condition)
proc newTransitionProperty*[T](self: Machine,
initialValue: T): TransitionProperty[T] =
TransitionProperty[T](machine: self, value: initialValue)
proc transition(_: type Event, previous, next: State): Event =
return proc (state: State): ?State =
if state == previous:
return some next
proc schedule*(machine: Machine, event: Event) =
try:
machine.scheduled.putNoWait(event)
except AsyncQueueFullError:
raiseAssert "unlimited queue is full?!"
proc setValue*[T](prop: TransitionProperty[T], value: T) =
prop.value = value
let machine = prop.machine
for transition in machine.transitions:
if transition.condition(machine, machine.state) and
machine.state == transition.prevState:
machine.schedule(Event.transition(machine.state, transition.nextState))
# TODO: provide context instead of machine?
method run*(state: State, machine: Machine): Future[?State] {.base, upraises:[].} =
proc schedule*(machine: Machine, event: Event) =
machine.scheduled.putNoWait(event)
method run*(state: State): Future[?State] {.base, upraises:[].} =
discard
proc run(machine: Machine, state: State) {.async.} =
try:
if next =? await state.run(machine):
if next =? await state.run():
machine.schedule(Event.transition(state, next))
except CancelledError:
discard
@ -50,11 +69,12 @@ proc scheduler(machine: Machine) {.async.} =
discard
proc start*(machine: Machine, initialState: State) =
if machine.scheduled.isNil:
machine.scheduled = newAsyncQueue[Event]()
machine.scheduling = machine.scheduler()
machine.schedule(Event.transition(machine.state, initialState))
proc stop*(machine: Machine) =
machine.scheduling.cancel()
machine.running.cancel()
proc new*(T: type Machine, transitions: seq[Transition]): T =
T(scheduled: newAsyncQueue[Event](), transitions: transitions)

View File

@ -6,6 +6,8 @@ import codex/utils/asyncstatemachine
import ../helpers/eventually
type
MyMachine = ref object of Machine
slotFilled: TransitionProperty[bool]
State1 = ref object of State
State2 = ref object of State
State3 = ref object of State
@ -37,8 +39,8 @@ method onMoveToNextStateEvent(state: State3): ?State =
some State(State1.new())
suite "async state machines":
var machine: Machine
var state1, state2: State
var machine: MyMachine
var state1, state2, state3: State
proc moveToNextStateEvent(state: State): ?State =
state.onMoveToNextStateEvent()
@ -46,9 +48,24 @@ suite "async state machines":
setup:
runs = [0, 0, 0]
cancellations = [0, 0, 0]
machine = Machine.new()
state1 = State1.new()
state2 = State2.new()
state3 = State3.new()
machine = MyMachine.new(@[
Transition.new(
state3,
state1,
proc(m: Machine, s: State): bool =
MyMachine(m).slotFilled.value
)
])
machine.slotFilled = machine.newTransitionProperty(false)
# EXAMPLE USAGE ONLY -- can be removed from tests
# should represent a typical external event callback, ie event called via
# subscription
proc externalEventCallback() =# would take params (rid: RequestId, slotIdx: UInt256) =
machine.slotFilled.setValue(true)
test "should call run on start state":
machine.start(state1)
@ -82,3 +99,12 @@ suite "async state machines":
await sleepAsync(1.millis)
check runs == [0, 1, 0]
check cancellations == [0, 1, 0]
test "can transition to state without next state":
machine.start(state3)
check eventually runs == [0, 0, 1]
test "moves states based on declared transitions and conditions":
machine.start(state3)
machine.slotFilled.setValue(true)
check eventually runs == [1, 0, 1]