[READ-ONLY] Mirror of https://github.com/andrioid/statesman. xstate inspired schema compatible statechart library for Go
fsm go statecharts
0

Configure Feed

Select the types of activity you want to include in your feed.

statesman / microstep.go
10 kB 334 lines
1package statesman 2 3// microstep.go is the generic engine that drives docs/transition-algorithm.md 4// §4 (macrostep/microstep loop) and §8 (action accumulation, transactionality). 5// It is generic over TCtx/TEvt but never names a per-machine ActionResult: it 6// calls the opaque applier/guard closures the generated constructor injects 7// (decision 36). compute works entirely on clones of the engine's live state, so 8// an observer-aborted microstep needs no rollback — the live state was untouched 9// until commit. 10 11// EffectKind tags an AppliedEffect. 12type EffectKind uint8 13 14const ( 15 EffectNoop EffectKind = iota 16 EffectAssign 17 EffectSend 18 EffectSpawn 19) 20 21// AppliedEffect is the closure's return: the engine's view of one action result. 22// Assign carries the new context (the closure preserves child refs, decision 32); 23// Send/Spawn are outbox intents fired by the actor loop after publish. 24type AppliedEffect[TCtx any] struct { 25 Kind EffectKind 26 Ctx TCtx // EffectAssign 27 Target ActorAddress // EffectSend / EffectSpawn 28 Event EventBase // EffectSend 29 Spawn any // EffectSpawn descriptor (wired in Phase 4) 30} 31 32type queuedEvent[TEvt EventBase] struct { 33 descriptor string 34 payload TEvt 35 hasPayload bool 36} 37 38// Microstep is one committed-or-stageable transition transaction. The actor loop 39// runs observers on (PrevActive/PrevContext → NextActive/NextContext), then 40// commits (publish + drain Effects) or aborts. 41type Microstep[TCtx any] struct { 42 Transitions []*Transition 43 Exited []*StateNode // reverse document order (exit order) 44 Entered []*StateNode // document order (entry order) 45 PrevActive []StateID 46 NextActive []StateID 47 PrevContext TCtx 48 NextContext TCtx 49 Effects []AppliedEffect[TCtx] // Send/Spawn outbox, in order 50 TriggeredByEvent bool 51 ReachedFinal bool 52 53 // staged engine state, swapped in by commit. 54 cfg *configuration 55 hist map[StateID][]*StateNode 56 internalEnqueue []string // done.state.* descriptors 57} 58 59// engine holds the live interpreter state. Unexported; the Machine wraps it. 60type engine[TCtx any, TEvt EventBase] struct { 61 def *Definition 62 cfg *configuration 63 ctx TCtx 64 hist map[StateID][]*StateNode 65 66 internalQueue []queuedEvent[TEvt] 67 pendingExternal *queuedEvent[TEvt] 68 69 applyAction func(callsite int, ctx TCtx, evt TEvt) AppliedEffect[TCtx] 70 evalGuard func(callsite int, ctx TCtx, evt TEvt) bool 71 72 alwaysLimit int 73 alwaysRun int 74 status ActorStatus 75 errReason error 76 lastEvt TEvt // triggering event of the most recently staged microstep 77} 78 79// triggerEvent returns the event that triggered the most recently staged 80// microstep (zero for the initial entry and eventless/internal microsteps). The 81// actor loop passes it to observers. 82func (e *engine[TCtx, TEvt]) triggerEvent() TEvt { return e.lastEvt } 83 84const defaultAlwaysLimit = 100 85 86func newEngine[TCtx any, TEvt EventBase]( 87 def *Definition, 88 initCtx TCtx, 89 applyAction func(int, TCtx, TEvt) AppliedEffect[TCtx], 90 evalGuard func(int, TCtx, TEvt) bool, 91 alwaysLimit int, 92) *engine[TCtx, TEvt] { 93 if alwaysLimit <= 0 { 94 alwaysLimit = defaultAlwaysLimit 95 } 96 return &engine[TCtx, TEvt]{ 97 def: def, 98 cfg: newConfiguration(), 99 ctx: initCtx, 100 hist: make(map[StateID][]*StateNode), 101 applyAction: applyAction, 102 evalGuard: evalGuard, 103 alwaysLimit: alwaysLimit, 104 status: StatusStarting, 105 } 106} 107 108// guardPass returns a predicate evaluating guards against the live context and 109// the supplied triggering event (zero for eventless/internal microsteps). 110func (e *engine[TCtx, TEvt]) guardPass(evt TEvt) guardPredicate { 111 return func(t *Transition) bool { 112 if t.Guard == nil { 113 return true 114 } 115 return e.evalGuard(t.Guard.Callsite, e.ctx, evt) 116 } 117} 118 119// start computes the initial-configuration entry as a microstep (entry actions 120// run; invokes are deferred to the actor loop). Not committed. 121func (e *engine[TCtx, TEvt]) start() *Microstep[TCtx] { 122 toEnter := make(map[*StateNode]struct{}) 123 defEntry := make(map[*StateNode]struct{}) 124 addDescendantStatesToEnter(e.def.Root, toEnter, defEntry, e.hist) 125 var zero TEvt 126 return e.apply(nil, nil, toEnter, zero, false) 127} 128 129// offer queues an external event (from the mailbox) for the next step. 130func (e *engine[TCtx, TEvt]) offer(evt TEvt) { 131 e.pendingExternal = &queuedEvent[TEvt]{descriptor: evt.EventType(), payload: evt, hasPayload: true} 132} 133 134// offerDescriptor queues a synthetic external event identified only by its 135// descriptor (an `after` fire or a child done/error notification with no 136// user-constructed payload). Action/guard callsites on such edges are 137// context-shape, so the zero TEvt they receive is never inspected. 138func (e *engine[TCtx, TEvt]) offerDescriptor(desc string) { 139 e.pendingExternal = &queuedEvent[TEvt]{descriptor: desc} 140} 141 142// step computes the next pending microstep without committing, or reports the 143// macrostep quiescent. Priority: eventless > internal event > external. §4. 144func (e *engine[TCtx, TEvt]) step() (ms *Microstep[TCtx], quiescent bool) { 145 var zero TEvt 146 if ts := selectEventlessTransitions(e.cfg, e.guardPass(zero)); len(ts) > 0 { 147 if e.alwaysRun >= e.alwaysLimit { 148 e.status = StatusError 149 e.errReason = ErrAlwaysLoopExceeded 150 return nil, true 151 } 152 e.alwaysRun++ 153 return e.microstepFor(ts, zero, false), false 154 } 155 for len(e.internalQueue) > 0 { 156 qe := e.internalQueue[0] 157 e.internalQueue = e.internalQueue[1:] 158 if ts := selectTransitions(e.cfg, qe.descriptor, e.guardPass(qe.payload)); len(ts) > 0 { 159 return e.microstepFor(ts, qe.payload, true), false 160 } 161 // unmatched internal event is discarded; try the next one. 162 } 163 if e.pendingExternal != nil { 164 qe := *e.pendingExternal 165 e.pendingExternal = nil 166 if ts := selectTransitions(e.cfg, qe.descriptor, e.guardPass(qe.payload)); len(ts) > 0 { 167 return e.microstepFor(ts, qe.payload, true), false 168 } 169 // unhandled external event is dropped. 170 } 171 return nil, true 172} 173 174func (e *engine[TCtx, TEvt]) microstepFor(ts []*Transition, evt TEvt, triggered bool) *Microstep[TCtx] { 175 exitSet := computeExitSet(e.cfg, ts) 176 toEnter, _ := computeEntrySet(ts, e.hist) 177 return e.apply(exitSet, ts, toEnter, evt, triggered) 178} 179 180// apply builds the staged microstep on clones of the live state. §6/§7/§8.1. 181func (e *engine[TCtx, TEvt]) apply(exitSet []*StateNode, ts []*Transition, toEnter map[*StateNode]struct{}, evt TEvt, triggered bool) *Microstep[TCtx] { 182 e.lastEvt = evt 183 stageCfg := e.cfg.clone() 184 stageHist := cloneHistory(e.hist) 185 stageCtx := e.ctx 186 var effects []AppliedEffect[TCtx] 187 188 fold := func(callsite int) { 189 eff := e.applyAction(callsite, stageCtx, evt) 190 switch eff.Kind { 191 case EffectAssign: 192 stageCtx = eff.Ctx 193 case EffectSend, EffectSpawn: 194 effects = append(effects, eff) 195 } 196 } 197 198 for _, s := range exitSet { // reverse document order 199 recordHistoryInto(s, stageHist, e.cfg) 200 for _, a := range s.Exit { 201 fold(a.Callsite) 202 } 203 stageCfg.remove(s) 204 } 205 for _, t := range ts { 206 for _, a := range t.Actions { 207 fold(a.Callsite) 208 } 209 } 210 211 entered := orderedEntry(toEnter) 212 var doneStates []string 213 reachedFinal := false 214 for _, s := range entered { 215 stageCfg.add(s) 216 for _, a := range s.Entry { 217 fold(a.Callsite) 218 } 219 if s.Kind != StateFinal { 220 continue 221 } 222 if s.Parent == nil || s.Parent == e.def.Root { 223 reachedFinal = true 224 continue 225 } 226 doneStates = append(doneStates, "done.state."+string(s.Parent.ID)) 227 if gp := s.Parent.Parent; gp != nil && gp.Kind == StateParallel && allRegionsFinal(gp, stageCfg) { 228 doneStates = append(doneStates, "done.state."+string(gp.ID)) 229 } 230 } 231 232 return &Microstep[TCtx]{ 233 Transitions: ts, 234 Exited: exitSet, 235 Entered: entered, 236 PrevActive: e.cfg.atomicIDs(), 237 NextActive: stageCfg.atomicIDs(), 238 PrevContext: e.ctx, 239 NextContext: stageCtx, 240 Effects: effects, 241 TriggeredByEvent: triggered, 242 ReachedFinal: reachedFinal, 243 cfg: stageCfg, 244 hist: stageHist, 245 internalEnqueue: doneStates, 246 } 247} 248 249// commit applies a staged microstep to the live engine state. 250func (e *engine[TCtx, TEvt]) commit(ms *Microstep[TCtx]) { 251 e.cfg = ms.cfg 252 e.hist = ms.hist 253 e.ctx = ms.NextContext 254 for _, d := range ms.internalEnqueue { 255 e.internalQueue = append(e.internalQueue, queuedEvent[TEvt]{descriptor: d}) 256 } 257 switch { 258 case ms.ReachedFinal: 259 e.status = StatusDone 260 case e.status == StatusStarting: 261 e.status = StatusRunning 262 } 263 if ms.TriggeredByEvent { 264 e.alwaysRun = 0 265 } 266} 267 268func (c *configuration) clone() *configuration { 269 n := newConfiguration() 270 for k := range c.members { 271 n.members[k] = struct{}{} 272 } 273 return n 274} 275 276func (c *configuration) atomicIDs() []StateID { 277 at := c.atomic() 278 out := make([]StateID, len(at)) 279 for i, n := range at { 280 out[i] = n.ID 281 } 282 return out 283} 284 285func cloneHistory(h map[StateID][]*StateNode) map[StateID][]*StateNode { 286 out := make(map[StateID][]*StateNode, len(h)) 287 for k, v := range h { 288 out[k] = v 289 } 290 return out 291} 292 293// recordHistoryInto records the to-restore states for any history child of s, 294// reading the configuration before s is removed. §6.4. 295func recordHistoryInto(s *StateNode, hist map[StateID][]*StateNode, cfg *configuration) { 296 for _, h := range s.Children { 297 if h.Kind != StateHistory { 298 continue 299 } 300 var rec []*StateNode 301 if h.HistoryKind == HistoryDeep { 302 for _, n := range cfg.all() { 303 if n.IsAtomic() && n.IsDescendant(s) { 304 rec = append(rec, n) 305 } 306 } 307 } else { 308 for _, c := range s.Children { 309 if cfg.has(c) { 310 rec = append(rec, c) 311 } 312 } 313 } 314 hist[h.ID] = rec 315 } 316} 317 318// allRegionsFinal reports whether every region of a parallel state has an active 319// final descendant. §7.5. 320func allRegionsFinal(gp *StateNode, cfg *configuration) bool { 321 for _, r := range gp.Children { 322 final := false 323 for _, n := range cfg.all() { 324 if n.Kind == StateFinal && (n == r || n.IsDescendant(r)) { 325 final = true 326 break 327 } 328 } 329 if !final { 330 return false 331 } 332 } 333 return true 334}