[READ-ONLY] Mirror of https://github.com/andrioid/statesman. xstate inspired schema compatible statechart library for Go
fsm
go
statecharts
1package statesman
2
3// Definition is the validated, structural machine model the engine walks. It is
4// non-generic: guards and actions are referenced by Callsite index, never by Go
5// type, which is the core/codegen boundary (decision 36) — the generated
6// constructor provides closures keyed by the same Callsite ids.
7//
8// internal/schema builds a Definition from machine.json (Type strings filled,
9// Callsite ids assigned in document order); `statesman generate` emits the same
10// shape as Go and the matching dispatch closures.
11type Definition struct {
12 ID string // machine id; must equal the package name (decision 50)
13 Root *StateNode
14 nodes map[StateID]*StateNode
15
16 // ActionCount / GuardCount are the number of distinct callsites enumerated,
17 // so a runtime or codegen consumer can size its dispatch table.
18 ActionCount int
19 GuardCount int
20}
21
22// Lookup returns the node with the given id, or nil.
23func (d *Definition) Lookup(id StateID) *StateNode { return d.nodes[id] }
24
25// NewDefinition assembles a Definition from a fully-built root node, indexing
26// every node by ID for Lookup. actionCount/guardCount are the totals from the
27// builder's callsite enumeration (they size the dispatch tables). Used by
28// internal/schema and by generated code; the engine never builds one itself.
29func NewDefinition(id string, root *StateNode, actionCount, guardCount int) *Definition {
30 d := &Definition{
31 ID: id,
32 Root: root,
33 nodes: make(map[StateID]*StateNode),
34 ActionCount: actionCount,
35 GuardCount: guardCount,
36 }
37 var index func(n *StateNode)
38 index = func(n *StateNode) {
39 d.nodes[n.ID] = n
40 for _, c := range n.Children {
41 index(c)
42 }
43 }
44 index(root)
45 return d
46}
47
48// StateKind classifies a state node.
49type StateKind uint8
50
51const (
52 StateAtomic StateKind = iota
53 StateCompound
54 StateParallel
55 StateFinal
56 StateHistory
57)
58
59func (k StateKind) String() string {
60 switch k {
61 case StateAtomic:
62 return "atomic"
63 case StateCompound:
64 return "compound"
65 case StateParallel:
66 return "parallel"
67 case StateFinal:
68 return "final"
69 case StateHistory:
70 return "history"
71 default:
72 return "unknown"
73 }
74}
75
76// HistoryKind is shallow or deep, for history pseudo-states.
77type HistoryKind uint8
78
79const (
80 HistoryShallow HistoryKind = iota
81 HistoryDeep
82)
83
84// ActionRef references an entry/exit/transition action. Type is the JSON action
85// type (for codegen name resolution); Params are the raw literal params (for
86// stub type inference only — never read as a map at runtime). Callsite is the
87// stable id the opaque applier dispatches on.
88type ActionRef struct {
89 Type string
90 Params map[string]any
91 Callsite int
92}
93
94// GuardRef references a transition guard. Same Callsite contract as ActionRef,
95// in a separate id space.
96type GuardRef struct {
97 Type string
98 Params map[string]any
99 Callsite int
100}
101
102// Delay is a resolved `after` delay: literal milliseconds when Symbol == "",
103// otherwise a symbolic delays.go const name resolved at codegen.
104type Delay struct {
105 Millis int64
106 Symbol string
107}
108
109// Transition is one transition out of a state. Event == "" means eventless
110// (`always`). Targets is empty for a targetless (internal, action-only)
111// transition; in v1 it holds at most one explicit target (fan-out to multiple
112// states happens only via parallel/initial entry closure).
113type Transition struct {
114 Source *StateNode
115 Event string
116 Guard *GuardRef
117 Targets []*StateNode
118 Actions []ActionRef
119 DocOrder int
120 IsAfter bool
121 Delay Delay // valid when IsAfter
122}
123
124// Eventless reports whether t is an `always` transition.
125func (t *Transition) Eventless() bool { return t.Event == "" && !t.IsAfter }
126
127// Internal reports whether t is targetless (action-only; no exit/entry).
128func (t *Transition) Internal() bool { return len(t.Targets) == 0 }
129
130// Invoke is an invoked actor declared on a state. ID is required (decision 10);
131// Src names the Go actor symbol. OnDone/OnError are the transitions wired on the
132// generated done.invoke.<id> / error.invoke.<id> events.
133type Invoke struct {
134 ID string
135 Src string
136 OnDone []*Transition
137 OnError []*Transition
138}
139
140// StateNode is a node in the machine tree.
141type StateNode struct {
142 ID StateID
143 Key string // local key under Parent.Children
144 Kind StateKind
145 Parent *StateNode
146 Children []*StateNode // document order
147 Initial *StateNode // compound: default child (nil otherwise)
148 HistoryKind HistoryKind // history nodes only
149 HistoryTo []*StateNode // history default target (nil => use shallow default)
150 Entry []ActionRef
151 Exit []ActionRef
152 Invokes []*Invoke
153 Transitions []*Transition // on + always + after, in document order
154 DocOrder int
155}
156
157// IsAtomic reports whether n is a leaf for selection purposes (atomic or final).
158func (n *StateNode) IsAtomic() bool { return n.Kind == StateAtomic || n.Kind == StateFinal }
159
160// ProperAncestors returns n's ancestors from its parent up to and including the
161// root, innermost first.
162func (n *StateNode) ProperAncestors() []*StateNode {
163 var out []*StateNode
164 for p := n.Parent; p != nil; p = p.Parent {
165 out = append(out, p)
166 }
167 return out
168}
169
170// IsDescendant reports whether n is a proper descendant of anc.
171func (n *StateNode) IsDescendant(anc *StateNode) bool {
172 if anc == nil {
173 return false
174 }
175 for p := n.Parent; p != nil; p = p.Parent {
176 if p == anc {
177 return true
178 }
179 }
180 return false
181}