1 /**
2 Copyright: Copyright (c) 2021, 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 Q-learning algorithm for training the schema generator.
11 
12 Each mutation subtype has a state in range [0,100]. It determins the
13 probability that a mutant of that kind is part of the intermediate schema
14 generation.
15 
16 The state is updated with feedback from if the schema successfully compiled and
17 executed the test suite OK.
18 */
19 module dextool.plugin.mutate.backend.analyze.schema_ml;
20 
21 import std.algorithm : joiner, clamp, min, max, filter;
22 import std.random : uniform01, MinstdRand0, unpredictableSeed;
23 import std.range : only;
24 
25 import dextool.plugin.mutate.backend.type : Mutation;
26 
27 @safe:
28 
29 enum SchemaStatus {
30     /// no status exist
31     none,
32     /// the schema compiled and the test suite executed OK
33     ok,
34     /// either it failed to compile or the test suite failed
35     broken
36 }
37 
38 struct SchemaQ {
39     import std.traits : EnumMembers;
40     import my.hash;
41     import my.path : Path;
42 
43     static immutable MinState = 0;
44     static immutable MaxState = 1000;
45     static immutable LearnRate = 0.01;
46 
47     alias StatusData = Mutation.Kind[]delegate(SchemaStatus);
48 
49     MinstdRand0 rnd0;
50     int[Mutation.Kind][Checksum64] state;
51     Checksum64[Path] pathCache;
52 
53     static auto make() {
54         return SchemaQ(MinstdRand0(unpredictableSeed));
55     }
56 
57     this(MinstdRand0 rnd) {
58         this.rnd0 = rnd;
59     }
60 
61     this(typeof(state) st) {
62         rnd0 = MinstdRand0(unpredictableSeed);
63         state = st;
64     }
65 
66     /// Deep copy.
67     SchemaQ dup() {
68         typeof(state) st;
69         foreach (a; state.byKeyValue) {
70             int[Mutation.Kind] v;
71             foreach (b; a.value.byKeyValue) {
72                 v[b.key] = b.value;
73             }
74             st[a.key] = v;
75         }
76         return SchemaQ(st);
77     }
78 
79     /// Add a state for the `p` if it doesn't exist.
80     void addIfNew(const Path p) {
81         if (checksum(p) !in state) {
82             state[checksum(p)] = (int[Mutation.Kind]).init;
83         }
84     }
85 
86     /// Update the state for all mutants.
87     void update(const Path path, scope StatusData data) {
88         import std.math : round;
89 
90         addIfNew(path);
91         const ch = checksum(path);
92 
93         double[Mutation.Kind] change;
94 
95         // punish
96         foreach (k; data(SchemaStatus.broken))
97             change.update(k, () => (1.0 - LearnRate), (ref double x) {
98                 x -= LearnRate;
99             });
100         // reward
101         foreach (k; data(SchemaStatus.ok))
102             change.update(k, () => (1.0 + LearnRate), (ref double x) {
103                 x += LearnRate;
104             });
105 
106         // apply change
107         foreach (v; change.byKeyValue.filter!(a => a.value != 1.0)) {
108             state[ch].update(v.key, () => cast(int) round(MaxState * v.value), (ref int x) {
109                 if (v.value > 1.0)
110                     x = max(x + 1, cast(int) round(x * v.value));
111                 else
112                     x = min(x - 1, cast(int) round(x * v.value));
113             });
114         }
115 
116         // fix probability to be max P(1)
117         foreach (k; change.byKey) {
118             state[ch].update(k, () => MaxState, (ref int x) {
119                 x = clamp(x, MinState, MaxState);
120             });
121         }
122     }
123 
124     /** To allow those with zero probability to self heal give them a random +1 now and then.
125      */
126     void scatterTick() nothrow {
127         foreach (p; state.byKeyValue) {
128             foreach (k; p.value.byKeyValue.filter!(a => a.value == 0 && uniform01(rnd0) < 0.05)) {
129                 state[p.key][k.key] = 1;
130             }
131         }
132     }
133 
134     /** Roll the dice to see if the mutant should be used.
135      *
136      * Params:
137      *  p = path the mutant is located at.
138      *  k = kind of mutant
139      *  threshold = the mutants probability must be above the threshold
140      *  otherwise it will automatically fail.
141      *
142      * Return: true if the roll is positive, use the mutant.
143      */
144     bool use(const Path p, const Mutation.Kind k, const double threshold) {
145         const s = getState(p, k) / cast(double) MaxState;
146         return s >= threshold && uniform01(rnd0) < s;
147     }
148 
149     /// Returns: true if the probability of success is zero.
150     bool isZero(const Path p, const Mutation.Kind k) {
151         return getState(p, k) == 0;
152     }
153 
154     private Checksum64 checksum(const Path p) {
155         return pathCache.require(p, makeChecksum64(cast(const(ubyte)[]) p.toString));
156     }
157 
158     private int getState(const Path p, const Mutation.Kind k) {
159         if (auto st = checksum(p) in state)
160             return (*st).require(k, MaxState);
161         return MaxState;
162     }
163 }
164 
165 struct Feature {
166     import my.hash;
167 
168     // The path is extremly important because it allows the tool to clear out old data.
169     Checksum64 path;
170 
171     Mutation.Kind kind;
172 
173     Checksum64[] context;
174 
175     size_t toHash() @safe pure nothrow const @nogc {
176         auto rval = (cast(size_t) kind).hashOf();
177         rval = path.c0.hashOf(rval);
178         foreach (a; context)
179             rval = a.c0.hashOf(rval);
180         return rval;
181     }
182 }
183 
184 @("shall update the table")
185 unittest {
186     import std.random : MinstdRand0;
187     import my.path : Path;
188 
189     const foo = Path("foo");
190     SchemaQ q;
191     q.rnd0 = MinstdRand0(42);
192 
193     Mutation.Kind[] r1(SchemaStatus s) {
194         if (s == SchemaStatus.broken)
195             return [Mutation.Kind.rorLE];
196         if (s == SchemaStatus.ok)
197             return [Mutation.Kind.rorLT];
198         return null;
199     }
200 
201     q.update(foo, &r1);
202     const ch = q.pathCache[foo];
203     assert(q.state[ch][Mutation.Kind.rorLE] == 990);
204     assert(q.state[ch][Mutation.Kind.rorLT] == SchemaQ.MaxState);
205 
206     Mutation.Kind[] r2(SchemaStatus s) {
207         if (s == SchemaStatus.broken)
208             return [Mutation.Kind.rorLE, Mutation.Kind.rorLT];
209         if (s == SchemaStatus.ok)
210             return [Mutation.Kind.rorLT];
211         return null;
212     }
213 
214     q.update(foo, &r2);
215     assert(q.state[ch][Mutation.Kind.rorLE] == 980);
216     // in the last run it was one broken and one OK thus the change where 1.0.
217     assert(q.state[ch][Mutation.Kind.rorLT] == SchemaQ.MaxState);
218 }
219 
220 struct SchemaSizeQ {
221     static immutable LearnRate = 0.01;
222     static immutable Threshold = 0.9;
223 
224     /// min size of a schema.
225     long minSize;
226     /// max size of a schema.
227     long maxSize;
228     /// current size of that are used to generate scheman.
229     private long currentSize_;
230     /// total number of mutants that are to be tested when schemas are generated.
231     long testMutantsSize;
232 
233     static auto make(const long minSize, const long maxSize) {
234         return SchemaSizeQ(minSize, maxSize, maxSize);
235     }
236 
237     void updateSize(const double sz) @safe pure nothrow @nogc {
238         currentSize_ = clamp(cast(long) sz, minSize, maxSize);
239     }
240 
241     long currentSize() @safe pure nothrow const @nogc {
242         return currentSize_;
243     }
244 
245     /**
246      * Params:
247      *  schemaMutants = mutants in the schema
248      */
249     void update(const SchemaStatus status, const long schemaMutants) {
250         double newValue = currentSize_;
251 
252         double adjust = 1.0;
253         // ensure there is at least some change.
254         long fixed;
255 
256         final switch (status) {
257         case SchemaStatus.none:
258             break;
259         case SchemaStatus.ok:
260             // only try to increase the size if the schema where close to max
261             if (maxSize > 0 && schemaMutants > currentSize_ * Threshold) {
262                 adjust += LearnRate * (cast(double) schemaMutants / cast(double) maxSize);
263                 fixed++;
264             }
265             break;
266         case SchemaStatus.broken:
267             if (maxSize > 0 && schemaMutants > currentSize_ * Threshold) {
268                 adjust -= LearnRate * (cast(double) schemaMutants / cast(double) maxSize);
269                 fixed--;
270             }
271             break;
272         }
273 
274         updateSize(newValue * adjust + fixed);
275     }
276 
277     /// No schema with `currentSize_` where generated.
278     void noCurrentSize() {
279         if (maxSize > 0 && testMutantsSize > currentSize_ * Threshold)
280             updateSize(currentSize_ - currentSize_ * LearnRate);
281     }
282 
283     string toString() @safe pure const {
284         import std.format : format;
285 
286         return format!"SchemaSizeQ(min:%s, max:%s, current:%s, testMutantsSize:%s)"(minSize,
287                 maxSize, currentSize_, testMutantsSize);
288     }
289 }