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 }