1 /** 2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 This module contains utilities to reduce the boilerplate when implementing a 11 FSM. 12 13 # Callback Builder 14 Useful to generate the callbacks that control the actions to perform in an FSM 15 or the state transitions. 16 */ 17 module dextool.fsm; 18 19 import logger = std.experimental.logger; 20 21 version (unittest) { 22 import unit_threaded.assertions; 23 } 24 25 /** A state machine derived from the types it is based on. 26 * 27 * Each state have its unique data that it works on. 28 * 29 * The state transitions are calculated by `next` and the actions are performed 30 * by `act`. 31 */ 32 struct Fsm(StateTT...) { 33 static import sumtype; 34 35 alias StateT = sumtype.SumType!StateTT; 36 37 /// The states and state specific data. 38 StateT state; 39 40 /// Helper function to convert the return type to `StateT`. 41 static StateT opCall(T)(auto ref T a) { 42 return StateT(a); 43 } 44 45 /// Returns: true if the fsm is in the specified state. 46 bool isState(Ts...)() { 47 import std.meta : AliasSeq; 48 49 template Fns(Ts...) { 50 static if (Ts.length == 0) { 51 alias Fns = AliasSeq!(); 52 } else static if (Ts.length == 1) { 53 alias Fns = AliasSeq!((Ts[0] a) => true); 54 } else { 55 alias Fns = AliasSeq!(Fns!(Ts[0 .. $ / 2]), Fns!(Ts[$ / 2 .. $])); 56 } 57 } 58 59 try { 60 return sumtype.tryMatch!(Fns!Ts)(state); 61 } catch (Exception e) { 62 } 63 return false; 64 } 65 } 66 67 /// Transition to the next state. 68 template next(handlers...) { 69 void next(Self)(auto ref Self self) if (is(Self : Fsm!StateT, StateT...)) { 70 static import sumtype; 71 72 auto nextSt = sumtype.match!handlers(self.state); 73 logger.tracef("state: %s -> %s", self.state.toString, nextSt.toString); 74 self.state = nextSt; 75 } 76 } 77 78 /// Act on the current state. Use `(ref S)` to modify the states data. 79 template act(handlers...) { 80 void act(Self)(auto ref Self self) if (is(Self : Fsm!StateT, StateT...)) { 81 static import sumtype; 82 83 logger.trace("act: ", self.state.toString); 84 sumtype.match!handlers(self.state); 85 } 86 } 87 88 @("shall transition the fsm from A to B|C") 89 unittest { 90 struct Global { 91 int x; 92 } 93 94 struct A { 95 } 96 97 struct B { 98 int x; 99 } 100 101 struct C { 102 bool x; 103 } 104 105 Global global; 106 Fsm!(A, B, C) fsm; 107 108 while (!fsm.isState!(B, C)) { 109 fsm.next!((A a) { global.x++; return fsm(B(0)); }, (B a) { 110 if (a.x > 3) 111 return fsm(C(true)); 112 return fsm(a); 113 }, (C a) { return fsm(a); }); 114 115 fsm.act!((A a) {}, (ref B a) { a.x++; }, (C a) {}); 116 } 117 118 global.x.shouldEqual(1); 119 } 120 121 /** Hold a mapping between a Type and data. 122 * 123 * The get function is used to get the corresponding data. 124 * 125 * This is useful when e.g. combined with a state machine to retrieve the state 126 * local data if a state is represented as a type. 127 * 128 * Params: 129 * RawDataT = type holding the data, retrieved via opIndex 130 * Ts = the types mapping to RawDataT by their position 131 */ 132 struct TypeDataMap(RawDataT, Ts...) 133 if (is(RawDataT : DataT!Args, alias DataT, Args...)) { 134 alias SrcT = Ts; 135 RawDataT data; 136 137 this(RawDataT a) { 138 this.data = a; 139 } 140 141 void opAssign(RawDataT a) { 142 this.data = a; 143 } 144 145 static if (is(RawDataT : DataT!Args, alias DataT, Args...)) 146 static assert(Ts.length == Args.length, 147 "Mismatch between Tuple and TypeMap template arguments"); 148 } 149 150 auto ref get(T, TMap)(auto ref TMap tmap) 151 if (is(TMap : TypeDataMap!(W, SrcT), W, SrcT...)) { 152 template Index(size_t Idx, T, Ts...) { 153 static if (Ts.length == 0) { 154 static assert(0, "Type " ~ T.stringof ~ " not found in the TypeMap"); 155 } else static if (is(T == Ts[0])) { 156 enum Index = Idx; 157 } else { 158 enum Index = Index!(Idx + 1, T, Ts[1 .. $]); 159 } 160 } 161 162 return tmap.data[Index!(0, T, TMap.SrcT)]; 163 } 164 165 @("shall retrieve the data for the type") 166 unittest { 167 import std.typecons : Tuple; 168 169 TypeDataMap!(Tuple!(int, bool), bool, int) a; 170 static assert(is(typeof(a.get!bool) == int), "wrong type"); 171 a.data[1] = true; 172 assert(a.get!int == true); 173 }