Source file instrument.ml
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
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
open Ppxlib
open Ast_builder.Default
module Exp = Ast_helper.Exp
let coverage_attribute { attr_name; attr_payload; attr_loc = loc } =
if not (String.equal attr_name.txt "coverage") then `None
else
let payload =
match attr_payload with
| PStr [ { pstr_desc = Pstr_eval (payload, _); _ } ] ->
Some payload.pexp_desc
| _ -> None
in
match payload with
| Some (Pexp_ident { txt = Lident "off"; _ })
| Some
(Pexp_apply
( { pexp_desc = Pexp_ident { txt = Lident "off"; _ }; _ },
[ (Nolabel, { pexp_desc = Pexp_constant (Pconst_string _); _ }) ]
)) ->
`Off
| Some (Pexp_ident { txt = Lident "on"; _ }) -> `On
| Some (Pexp_ident { txt = Lident "exclude_file"; _ }) -> `Exclude_file
| _ -> Location.raise_errorf ~loc "Bad payload in coverage attribute."
let err_misplaced ~loc spelling =
Location.raise_errorf ~loc "coverage %s is not allowed here." spelling
let is_off attributes =
List.fold_left
(fun off attribute ->
match coverage_attribute attribute with
| `None -> off
| `Off -> true
| `On -> err_misplaced ~loc:attribute.attr_loc "on"
| `Exclude_file -> err_misplaced ~loc:attribute.attr_loc "exclude_file")
false attributes
let is_tail_mod_cons attributes =
List.exists
(fun { attr_name = { txt; _ }; _ } ->
txt = "tail_mod_cons" || txt = "ocaml.tail_mod_cons")
attributes
let excludes_file = function
| { pstr_desc = Pstr_attribute attribute; _ } ->
coverage_attribute attribute = `Exclude_file
| _ -> false
let is_test_item si =
let marked =
List.exists (fun a -> String.equal a.attr_name.txt "windtrap.test")
in
match si.pstr_desc with
| Pstr_value (_, bindings) ->
List.exists (fun b -> marked b.pvb_attributes) bindings
| Pstr_module mb -> marked mb.pmb_attributes
| _ -> false
type point = { key : int; start_ofs : int; end_ofs : int }
type state = {
mutable rev_points : point list;
mutable count : int;
mutable uses_post : bool;
}
let point st ~key (extent : location) =
let rec find index = function
| p :: _ when p.key = key -> index
| _ :: older -> find (index - 1) older
| [] ->
let start_ofs = extent.loc_start.pos_cnum in
let end_ofs = extent.loc_end.pos_cnum in
st.rev_points <- { key; start_ofs; end_ofs } :: st.rev_points;
st.count <- st.count + 1;
st.count - 1
in
find (st.count - 1) st.rev_points
let mark st ~anchor ~at_end ~extent ~post e =
let at = anchor.pexp_loc in
if at.loc_ghost || is_off anchor.pexp_attributes then e
else
let key =
if at_end then at.loc_end.pos_cnum - 1 else at.loc_start.pos_cnum
in
let loc = e.pexp_loc in
let index = eint ~loc (point st ~key extent) in
if post then begin
st.uses_post <- true;
[%expr ___windtrap_post_visit___ [%e index] [%e e]]
end
else
[%expr
___windtrap_visit___ [%e index];
[%e e]]
let entry st ?extent e =
let extent = Option.value extent ~default:e.pexp_loc in
mark st ~anchor:e ~at_end:false ~extent ~post:false e
let arm st case =
match case.pc_rhs with
| [%expr assert false] | { pexp_desc = Pexp_unreachable; _ } -> case
| { pexp_attributes; _ } when is_off pexp_attributes -> case
| rhs ->
let pat = case.pc_lhs.ppat_loc in
let extent =
if
pat.loc_ghost
|| pat.loc_start.pos_cnum > rhs.pexp_loc.loc_start.pos_cnum
then rhs.pexp_loc
else { rhs.pexp_loc with loc_start = pat.loc_start }
in
let pc_guard = Option.map (entry st) case.pc_guard in
let pc_rhs = entry st ~extent rhs in
{ case with pc_guard; pc_rhs }
let rec is_trivial_syntactic_value e =
match e.pexp_desc with
| Pexp_function _ | Pexp_poly _ | Pexp_ident _ | Pexp_constant _
| Pexp_construct (_, None) ->
true
| Pexp_constraint (inner, _) | Pexp_coerce (inner, _, _) ->
is_trivial_syntactic_value inner
| _ -> false
let trivial_primitives =
String.split_on_char ' '
"&& & not = <> < <= > >= == != ref ! := @ ^ + - * / +. -. *. /. mod land \
lor lxor lsl lsr asr raise raise_notrace failwith ignore ##"
let is_trivial_function e =
match e.pexp_desc with
| Pexp_ident { txt = Lident name; _ } -> List.mem name trivial_primitives
| Pexp_ident
{
txt =
Ldot (Lident "Sys", "opaque_identity") | Ldot (Lident "Obj", "magic");
_;
} ->
true
| _ -> false
let holds_tail_call e =
match e.pexp_desc with
| Pexp_apply (callee, _) -> not (is_trivial_function callee)
| Pexp_send _ | Pexp_new _ | Pexp_let _ | Pexp_letmodule _
| Pexp_letexception _ | Pexp_open _ | Pexp_match _ | Pexp_try _
| Pexp_ifthenelse _ | Pexp_sequence _ | Pexp_letop _ | Pexp_constraint _
| Pexp_coerce _ ->
true
| _ -> false
type position = Tail | Nontail | Before of expression | Observed
let is_tail = function Tail -> true | Nontail | Before _ | Observed -> false
let inherited position = if is_tail position then Tail else Nontail
let rec head_callee e =
match e.pexp_desc with
| Pexp_apply (f, _) when not (is_off e.pexp_attributes) -> head_callee f
| _ -> e
let out_edge st position ~callee e =
match position with
| Tail | Observed -> e
| Nontail ->
mark st ~anchor:callee ~at_end:true ~extent:e.pexp_loc ~post:true e
| Before next ->
mark st ~anchor:next ~at_end:false ~extent:e.pexp_loc ~post:true e
class instrumenter st =
object (self)
inherit Ast_traverse.map as super
val mutable suppressed = false
val mutable in_tmc_body = false
method private binding traverse (binding : value_binding) =
let outer = in_tmc_body in
if is_tail_mod_cons binding.pvb_attributes then in_tmc_body <- true;
let pvb_expr = traverse binding.pvb_expr in
in_tmc_body <- outer;
{ binding with pvb_expr }
method! expression e =
if suppressed then e
else
let rec traverse position e =
if is_off e.pexp_attributes then e
else
let loc = e.pexp_loc and attrs = e.pexp_attributes in
match e.pexp_desc with
| Pexp_apply
( (([%expr ( |> )] | [%expr ( |. )]) as pipe),
[ (l, lhs); (l', rhs) ] ) ->
let lhs' = traverse (Before rhs) lhs in
let rhs' = traverse Observed rhs in
let apply =
Exp.apply ~loc ~attrs pipe [ (l, lhs'); (l', rhs') ]
in
out_edge st position ~callee:(head_callee rhs) apply
| Pexp_apply
(([%expr ( || )] | [%expr ( or )]), [ (_, left); (_, right) ])
->
let was_true operand =
mark st ~anchor:operand ~at_end:true ~extent:operand.pexp_loc
~post:false [%expr true]
in
let left_true = was_true left in
let right' =
match right.pexp_desc with
| Pexp_apply (([%expr ( || )] | [%expr ( or )]), _) ->
traverse (inherited position) right
| _ when is_tail position && holds_tail_call right ->
traverse Tail right
| _ ->
let right' = traverse Nontail right in
let right_true = was_true right in
[%expr if [%e right'] then [%e right_true] else false]
in
let left' = traverse Nontail left in
[%expr if [%e left'] then [%e left_true] else [%e right']]
| Pexp_apply (fn, args) ->
let args =
match (fn, args) with
| ([%expr ( && )] | [%expr ( & )]), [ (l, left); (l', right) ]
->
let left = traverse Nontail left in
let right = traverse (inherited position) right in
[ (l, left); (l', entry st right) ]
| ( [%expr ( @@ )],
[ (l, ({ pexp_desc = Pexp_apply _; _ } as f)); (l', x) ] )
->
let f = traverse Observed f in
[ (l, f); (l', traverse Nontail x) ]
| _ -> List.map (fun (l, x) -> (l, traverse Nontail x)) args
in
let fn' =
match fn.pexp_desc with
| Pexp_new _ -> fn
| Pexp_send _ -> traverse Observed fn
| _ -> traverse Nontail fn
in
let apply = Exp.apply ~loc ~attrs fn' args in
let all_labelled =
List.for_all
(function
| Nolabel, _ -> false
| (Labelled _ | Optional _), _ -> true)
args
in
if in_tmc_body || all_labelled || is_trivial_function fn then
apply
else
let callee =
match (fn, args) with
| [%expr ( @@ )], [ (_, f); _ ] -> f
| _ -> fn
in
out_edge st position ~callee apply
| Pexp_send (obj, meth) ->
let send = Exp.send ~loc ~attrs (traverse Nontail obj) meth in
if in_tmc_body then send
else out_edge st position ~callee:send send
| Pexp_new _ -> out_edge st position ~callee:e e
| Pexp_assert [%expr false] -> e
| Pexp_assert inner ->
let assertion =
Exp.assert_ ~loc ~attrs (traverse Nontail inner)
in
mark st ~anchor:inner ~at_end:false ~extent:loc ~post:true
assertion
| Pexp_function (params, constraint_, body) ->
let params = List.map param params in
let rec leaf body =
match body.pexp_desc with
| Pexp_function _ -> body
| Pexp_constraint (inner, t) ->
{ body with pexp_desc = Pexp_constraint (leaf inner, t) }
| Pexp_coerce (inner, t, t') ->
{ body with pexp_desc = Pexp_coerce (leaf inner, t, t') }
| _ -> entry st body
in
let body =
match body with
| Pfunction_body body ->
Pfunction_body (leaf (traverse Tail body))
| Pfunction_cases (cases, cases_loc, cases_attrs) ->
Pfunction_cases (arms Tail cases, cases_loc, cases_attrs)
in
{ e with pexp_desc = Pexp_function (params, constraint_, body) }
| Pexp_match (scrutinee, cases) ->
let cases = arms (inherited position) cases in
Exp.match_ ~loc ~attrs (traverse Observed scrutinee) cases
| Pexp_try (body, cases) ->
let cases = arms (inherited position) cases in
Exp.try_ ~loc ~attrs (traverse Nontail body) cases
| Pexp_ifthenelse (cond, then_, else_) ->
let cond = traverse Observed cond in
let then_ = traverse (inherited position) then_ in
let else_ =
Option.map
(fun else_ ->
entry st (traverse (inherited position) else_))
else_
in
Exp.ifthenelse ~loc ~attrs cond (entry st then_) else_
| Pexp_while (cond, body) ->
let cond = traverse Nontail cond in
Exp.while_ ~loc ~attrs cond (entry st (traverse Nontail body))
| Pexp_for (pat, init, bound, direction, body) ->
let init = traverse Nontail init in
let bound = traverse Nontail bound in
Exp.for_ ~loc ~attrs pat init bound direction
(entry st (traverse Nontail body))
| Pexp_lazy body ->
let body = traverse Tail body in
Exp.lazy_ ~loc ~attrs
(if is_trivial_syntactic_value body then body
else entry st body)
| Pexp_poly (body, t) ->
let body = traverse Tail body in
let body =
match body.pexp_desc with
| Pexp_function _ -> body
| _ -> entry st body
in
Exp.poly ~loc ~attrs body t
| Pexp_letop { let_; ands; body } ->
let operand op =
{ op with pbop_exp = traverse Nontail op.pbop_exp }
in
let let_ = operand let_ in
let ands = List.map operand ands in
Exp.letop ~loc ~attrs let_ ands (entry st (traverse Tail body))
| Pexp_let (rec_flag, bindings, body) ->
let bound =
match bindings with [ _ ] -> Before body | _ -> Nontail
in
let bindings =
List.map (self#binding (traverse bound)) bindings
in
Exp.let_ ~loc ~attrs rec_flag bindings
(traverse (inherited position) body)
| Pexp_sequence (first, second) ->
let second = traverse (inherited position) second in
let second =
match first.pexp_desc with
| Pexp_ifthenelse (_, _, None) -> entry st second
| _ -> second
in
Exp.sequence ~loc ~attrs (traverse (Before second) first) second
| Pexp_ident _ | Pexp_constant _ | Pexp_extension _
| Pexp_unreachable ->
e
| Pexp_tuple es ->
Exp.tuple ~loc ~attrs (List.map (traverse Nontail) es)
| Pexp_construct (c, arg) ->
Exp.construct ~loc ~attrs c (Option.map (traverse Nontail) arg)
| Pexp_variant (c, arg) ->
Exp.variant ~loc ~attrs c (Option.map (traverse Nontail) arg)
| Pexp_record (fields, base) ->
let fields =
List.map (fun (f, x) -> (f, traverse Nontail x)) fields
in
Exp.record ~loc ~attrs fields
(Option.map (traverse Nontail) base)
| Pexp_field (record, f) ->
Exp.field ~loc ~attrs (traverse Nontail record) f
| Pexp_setfield (record, f, value) ->
let record = traverse Nontail record in
Exp.setfield ~loc ~attrs record f (traverse Nontail value)
| Pexp_array es ->
Exp.array ~loc ~attrs (List.map (traverse Nontail) es)
| Pexp_constraint (inner, t) ->
Exp.constraint_ ~loc ~attrs
(traverse (inherited position) inner)
t
| Pexp_coerce (inner, t, t') ->
Exp.coerce ~loc ~attrs
(traverse (inherited position) inner)
t t'
| Pexp_setinstvar (f, value) ->
Exp.setinstvar ~loc ~attrs f (traverse Nontail value)
| Pexp_override fields ->
Exp.override ~loc ~attrs
(List.map (fun (f, x) -> (f, traverse Nontail x)) fields)
| Pexp_letmodule (m, module_expr, body) ->
let module_expr = self#module_expr module_expr in
Exp.letmodule ~loc ~attrs m module_expr
(traverse (inherited position) body)
| Pexp_letexception (c, body) ->
Exp.letexception ~loc ~attrs c
(traverse (inherited position) body)
| Pexp_open (decl, body) ->
let decl = self#open_declaration decl in
Exp.open_ ~loc ~attrs decl (traverse (inherited position) body)
| Pexp_newtype (t, body) ->
Exp.newtype ~loc ~attrs t (traverse (inherited position) body)
| Pexp_object c -> Exp.object_ ~loc ~attrs (self#class_structure c)
| Pexp_pack m -> Exp.pack ~loc ~attrs (self#module_expr m)
and param p =
match p.pparam_desc with
| Pparam_val (label, Some default, pat) ->
let default = entry st (traverse Nontail default) in
{ p with pparam_desc = Pparam_val (label, Some default, pat) }
| Pparam_val (_, None, _) | Pparam_newtype _ -> p
and arms position cases =
let case c =
let pc_guard = Option.map (traverse Nontail) c.pc_guard in
{ c with pc_guard; pc_rhs = traverse position c.pc_rhs }
in
List.map (arm st) (List.map case cases)
in
traverse Nontail e
method! class_expr ce =
if suppressed then ce
else
let ce = super#class_expr ce in
match ce.pcl_desc with
| Pcl_fun (label, default, pat, body) ->
let default = Option.map (entry st) default in
{ ce with pcl_desc = Pcl_fun (label, default, pat, body) }
| _ -> ce
method! class_field cf =
if suppressed then cf
else
let cf = super#class_field cf in
match cf.pcf_desc with
| Pcf_method (name, private_, Cfk_concrete (override, body)) ->
let body = Cfk_concrete (override, entry st body) in
{ cf with pcf_desc = Pcf_method (name, private_, body) }
| Pcf_initializer body ->
{ cf with pcf_desc = Pcf_initializer (entry st body) }
| _ -> cf
method! module_binding mb =
if is_off mb.pmb_attributes then mb else super#module_binding mb
method! structure_item si =
match si.pstr_desc with
| Pstr_attribute attribute ->
let loc = attribute.attr_loc in
(match coverage_attribute attribute with
| `None -> ()
| `Off ->
if suppressed then
Location.raise_errorf ~loc "Coverage is already off.";
suppressed <- true
| `On ->
if not suppressed then
Location.raise_errorf ~loc "Coverage is already on.";
suppressed <- false
| `Exclude_file -> err_misplaced ~loc "exclude_file");
si
| Pstr_value (rec_flag, bindings) when not suppressed ->
let binding b =
if is_off b.pvb_attributes then b
else self#binding self#expression b
in
{
si with
pstr_desc = Pstr_value (rec_flag, List.map binding bindings);
}
| _ -> super#structure_item si
method! structure items =
let outer = suppressed in
let items =
List.map
(fun si -> if is_test_item si then si else self#structure_item si)
items
in
suppressed <- outer;
items
method! extension x = x
method! attribute x = x
end
let module_name file =
let b = Buffer.create (String.length file + 16) in
Buffer.add_string b "Windtrap_cov___";
String.iter
(function
| ('A' .. 'Z' | 'a' .. 'z' | '0' .. '9' | '_') as c -> Buffer.add_char b c
| _ -> Buffer.add_string b "___")
file;
Buffer.contents b
let generated_module st ~file =
let loc = { (Location.in_file file) with loc_ghost = true } in
let name = module_name file in
let point p =
[%expr
{
Windtrap_runtime.Coverage.start_ofs = [%e eint ~loc p.start_ofs];
Windtrap_runtime.Coverage.end_ofs = [%e eint ~loc p.end_ofs];
}]
in
let visit =
[%stri
let ___windtrap_visit___ =
let counts = Array.make [%e eint ~loc st.count] 0 in
Windtrap_runtime.Coverage.register ~file:[%e estring ~loc file]
~points:[%e pexp_array ~loc (List.rev_map point st.rev_points)]
~counts;
fun index -> Windtrap_runtime.Coverage.visit counts index]
in
let post_visit =
[%stri
let ___windtrap_post_visit___ point_index result =
___windtrap_visit___ point_index;
result]
in
let items = if st.uses_post then [ visit; post_visit ] else [ visit ] in
let = [%stri [@@@ocaml.text "/*"]] in
[
stop_comment;
pstr_module ~loc
(module_binding ~loc ~name:{ txt = Some name; loc }
~expr:(pmod_structure ~loc items));
pstr_open ~loc
(open_infos ~loc ~override:Fresh
~expr:(pmod_ident ~loc { txt = Lident name; loc }));
stop_comment;
]
let is_ignored file =
List.mem file [ "//toplevel//"; "(stdin)" ]
|| List.mem (Filename.basename file) [ ".ocamlinit"; "topfind" ]
let transform_impl_file ctxt ast =
let file = Expansion_context.Base.input_name ctxt in
if is_ignored file || List.exists excludes_file ast then ast
else
let st = { rev_points = []; count = 0; uses_post = false } in
let instrumented = (new instrumenter st)#structure ast in
if st.count = 0 then ast else generated_module st ~file @ instrumented