1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
let strf = Printf.sprintf
let saturating_add x y = if x > max_int - y then max_int else x + y
type id = { file : string; line : int; col : int; rewrite : string }
let rewrites =
[
"not";
"lt";
"le";
"gt";
"ge";
"eq";
"neq";
"add";
"sub";
"fadd";
"fsub";
"and";
"or";
]
let is_rewrite r = List.exists (String.equal r) rewrites
let id_to_string id = strf "%s:%d:%d:%s" id.file id.line id.col id.rewrite
let pp_id ppf id = Format.pp_print_string ppf (id_to_string id)
let compare_id a b =
let c = String.compare a.file b.file in
if c <> 0 then c
else
let c = Int.compare a.line b.line in
if c <> 0 then c
else
let c = Int.compare a.col b.col in
if c <> 0 then c else String.compare a.rewrite b.rewrite
type site = {
line : int;
col : int;
rewrite : string;
before : string;
after : string;
dismissed : string option;
}
type mutant = {
id : id;
before : string;
after : string;
dismissed : string option;
}
let compare_mutant a b = compare_id a.id b.id
type entry = {
file : string;
sites : site array;
reach : int array;
seen : int array;
base : int array;
mutable armed_index : int;
}
type state = {
mutable registry : entry list;
mutable epoch : int;
mutable marked : (entry * int) list;
mutable armed : entry list;
mutable budget : int;
}
let state =
{ registry = []; epoch = 1; marked = []; armed = []; budget = max_int }
exception Runaway of { id : id; hits : int; budget : int }
let site_mutant entry i =
let s = entry.sites.(i) in
{
id = { file = entry.file; line = s.line; col = s.col; rewrite = s.rewrite };
before = s.before;
after = s.after;
dismissed = s.dismissed;
}
let err_site ~file i fmt =
Printf.ksprintf
(fun m ->
invalid_arg (strf "Windtrap_runtime.Mutate: %s: site %d: %s" file i m))
fmt
let validate ~file sites =
Array.iteri
(fun i s ->
if s.line < 1 then err_site ~file i "line %d is not 1-based" s.line;
if s.col < 0 then err_site ~file i "negative column %d" s.col;
if not (is_rewrite s.rewrite) then
err_site ~file i "unknown rewrite %S" s.rewrite)
sites
let sites_equal a b =
let site_equal a b =
a.line = b.line && a.col = b.col
&& String.equal a.rewrite b.rewrite
&& String.equal a.before b.before
&& String.equal a.after b.after
&& Option.equal String.equal a.dismissed b.dismissed
in
Array.length a = Array.length b && Array.for_all2 site_equal a b
let register ~file ~sites =
validate ~file sites;
match List.find_opt (fun e -> String.equal e.file file) state.registry with
| Some prior when not (sites_equal prior.sites sites) ->
Instr.warn
"%s: conflicting instrumentation tables in one executable (stale build \
artifacts? rebuild from clean); ignoring one module's sites"
file;
Fun.const false
| Some _ | None ->
let n = Array.length sites in
let entry =
{
file;
sites;
reach = Array.make n 0;
seen = Array.make n 0;
base = Array.make n (-1);
armed_index = -1;
}
in
state.registry <- entry :: state.registry;
fun i ->
let hits = saturating_add entry.reach.(i) 1 in
entry.reach.(i) <- hits;
if entry.seen.(i) <> state.epoch then begin
entry.seen.(i) <- state.epoch;
if entry.base.(i) < 0 then begin
entry.base.(i) <- hits - 1;
state.marked <- (entry, i) :: state.marked
end
end;
if i <> entry.armed_index then false
else if hits <= state.budget then true
else
raise
(Runaway
{ id = (site_mutant entry i).id; hits; budget = state.budget })
let catalogue () =
let mutants entry =
List.init (Array.length entry.sites) (site_mutant entry)
in
List.sort_uniq compare_mutant (List.concat_map mutants state.registry)
type arm_error =
| Malformed of { spec : string; reason : string }
| Uncatalogued of { id : id }
| Unmatched of { id : id; candidates : mutant list }
| Ambiguous of { id : id; candidates : mutant list }
let pp_candidates ppf mutants =
let shown = 6 in
List.iteri
(fun i m -> if i < shown then Format.fprintf ppf "@\n %a" pp_id m.id)
mutants;
let more = List.length mutants - shown in
if more > 0 then Format.fprintf ppf "@\n (and %d more)" more
let pp_arm_error ppf = function
| Malformed { spec; reason } ->
Format.fprintf ppf
"%S is not a mutant identifier: %s; expected \
<file>:<line>:<col>:<rewrite>"
spec reason
| Uncatalogued { id } ->
Format.fprintf ppf
"%a: not this executable's mutant; it catalogues no site in %s (if you \
expected one, is the library under test instrumented with \
ppx_windtrap.mutate?)"
pp_id id id.file
| Unmatched { id; candidates } ->
Format.fprintf ppf "%a: no such mutation site; %s has these:%a" pp_id id
id.file pp_candidates candidates
| Ambiguous { id; candidates } ->
Format.fprintf ppf
"%a: names %d mutation sites, so no identifier can tell them apart (a \
rewriter duplicating locations?); dismiss the expression with \
[@mutate off] or exclude the file:%a"
pp_id id (List.length candidates) pp_candidates candidates
let is_digit = function '0' .. '9' -> true | _ -> false
let parse_nat s =
if s = "" || not (String.for_all is_digit s) then None
else int_of_string_opt s
let id_of_string spec =
let malformed fmt =
Printf.ksprintf (fun reason -> Error (Malformed { spec; reason })) fmt
in
let number field s =
match parse_nat s with
| Some n -> Ok n
| None -> malformed "invalid %s %S" field s
in
let ( let* ) = Result.bind in
match List.rev (String.split_on_char ':' spec) with
| [] | [ _ ] -> malformed "no ':' separator"
| rewrite :: _ when not (is_rewrite rewrite) ->
malformed "unknown rewrite %S (expected one of %s)" rewrite
(String.concat ", " rewrites)
| [ _; _ ] -> malformed "no position before the rewrite"
| [ _; col; _ ] ->
let* _ = number "column" col in
malformed "no line number"
| rewrite :: col :: line :: file ->
let* col = number "column" col in
let* line = number "line" line in
let file = String.concat ":" (List.rev file) in
if file = "" then malformed "empty file name"
else if line < 1 then malformed "line numbers are 1-based"
else Ok { file; line; col; rewrite }
let disarm () =
List.iter (fun entry -> entry.armed_index <- -1) state.armed;
state.armed <- [];
state.budget <- max_int
let arm ?(budget = max_int) (id : id) =
if budget <= 0 then
invalid_arg "Windtrap_runtime.Mutate.arm: budget must be positive";
disarm ();
let catalogues entry =
String.equal entry.file id.file && Array.length entry.sites > 0
in
match List.filter catalogues state.registry with
| [] -> Error (Uncatalogued { id })
| first :: _ as entries -> (
let names i =
let s = first.sites.(i) in
s.line = id.line && s.col = id.col && String.equal s.rewrite id.rewrite
in
match List.filter names (List.init (Array.length first.sites) Fun.id) with
| [] ->
let candidates =
List.filter (fun m -> String.equal m.id.file id.file) (catalogue ())
in
Error (Unmatched { id; candidates })
| [ i ] ->
List.iter (fun entry -> entry.armed_index <- i) entries;
state.armed <- entries;
state.budget <- budget;
Ok (site_mutant first i)
| indices ->
let candidates = List.map (site_mutant first) indices in
Error (Ambiguous { id; candidates }))
let armed_hits () =
List.fold_left
(fun acc entry -> saturating_add acc entry.reach.(entry.armed_index))
0 state.armed
type reached = { mutant : mutant; hits : int }
let next_epoch () = state.epoch <- state.epoch + 1
let drain () =
let marked = state.marked in
state.marked <- [];
let reached (entry, i) =
let hits = entry.reach.(i) - entry.base.(i) in
entry.base.(i) <- -1;
{ mutant = site_mutant entry i; hits }
in
let merge acc r =
match acc with
| prev :: acc when compare_mutant prev.mutant r.mutant = 0 ->
{ prev with hits = saturating_add prev.hits r.hits } :: acc
| acc -> r :: acc
in
let by_mutant a b = compare_mutant a.mutant b.mutant in
List.rev
(List.fold_left merge []
(List.sort by_mutant (List.rev_map reached marked)))
let reset_reach () =
List.iter
(fun entry ->
Array.fill entry.reach 0 (Array.length entry.reach) 0;
Array.fill entry.base 0 (Array.length entry.base) (-1))
state.registry;
state.marked <- [];
next_epoch ()
let () =
Printexc.register_printer (function
| Runaway { id; hits; budget } ->
Some
(strf
"Windtrap_runtime.Mutate.Runaway: %s evaluated %d times (budget \
%d)"
(id_to_string id) hits budget)
| _ -> None)