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 std.format : format; 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 /// Log messages of the last state transition (next). 41 string logNext; 42 43 /// Helper function to convert the return type to `StateT`. 44 static StateT opCall(T)(auto ref T a) { 45 return StateT(a); 46 } 47 48 /// Returns: true if the fsm is in the specified state. 49 bool isState(Ts...)() { 50 import std.meta : AliasSeq; 51 52 template Fns(Ts...) { 53 static if (Ts.length == 0) { 54 alias Fns = AliasSeq!(); 55 } else static if (Ts.length == 1) { 56 alias Fns = AliasSeq!((Ts[0] a) => true); 57 } else { 58 alias Fns = AliasSeq!(Fns!(Ts[0 .. $ / 2]), Fns!(Ts[$ / 2 .. $])); 59 } 60 } 61 62 try { 63 return sumtype.tryMatch!(Fns!Ts)(state); 64 } catch (Exception e) { 65 } 66 return false; 67 } 68 } 69 70 /// Transition to the next state. 71 template next(handlers...) { 72 void next(Self)(auto ref Self self) if (is(Self : Fsm!StateT, StateT...)) { 73 static import sumtype; 74 75 auto nextSt = sumtype.match!handlers(self.state); 76 self.logNext = format!"%s -> %s"(self.state, nextSt); 77 78 self.state = nextSt; 79 } 80 } 81 82 /// Act on the current state. Use `(ref S)` to modify the states data. 83 template act(handlers...) { 84 void act(Self)(auto ref Self self) if (is(Self : Fsm!StateT, StateT...)) { 85 static import sumtype; 86 87 sumtype.match!handlers(self.state); 88 } 89 } 90 91 @("shall transition the fsm from A to B|C") 92 unittest { 93 struct Global { 94 int x; 95 } 96 97 struct A { 98 } 99 100 struct B { 101 int x; 102 } 103 104 struct C { 105 bool x; 106 } 107 108 Global global; 109 Fsm!(A, B, C) fsm; 110 111 while (!fsm.isState!(B, C)) { 112 fsm.next!((A a) { global.x++; return fsm(B(0)); }, (B a) { 113 if (a.x > 3) 114 return fsm(C(true)); 115 return fsm(a); 116 }, (C a) { return fsm(a); }); 117 118 fsm.act!((A a) {}, (ref B a) { a.x++; }, (C a) {}); 119 } 120 121 global.x.shouldEqual(1); 122 } 123 124 /** Hold a mapping between a Type and data. 125 * 126 * The get function is used to get the corresponding data. 127 * 128 * This is useful when e.g. combined with a state machine to retrieve the state 129 * local data if a state is represented as a type. 130 * 131 * Params: 132 * RawDataT = type holding the data, retrieved via opIndex 133 * Ts = the types mapping to RawDataT by their position 134 */ 135 struct TypeDataMap(RawDataT, Ts...) 136 if (is(RawDataT : DataT!Args, alias DataT, Args...)) { 137 alias SrcT = Ts; 138 RawDataT data; 139 140 this(RawDataT a) { 141 this.data = a; 142 } 143 144 void opAssign(RawDataT a) { 145 this.data = a; 146 } 147 148 static if (is(RawDataT : DataT!Args, alias DataT, Args...)) 149 static assert(Ts.length == Args.length, 150 "Mismatch between Tuple and TypeMap template arguments"); 151 } 152 153 auto ref get(T, TMap)(auto ref TMap tmap) 154 if (is(TMap : TypeDataMap!(W, SrcT), W, SrcT...)) { 155 template Index(size_t Idx, T, Ts...) { 156 static if (Ts.length == 0) { 157 static assert(0, "Type " ~ T.stringof ~ " not found in the TypeMap"); 158 } else static if (is(T == Ts[0])) { 159 enum Index = Idx; 160 } else { 161 enum Index = Index!(Idx + 1, T, Ts[1 .. $]); 162 } 163 } 164 165 return tmap.data[Index!(0, T, TMap.SrcT)]; 166 } 167 168 @("shall retrieve the data for the type") 169 unittest { 170 import std.typecons : Tuple; 171 172 TypeDataMap!(Tuple!(int, bool), bool, int) a; 173 static assert(is(typeof(a.get!bool) == int), "wrong type"); 174 a.data[1] = true; 175 assert(a.get!int == true); 176 }