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 }