├── .travis.yml ├── .gitignore ├── readme.md ├── LICENSE ├── blueprint.go ├── fsm.go ├── fsm_test.go └── transition.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.4 4 | - tip 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: http://goel.io/joe 2 | 3 | #####=== Go ===##### 4 | 5 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 6 | *.o 7 | *.a 8 | *.so 9 | 10 | # Folders 11 | _obj 12 | _test 13 | 14 | # Architecture specific extensions/prefixes 15 | *.[568vq] 16 | [568vq].out 17 | 18 | *.cgo1.go 19 | *.cgo2.c 20 | _cgo_defun.c 21 | _cgo_gotypes.go 22 | _cgo_export.* 23 | 24 | _testmain.go 25 | 26 | *.exe 27 | *.test 28 | *.prof 29 | 30 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # fsm [![GoDoc](https://godoc.org/github.com/mixer/fsm?status.svg)](https://godoc.org/github.com/mixer/fsm) [![Build Status](https://travis-ci.org/mixer/fsm.svg)](https://travis-ci.org/mixer/fsm) 2 | 3 | 4 | Why another FSM implementation? Because we didn't see one suited for smaller-scale, programmatic use in Go which was very efficient. Example use: 5 | 6 | ```go 7 | bp := fsm.New() 8 | bp.Start(0) 9 | bp.From(0).To(1) 10 | bp.From(1).To(2).Then(func (m *fsm.Machine) { fmt.Println("hola!") }) 11 | 12 | m := bp.Machine() 13 | m.State() // => a 14 | m.Goto(1) // => error(nil) 15 | m.State() // => b 16 | m.Goto(2) // => error(nil) 17 | // => hola! 18 | m.Goto(1) // => error, "Transition 2 to 1 not permitted." 19 | ``` 20 | 21 | See the godocs for more information. 22 | 23 | Benchmarks well, especially against [comparable solutions](https://github.com/ryanfaerman/fsm#benchmarks). 24 | 25 | ``` 26 | ➜ fsm git:(fsm) ✗ go test -bench=. 27 | PASS 28 | PASS 29 | BenchmarkTransitions 100000000 20.8 ns/op 30 | BenchmarkAllows 50000000 20.6 ns/op 31 | BenchmarkGetState 2000000000 0.48 ns/op 32 | ``` 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Beam Interactive, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /blueprint.go: -------------------------------------------------------------------------------- 1 | package fsm 2 | 3 | type Blueprint struct { 4 | transitions list 5 | start uint8 6 | } 7 | 8 | // New creates a new finite state machine blueprint. 9 | func New() *Blueprint { 10 | return &Blueprint{ 11 | transitions: make(list, 0), 12 | } 13 | } 14 | 15 | // From returns a new transition for the blueprint. 16 | // The transition will be added to the blueprint automatically when it has both 17 | // "from" and "to" values. 18 | func (b *Blueprint) From(start uint8) *Transition { 19 | return (&Transition{blueprint: b}).From(start) 20 | } 21 | 22 | // Add adds a complete transition to the blueprint. 23 | func (b *Blueprint) Add(t *Transition) { 24 | idx := b.transitions.InsertPos(t) 25 | trans := make(list, len(b.transitions)+1) 26 | 27 | copy(trans, b.transitions[:idx]) 28 | copy(trans[idx+1:], b.transitions[idx:]) 29 | trans[idx] = t 30 | b.transitions = trans 31 | } 32 | 33 | // Start sets the start state for the machine. 34 | func (b *Blueprint) Start(state uint8) { 35 | b.start = state 36 | } 37 | 38 | // Machine returns a new machine created from the blueprint. 39 | func (b *Blueprint) Machine() *Machine { 40 | fsm := &Machine{ 41 | state: b.start, 42 | transitions: b.transitions, 43 | } 44 | 45 | return fsm 46 | } 47 | -------------------------------------------------------------------------------- /fsm.go: -------------------------------------------------------------------------------- 1 | package fsm 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Machine represents a finite state machine. 8 | // Instances of this struct should not be constructed by hand and instead 9 | // should be created using a blueprint. 10 | type Machine struct { 11 | // A list of available transitions. 12 | transitions list 13 | // The current machine state. 14 | state uint8 15 | } 16 | 17 | // isLegal returns whether or not the specified transition from state a to b 18 | // is legal. 19 | func (f *Machine) isLegal(a uint8, b uint8) bool { 20 | return f.transitions.Search(serialize(a, b)) != nil 21 | } 22 | 23 | // Allows returns whether or not this machine can transition to the state b. 24 | func (f *Machine) Allows(b uint8) bool { 25 | return f.isLegal(f.state, b) 26 | } 27 | 28 | // Disallows returns whether or not this machine can't transition to the state 29 | // b. 30 | func (f *Machine) Disallows(b uint8) bool { 31 | return !f.Allows(b) 32 | } 33 | 34 | // State returns the current state. 35 | func (f *Machine) State() uint8 { 36 | return f.state 37 | } 38 | 39 | // Goto moves the machine to the specified state. An error is returned if the 40 | // transition is not valid. 41 | func (f *Machine) Goto(state uint8) error { 42 | t := f.transitions.Search(serialize(f.state, state)) 43 | if t == nil { 44 | return fmt.Errorf("can't transition from state %d to %d", f.state, state) 45 | } 46 | 47 | f.state = state 48 | if t.fn != nil { 49 | t.fn(f) 50 | } 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /fsm_test.go: -------------------------------------------------------------------------------- 1 | package fsm 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | const ( 10 | A uint8 = iota 11 | B 12 | C 13 | D 14 | ) 15 | 16 | func makeMachine() *Machine { 17 | bp := New() 18 | bp.Start(A) 19 | bp.From(A).To(B) 20 | bp.From(B).To(C) 21 | bp.From(B).To(B) 22 | return bp.Machine() 23 | } 24 | 25 | func TestWorksNormally(t *testing.T) { 26 | m := makeMachine() 27 | assert.Equal(t, A, m.State()) 28 | assert.Nil(t, m.Goto(B)) 29 | assert.Equal(t, B, m.State()) 30 | assert.Nil(t, m.Goto(C)) 31 | assert.Equal(t, C, m.State()) 32 | err := m.Goto(B) 33 | assert.NotNil(t, err) 34 | assert.Equal(t, C, m.State()) 35 | assert.Equal(t, "can't transition from state 2 to 1", err.Error()) 36 | } 37 | 38 | func TestAddsHandler(t *testing.T) { 39 | bp := New() 40 | out := []uint8{} 41 | bp.From(A).To(B).Then(func(m *Machine) { out = append(out, 1) }) 42 | bp.From(B).To(C) 43 | bp.From(C).To(D).Then(func(m *Machine) { out = append(out, 2) }) 44 | m := bp.Machine() 45 | 46 | assert.Equal(t, []uint8{}, out) 47 | m.Goto(B) 48 | assert.Equal(t, []uint8{1}, out) 49 | m.Goto(C) 50 | assert.Equal(t, []uint8{1}, out) 51 | m.Goto(D) 52 | assert.Equal(t, []uint8{1, 2}, out) 53 | } 54 | 55 | func BenchmarkTransitions(b *testing.B) { 56 | m := makeMachine() 57 | m.Goto(B) 58 | b.ResetTimer() 59 | 60 | for n := 0; n < b.N; n++ { 61 | m.Goto(B) 62 | } 63 | } 64 | 65 | func BenchmarkAllows(b *testing.B) { 66 | m := makeMachine() 67 | for n := 0; n < b.N; n++ { 68 | m.Allows(B) 69 | } 70 | } 71 | 72 | func BenchmarkGetState(b *testing.B) { 73 | m := makeMachine() 74 | for n := 0; n < b.N; n++ { 75 | m.State() 76 | } 77 | } 78 | 79 | func BenchmarkBuildMachine(b *testing.B) { 80 | for n := 0; n < b.N; n++ { 81 | makeMachine() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /transition.go: -------------------------------------------------------------------------------- 1 | package fsm 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | // Handler represents a callback to be called when the machine performs a 8 | // certain transition between states. 9 | type Handler func(m *Machine) 10 | 11 | // Transition represents a transition between two states. 12 | type Transition struct { 13 | start bool 14 | from uint8 15 | fromSet bool 16 | to uint8 17 | toSet bool 18 | hash uint16 19 | blueprint *Blueprint 20 | fn func(m *Machine) 21 | } 22 | 23 | // From sets the source state of the transition. 24 | func (t *Transition) From(from uint8) *Transition { 25 | t.from = from 26 | t.fromSet = true 27 | t.recalculate() 28 | return t 29 | } 30 | 31 | // To sets the destination state of the transition. 32 | func (t *Transition) To(to uint8) *Transition { 33 | t.to = to 34 | t.toSet = true 35 | t.recalculate() 36 | return t 37 | } 38 | 39 | // recalculate calculates the hash for this transition if both "from" and "to" 40 | // have been set. If both "from" and "to" are set then this transition will 41 | // also be added to the blueprint. 42 | func (t *Transition) recalculate() { 43 | if !t.toSet || !t.fromSet { 44 | return 45 | } 46 | 47 | t.hash = serialize(t.from, t.to) 48 | t.blueprint.Add(t) 49 | } 50 | 51 | // Then sets the callback function for when the transition has occurred. 52 | func (t *Transition) Then(fn Handler) *Transition { 53 | t.fn = fn 54 | return t 55 | } 56 | 57 | // serialize serializes a transition between two states into a single value, 58 | // where the first 8 bits are the "from" and the last 8 bits are the "to" 59 | // state. 60 | func serialize(from, to uint8) uint16 { 61 | return (uint16(from) << 8) | uint16(to) 62 | } 63 | 64 | // list represents a list of transitions. 65 | type list []*Transition 66 | 67 | // Len returns the length of the list. 68 | func (t list) Len() int { 69 | return len(t) 70 | } 71 | 72 | // Swap swaps the two elements with indexes a and b. 73 | func (t list) Swap(a, b int) { 74 | t[a], t[b] = t[b], t[a] 75 | } 76 | 77 | // Less returns whether the element at index a should appear before b. 78 | func (t list) Less(a, b int) bool { 79 | return t[a].hash < t[b].hash 80 | } 81 | 82 | // Search searches for the specified hash in the list and returns it if it is 83 | // present. 84 | func (t list) Search(x uint16) *Transition { 85 | low, high := 0, len(t)-1 86 | for low <= high { 87 | i := (low + high) / 2 88 | if t[i].hash > x { 89 | high = i - 1 90 | } else if t[i].hash < x { 91 | low = i + 1 92 | } else { 93 | return t[i] 94 | } 95 | } 96 | 97 | return nil 98 | } 99 | 100 | // InsertPos returns the index at which the specified transition should be 101 | // inserted into the slice to retain it's order. 102 | func (t list) InsertPos(v *Transition) int { 103 | return sort.Search(len(t), func(i int) bool { 104 | return t[i].hash >= v.hash 105 | }) 106 | } 107 | --------------------------------------------------------------------------------