Merge pull request #9511 from gasche/parmatch-exhaust-lazy

Parmatch: make `exhaust` (exhaustivity and fragility checking) lazy
master
Gabriel Scherer 2020-05-30 19:01:38 +02:00 committed by GitHub
commit 5c9d5db728
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 217 additions and 124 deletions

View File

@ -96,6 +96,12 @@ Working version
- #9604: refactoring of the ocamltest codebase.
(Nicolás Ojeda Bär, review by Gabriel Scherer and Sébastien Hinderer)
- #9498, #9511: make the pattern-matching analyzer more robust to
or-pattern explosion, by stopping after the first counter-example to
exhaustivity
(Gabriel Scherer, review by Luc Maranget, Thomas Refis and Florian Angeletti,
report by Alex Fedoseev through Hongbo Zhang)
### Build system:
- #9332, #9518, #9529: Cease storing C dependencies in the codebase. C

View File

@ -49,7 +49,7 @@ File "morematch.ml", lines 1050-1053, characters 8-10:
1053 | | C -> 2
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
(A `D|B (`B, (`A|`C)))
A `D
File "morematch.ml", line 1084, characters 5-51:
1084 | | _, _, _, _, _, A, _, _, _, _, B, _, _, _, _, _ -> "11"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -19,7 +19,7 @@ Lines 7-9, characters 43-24:
9 | | Two, Two -> "four"
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
(Two, One)
(One, Two)
module Add :
functor (T : sig type two end) ->
sig

View File

@ -115,7 +115,7 @@ Lines 24-26, characters 6-30:
26 | | Bar _, Bar _ -> true
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
(Bar _, Foo _)
(Foo _, Bar _)
module Nonexhaustive :
sig
type 'a u = C1 : int -> int u | C2 : bool -> bool u

View File

@ -62,7 +62,7 @@ Lines 5-7, characters 39-23:
7 | | IntLit , 6 -> false
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
(IntLit, 0)
(BoolLit, true)
val check : 's t * 's -> bool = <fun>
|}];;
@ -80,6 +80,6 @@ Lines 3-5, characters 45-38:
5 | | {fst = IntLit ; snd = 6} -> false
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
{fst=IntLit; snd=0}
{fst=BoolLit; snd=true}
val check : ('s t, 's) pair -> bool = <fun>
|}];;

View File

@ -3,7 +3,6 @@
* expect
*)
(* Warn about all relevant cases when possible *)
let f = function
None, None -> 1
| Some _, Some _ -> 2;;
@ -14,7 +13,7 @@ Lines 1-3, characters 8-23:
3 | | Some _, Some _ -> 2..
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
((Some _, None)|(None, Some _))
(None, Some _)
val f : 'a option * 'b option -> int = <fun>
|}]
@ -124,7 +123,7 @@ Line 1, characters 8-47:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
({left=Box 0; right=Box 0}|{left=Box 1; right=Box _})
{left=Box 0; right=Box 0}
val f : int box pair -> unit = <fun>
|}]

View File

@ -0,0 +1,108 @@
(* TEST *)
(* Tests for stack-overflow crashes caused by a combinatorial
explosition in fragile pattern checking. *)
[@@@warning "+4"]
module SyntheticTest = struct
(* from Luc Maranget *)
type t = A | B
let f = function
| A,A,A,A,A, A,A,A,A,A, A,A,A,A,A, A,A,A -> 1
| (A|B),(A|B),(A|B),(A|B),(A|B),
(A|B),(A|B),(A|B),(A|B),(A|B),
(A|B),(A|B),(A|B),(A|B),(A|B),
(A|B),(A|B),(A|B) -> 2
end
module RealCodeTest = struct
(* from Alex Fedoseev *)
type visibility = Shown | Hidden
type ('outputValue, 'message) fieldStatus =
| Pristine
| Dirty of ('outputValue, 'message) result * visibility
type message = string
type fieldsStatuses = {
iaasStorageConfigurations :
iaasStorageConfigurationFieldsStatuses array;
}
and iaasStorageConfigurationFieldsStatuses = {
startDate : (int, message) fieldStatus;
term : (int, message) fieldStatus;
rawStorageCapacity : (int, message) fieldStatus;
diskType : (string option, message) fieldStatus;
connectivityMethod : (string option, message) fieldStatus;
getRequest : (int option, message) fieldStatus;
getRequestUnit : (string option, message) fieldStatus;
putRequest : (int option, message) fieldStatus;
putRequestUnit : (string option, message) fieldStatus;
transferOut : (int option, message) fieldStatus;
transferOutUnit : (string option, message) fieldStatus;
region : (string option, message) fieldStatus;
cloudType : (string option, message) fieldStatus;
description : (string option, message) fieldStatus;
features : (string array, message) fieldStatus;
accessTypes : (string array, message) fieldStatus;
certifications : (string array, message) fieldStatus;
additionalRequirements : (string option, message) fieldStatus;
}
type interface = { dirty : unit -> bool }
let useForm () = {
dirty = fun () ->
Array.for_all
(fun item ->
match item with
| {
additionalRequirements = Pristine;
certifications = Pristine;
accessTypes = Pristine;
features = Pristine;
description = Pristine;
cloudType = Pristine;
region = Pristine;
transferOutUnit = Pristine;
transferOut = Pristine;
putRequestUnit = Pristine;
putRequest = Pristine;
getRequestUnit = Pristine;
getRequest = Pristine;
connectivityMethod = Pristine;
diskType = Pristine;
rawStorageCapacity = Pristine;
term = Pristine;
startDate = Pristine;
} ->
false
| {
additionalRequirements = Pristine | Dirty (_, _);
certifications = Pristine | Dirty (_, _);
accessTypes = Pristine | Dirty (_, _);
features = Pristine | Dirty (_, _);
description = Pristine | Dirty (_, _);
cloudType = Pristine | Dirty (_, _);
region = Pristine | Dirty (_, _);
transferOutUnit = Pristine | Dirty (_, _);
transferOut = Pristine | Dirty (_, _);
putRequestUnit = Pristine | Dirty (_, _);
putRequest = Pristine | Dirty (_, _);
getRequestUnit = Pristine | Dirty (_, _);
getRequest = Pristine | Dirty (_, _);
connectivityMethod = Pristine | Dirty (_, _);
diskType = Pristine | Dirty (_, _);
rawStorageCapacity = Pristine | Dirty (_, _);
term = Pristine | Dirty (_, _);
startDate = Pristine | Dirty (_, _);
} ->
true)
[||]
}
end

View File

@ -634,7 +634,13 @@ let build_specialized_submatrices ~extend_row discr rows =
(discr, r :: rs)
in
(* insert a row of head [p] and rest [r] into the right group *)
(* insert a row of head [p] and rest [r] into the right group
Note: with this implementation, the order of the groups
is the order of their first row in the source order.
This is a nice property to get exhaustivity counter-examples
in source order.
*)
let rec insert_constr head args r = function
| [] ->
(* if no group matched this row, it has a head constructor that
@ -679,12 +685,15 @@ let build_specialized_submatrices ~extend_row discr rows =
in
form_groups initial_constr_group [] rows
in
{
default = omega_tails;
constrs =
(* insert omega rows in all groups *)
List.fold_right insert_omega omega_tails constr_groups;
}
(* groups are accumulated in reverse order;
we restore the order of rows in the source code *)
let default = List.rev omega_tails in
let constrs =
List.fold_right insert_omega omega_tails constr_groups
|> List.map (fun (discr, rs) -> (discr, List.rev rs))
in
{ default; constrs; }
(* Variant related functions *)
@ -1250,22 +1259,6 @@ let rec do_match pss qs = match qs with
(build_specialized_submatrix ~extend_row:(@) q0 pss)
(qargs @ qs)
type 'a exhaust_result =
| No_matching_value
| Witnesses of 'a list
let rappend r1 r2 =
match r1, r2 with
| No_matching_value, _ -> r2
| _, No_matching_value -> r1
| Witnesses l1, Witnesses l2 -> Witnesses (l1 @ l2)
let rec try_many f = function
| [] -> No_matching_value
| (p,pss)::rest ->
rappend (f (p, pss)) (try_many f rest)
(*
let print_pat pat =
let rec string_of_pat pat =
@ -1296,14 +1289,14 @@ let print_pat pat =
This function should be called for exhaustiveness check only.
*)
let rec exhaust (ext:Path.t option) pss n = match pss with
| [] -> Witnesses [omegas n]
| []::_ -> No_matching_value
| [] -> Seq.return (omegas n)
| []::_ -> Seq.empty
| pss ->
let pss = simplify_first_col pss in
if not (all_coherent (first_column pss)) then
(* We're considering an ill-typed branch, we won't actually be able to
produce a well typed value taking that branch. *)
No_matching_value
Seq.empty
else begin
(* Assuming the first column is ill-typed but considered coherent, we
might end up producing an ill-typed witness of non-exhaustivity
@ -1319,61 +1312,53 @@ let rec exhaust (ext:Path.t option) pss n = match pss with
match build_specialized_submatrices ~extend_row:(@) q0 pss with
| { default; constrs = [] } ->
(* first column of pss is made of variables only *)
begin match exhaust ext default (n-1) with
| Witnesses r ->
let q0 = Patterns.Head.to_omega_pattern q0 in
Witnesses (List.map (fun row -> q0::row) r)
| r -> r
end
let sub_witnesses = exhaust ext default (n-1) in
let q0 = Patterns.Head.to_omega_pattern q0 in
Seq.map (fun row -> q0::row) sub_witnesses
| { default; constrs } ->
let try_non_omega (p,pss) =
if is_absent_pat p then
No_matching_value
Seq.empty
else
match
let sub_witnesses =
exhaust
ext pss
(List.length (simple_match_args p Patterns.Head.omega [])
+ n - 1)
with
| Witnesses r ->
let p = Patterns.Head.to_omega_pattern p in
Witnesses (List.map (set_args p) r)
| r -> r in
let before = try_many try_non_omega constrs in
if
full_match false constrs && not (should_extend ext constrs)
then
before
else
let r = exhaust ext default (n-1) in
match r with
| No_matching_value -> before
| Witnesses r ->
try
let p = build_other ext constrs in
let dug = List.map (fun tail -> p :: tail) r in
match before with
| No_matching_value -> Witnesses dug
| Witnesses x -> Witnesses (x @ dug)
with
(* cannot occur, since constructors don't make a full signature *)
| Empty -> fatal_error "Parmatch.exhaust"
end
in
let p = Patterns.Head.to_omega_pattern p in
Seq.map (set_args p) sub_witnesses
in
let try_omega () =
if full_match false constrs && not (should_extend ext constrs) then
Seq.empty
else
let sub_witnesses = exhaust ext default (n-1) in
match build_other ext constrs with
| exception Empty ->
(* cannot occur, since constructors don't make
a full signature *)
fatal_error "Parmatch.exhaust"
| p ->
Seq.map (fun tail -> p :: tail) sub_witnesses
in
(* Lazily compute witnesses for all constructor submatrices
(Some constr_mat) then the default submatrix (None).
Note that the call to [try_omega ()] is delayed to after
all constructor matrices have been traversed. *)
List.map (fun constr_mat -> Some constr_mat) constrs @ [None]
|> List.to_seq
|> Seq.flat_map
(function
| Some constr_mat -> try_non_omega constr_mat
| None -> try_omega ())
end
let exhaust ext pss n =
let ret = exhaust ext pss n in
match ret with
No_matching_value -> No_matching_value
| Witnesses lst ->
let singletons =
List.map
(function
[x] -> x
| _ -> assert false)
lst
in
Witnesses [orify_many singletons]
exhaust ext pss n
|> Seq.map (function
| [x] -> x
| _ -> assert false)
(*
Another exhaustiveness check, enforcing variant typing.
@ -1931,6 +1916,10 @@ let ppat_of_type env ty =
let (ppat, constrs, labels) = Conv.conv (orify_many pats) in
PT_pattern (PE_gadt_cases, ppat, constrs, labels)
let typecheck ~pred p =
let (pattern,constrs,labels) = Conv.conv p in
pred constrs labels pattern
let do_check_partial ~pred loc casel pss = match pss with
| [] ->
(*
@ -1949,48 +1938,34 @@ let do_check_partial ~pred loc casel pss = match pss with
end ;
Partial
| ps::_ ->
begin match exhaust None pss (List.length ps) with
| No_matching_value -> Total
| Witnesses [u] ->
let v =
let (pattern,constrs,labels) = Conv.conv u in
let u' = pred constrs labels pattern in
(* pretty_pat u;
begin match u' with
None -> prerr_endline ": impossible"
| Some _ -> prerr_endline ": possible"
end; *)
u'
let counter_examples =
exhaust None pss (List.length ps)
|> Seq.filter_map (typecheck ~pred) in
match counter_examples () with
| Seq.Nil -> Total
| Seq.Cons (v, _rest) ->
if Warnings.is_active (Warnings.Partial_match "") then begin
let errmsg =
try
let buf = Buffer.create 16 in
let fmt = Format.formatter_of_buffer buf in
Printpat.top_pretty fmt v;
if do_match (initial_only_guarded casel) [v] then
Buffer.add_string buf
"\n(However, some guarded clause may match this value.)";
if contains_extension v then
Buffer.add_string buf
"\nMatching over values of extensible variant types \
(the *extension* above)\n\
must include a wild card pattern in order to be exhaustive."
;
Buffer.contents buf
with _ ->
""
in
begin match v with
None -> Total
| Some v ->
if Warnings.is_active (Warnings.Partial_match "") then begin
let errmsg =
try
let buf = Buffer.create 16 in
let fmt = Format.formatter_of_buffer buf in
Printpat.top_pretty fmt v;
if do_match (initial_only_guarded casel) [v] then
Buffer.add_string buf
"\n(However, some guarded clause may match this value.)";
if contains_extension v then
Buffer.add_string buf
"\nMatching over values of extensible variant types \
(the *extension* above)\n\
must include a wild card pattern in order to be exhaustive."
;
Buffer.contents buf
with _ ->
""
in
Location.prerr_warning loc (Warnings.Partial_match errmsg)
end;
Partial
end
| _ ->
fatal_error "Parmatch.check_partial"
end
Location.prerr_warning loc (Warnings.Partial_match errmsg)
end;
Partial
(*****************)
(* Fragile check *)
@ -2054,12 +2029,13 @@ let do_check_fragile loc casel pss =
| ps::_ ->
List.iter
(fun ext ->
match exhaust (Some ext) pss (List.length ps) with
| No_matching_value ->
let witnesses = exhaust (Some ext) pss (List.length ps) in
match witnesses () with
| Seq.Nil ->
Location.prerr_warning
loc
(Warnings.Fragile_match (Path.name ext))
| Witnesses _ -> ())
| Seq.Cons _ -> ())
exts
(********************************)
@ -2075,7 +2051,11 @@ let check_unused pred casel =
let qs = [q] in
begin try
let pss =
get_mins le_pats (List.filter (compats qs) pref) in
(* prev was accumulated in reverse order;
restore source order to get ordered counter-examples *)
List.rev pref
|> List.filter (compats qs)
|> get_mins le_pats in
(* First look for redundant or partially redundant patterns *)
let r = every_satisfiables (make_rows pss) (make_row qs) in
let refute = (c_rhs.exp_desc = Texp_unreachable) in