feat(match): add pattern normalization and exhaustiveness checking
This commit is contained in:
1 parent
f7d69f3067
commit
14fdc32383
1 file changed
+274
@@ -0,0 +1,274 @@
|
||||
module Fml.Core.Match
|
||||
|
||||
open Fml.Core.Ast
|
||||
open Fml.Core.Decls
|
||||
|
||||
// Pattern-match analysis and compilation. Patterns are normalized to a uniform
|
||||
// constructor form (CPat). On that form we run Maranget's usefulness algorithm
|
||||
// for exhaustiveness and redundancy, and a matrix compiler that lowers a match
|
||||
// into a decision tree the bytecode backend can walk.
|
||||
|
||||
exception MatchError of string
|
||||
|
||||
type CPat =
|
||||
| CWild
|
||||
| CCtor of string * int * CPat list
|
||||
|
||||
type Signature =
|
||||
| Finite of (string * int) list
|
||||
| Infinite
|
||||
|
||||
// A path addresses a sub-value of the scrutinee by successive positional
|
||||
// projections: tuple element, cons head(0)/tail(1), constructor argument, or
|
||||
// record field in sorted-name order.
|
||||
type Path = int list
|
||||
|
||||
type SwitchTest =
|
||||
| SInt of int
|
||||
| SStr of string
|
||||
| SBool of bool
|
||||
| SUnit
|
||||
| SNil
|
||||
| SCons
|
||||
| SCtor of string
|
||||
| STuple
|
||||
| SRecord
|
||||
|
||||
type DecisionTree =
|
||||
| Leaf of int
|
||||
| FailNode
|
||||
| SwitchNode of Path * (SwitchTest * DecisionTree) list * DecisionTree option
|
||||
|
||||
let private recordSortedFields (typeName: string) : string list =
|
||||
match lookupType typeName with
|
||||
| Some ti -> List.sort ti.Fields
|
||||
| None -> []
|
||||
|
||||
let private recordOrderedPatterns (fields: (string * Pattern) list) : Pattern list =
|
||||
match fields with
|
||||
| [] -> []
|
||||
| (firstName, _) :: _ ->
|
||||
let typeName =
|
||||
match lookupField firstName with
|
||||
| Some info -> info.TypeName
|
||||
| None -> ""
|
||||
let provided = Map.ofList fields
|
||||
recordSortedFields typeName
|
||||
|> List.map (fun name ->
|
||||
match Map.tryFind name provided with
|
||||
| Some p -> p
|
||||
| None -> PWildcard)
|
||||
|
||||
let rec toCPat (p: Pattern) : CPat =
|
||||
match p with
|
||||
| PWildcard | PVar _ -> CWild
|
||||
| PInt n -> CCtor("#int:" + string n, 0, [])
|
||||
| PString s -> CCtor("#str:" + s, 0, [])
|
||||
| PBool b -> CCtor("#bool:" + (if b then "true" else "false"), 0, [])
|
||||
| PUnit -> CCtor("#unit", 0, [])
|
||||
| PNil -> CCtor("#nil", 0, [])
|
||||
| PCons(h, t) -> CCtor("#cons", 2, [ toCPat h; toCPat t ])
|
||||
| PTuple ps -> CCtor("#tuple:" + string (List.length ps), List.length ps, List.map toCPat ps)
|
||||
| PConstruct(name, ps) -> CCtor(name, List.length ps, List.map toCPat ps)
|
||||
| PRecord fields ->
|
||||
let ordered = recordOrderedPatterns fields
|
||||
let typeName =
|
||||
match fields with
|
||||
| (firstName, _) :: _ ->
|
||||
match lookupField firstName with
|
||||
| Some info -> info.TypeName
|
||||
| None -> ""
|
||||
| [] -> ""
|
||||
CCtor("#record:" + typeName, List.length ordered, List.map toCPat ordered)
|
||||
|
||||
let private signatureOf (tag: string) : Signature =
|
||||
if tag.StartsWith "#int:" || tag.StartsWith "#str:" then Infinite
|
||||
elif tag = "#bool:true" || tag = "#bool:false" then Finite [ "#bool:true", 0; "#bool:false", 0 ]
|
||||
elif tag = "#unit" then Finite [ "#unit", 0 ]
|
||||
elif tag = "#nil" || tag = "#cons" then Finite [ "#nil", 0; "#cons", 2 ]
|
||||
elif tag.StartsWith "#tuple:" then
|
||||
let arity = int (tag.Substring 7)
|
||||
Finite [ tag, arity ]
|
||||
elif tag.StartsWith "#record:" then
|
||||
let typeName = tag.Substring 8
|
||||
match lookupType typeName with
|
||||
| Some ti -> Finite [ tag, List.length ti.Fields ]
|
||||
| None -> Finite [ tag, 0 ]
|
||||
else
|
||||
match lookupCtor tag with
|
||||
| Some ci ->
|
||||
match lookupType ci.TypeName with
|
||||
| Some ti -> Finite(ti.Ctors |> List.map (fun c -> c, ctorArity c))
|
||||
| None -> Finite [ tag, List.length ci.ArgTypes ]
|
||||
| None -> Finite [ tag, 0 ]
|
||||
|
||||
let private completeSignature (presentTags: string list) : (string * int) list option =
|
||||
match presentTags with
|
||||
| [] -> None
|
||||
| first :: _ ->
|
||||
match signatureOf first with
|
||||
| Infinite -> None
|
||||
| Finite fullSig ->
|
||||
let fullTagSet = fullSig |> List.map fst |> Set.ofList
|
||||
let presentSet = Set.ofList presentTags
|
||||
if Set.isSubset fullTagSet presentSet then Some fullSig else None
|
||||
|
||||
let private isWild p =
|
||||
match p with
|
||||
| CWild -> true
|
||||
| _ -> false
|
||||
|
||||
let private specializeRow (tag: string) (arity: int) (row: CPat list) : CPat list option =
|
||||
match row with
|
||||
| head :: rest ->
|
||||
match head with
|
||||
| CWild -> Some(List.replicate arity CWild @ rest)
|
||||
| CCtor(t, _, args) -> if t = tag then Some(args @ rest) else None
|
||||
| [] -> None
|
||||
|
||||
let private specialize tag arity matrix = matrix |> List.choose (specializeRow tag arity)
|
||||
|
||||
let private defaultRow (row: CPat list) : CPat list option =
|
||||
match row with
|
||||
| CWild :: rest -> Some rest
|
||||
| _ -> None
|
||||
|
||||
let private defaultMatrix matrix = matrix |> List.choose defaultRow
|
||||
|
||||
let private headTags matrix =
|
||||
matrix
|
||||
|> List.choose (fun row ->
|
||||
match row with
|
||||
| CCtor(t, a, _) :: _ -> Some(t, a)
|
||||
| _ -> None)
|
||||
|> List.distinctBy fst
|
||||
|
||||
let rec private useful (matrix: CPat list list) (q: CPat list) : bool =
|
||||
match q with
|
||||
| [] -> List.isEmpty matrix
|
||||
| q1 :: qs ->
|
||||
match q1 with
|
||||
| CCtor(tag, arity, args) -> useful (specialize tag arity matrix) (args @ qs)
|
||||
| CWild ->
|
||||
let present = headTags matrix |> List.map fst
|
||||
match completeSignature present with
|
||||
| Some fullSig ->
|
||||
fullSig
|
||||
|> List.exists (fun (tag, arity) ->
|
||||
useful (specialize tag arity matrix) (List.replicate arity CWild @ qs))
|
||||
| None -> useful (defaultMatrix matrix) qs
|
||||
|
||||
let private describeCPat (p: CPat) : string =
|
||||
match p with
|
||||
| CWild -> "_"
|
||||
| CCtor(tag, _, _) ->
|
||||
if tag.StartsWith "#int:" then tag.Substring 5
|
||||
elif tag.StartsWith "#str:" then "a string"
|
||||
elif tag.StartsWith "#bool:" then tag.Substring 6
|
||||
elif tag = "#unit" then "()"
|
||||
elif tag = "#nil" then "[]"
|
||||
elif tag = "#cons" then "_ :: _"
|
||||
elif tag.StartsWith "#tuple:" then "a tuple"
|
||||
elif tag.StartsWith "#record:" then "a record"
|
||||
else tag
|
||||
|
||||
// Renders a constructor pattern back into surface-like syntax so an
|
||||
// exhaustiveness counterexample reads the way a user would have written it.
|
||||
let rec private renderCPat (p: CPat) : string =
|
||||
match p with
|
||||
| CWild -> "_"
|
||||
| CCtor(tag, _, args) ->
|
||||
if tag.StartsWith "#int:" then tag.Substring 5
|
||||
elif tag.StartsWith "#str:" then "\"" + tag.Substring 5 + "\""
|
||||
elif tag.StartsWith "#bool:" then tag.Substring 6
|
||||
elif tag = "#unit" then "()"
|
||||
elif tag = "#nil" then "[]"
|
||||
elif tag = "#cons" then
|
||||
match args with
|
||||
| [ h; t ] -> renderConsArg h + " :: " + renderCPat t
|
||||
| _ -> "_ :: _"
|
||||
elif tag.StartsWith "#tuple:" then
|
||||
"(" + (args |> List.map renderCPat |> String.concat ", ") + ")"
|
||||
elif tag.StartsWith "#record:" then "{ ... }"
|
||||
elif List.isEmpty args then tag
|
||||
else tag + " " + (args |> List.map renderCtorArg |> String.concat " ")
|
||||
|
||||
and private renderCtorArg (p: CPat) : string =
|
||||
match p with
|
||||
| CCtor(tag, _, _ :: _) when not (tag.StartsWith "#") -> "(" + renderCPat p + ")"
|
||||
| CCtor("#cons", _, _) -> "(" + renderCPat p + ")"
|
||||
| _ -> renderCPat p
|
||||
|
||||
and private renderConsArg (p: CPat) : string =
|
||||
match p with
|
||||
| CCtor("#cons", _, _) -> "(" + renderCPat p + ")"
|
||||
| _ -> renderCPat p
|
||||
|
||||
// Picks a head constructor (or literal value) that no clause in the column
|
||||
// mentions, so the witness names a genuinely missing case.
|
||||
let private missingHead (presentTags: string list) : CPat option =
|
||||
match presentTags with
|
||||
| [] -> None
|
||||
| first :: _ ->
|
||||
match signatureOf first with
|
||||
| Infinite ->
|
||||
if first.StartsWith "#int:" then
|
||||
let used =
|
||||
presentTags
|
||||
|> List.choose (fun t -> if t.StartsWith "#int:" then Some(int (t.Substring 5)) else None)
|
||||
|> Set.ofList
|
||||
let mutable v = 0
|
||||
while Set.contains v used do v <- v + 1
|
||||
Some(CCtor("#int:" + string v, 0, []))
|
||||
elif first.StartsWith "#str:" then
|
||||
let used = presentTags |> List.filter (fun t -> t.StartsWith "#str:") |> Set.ofList
|
||||
let mutable v = "#str:x"
|
||||
while Set.contains v used do v <- v + "x"
|
||||
Some(CCtor(v, 0, []))
|
||||
else None
|
||||
| Finite fullSig ->
|
||||
match fullSig |> List.filter (fun (t, _) -> not (List.contains t presentTags)) with
|
||||
| (tag, arity) :: _ -> Some(CCtor(tag, arity, List.replicate arity CWild))
|
||||
| [] -> None
|
||||
|
||||
// Maranget's witness-producing usefulness algorithm. Returns a counterexample
|
||||
// row of width n that no row of the matrix matches, or None when the matrix is
|
||||
// exhaustive over n columns.
|
||||
let rec private witnessRows (matrix: CPat list list) (n: int) : CPat list option =
|
||||
if n = 0 then
|
||||
if List.isEmpty matrix then Some [] else None
|
||||
else
|
||||
let present = headTags matrix
|
||||
let presentTags = present |> List.map fst
|
||||
match completeSignature presentTags with
|
||||
| Some fullSig ->
|
||||
fullSig
|
||||
|> List.tryPick (fun (tag, arity) ->
|
||||
match witnessRows (specialize tag arity matrix) (arity + n - 1) with
|
||||
| Some w ->
|
||||
let args = w |> List.take arity
|
||||
let rest = w |> List.skip arity
|
||||
Some(CCtor(tag, arity, args) :: rest)
|
||||
| None -> None)
|
||||
| None ->
|
||||
match witnessRows (defaultMatrix matrix) (n - 1) with
|
||||
| Some w ->
|
||||
let head =
|
||||
match missingHead presentTags with
|
||||
| Some cpat -> cpat
|
||||
| None -> CWild
|
||||
Some(head :: w)
|
||||
| None -> None
|
||||
|
||||
// Runs redundancy and exhaustiveness analysis, raising MatchError on failure.
|
||||
let checkMatch (patterns: Pattern list) : unit =
|
||||
let rows = patterns |> List.map (fun p -> [ toCPat p ])
|
||||
rows
|
||||
|> List.iteri (fun i row ->
|
||||
let earlier = rows |> List.take i
|
||||
if not (useful earlier row) then
|
||||
raise (MatchError(sprintf "unreachable match case: %s" (describeCPat row.Head))))
|
||||
match witnessRows rows 1 with
|
||||
| Some(witness :: _) ->
|
||||
raise (MatchError(sprintf "non-exhaustive match: no clause covers %s" (renderCPat witness)))
|
||||
| _ -> ()
|
||||
Reference in new issue
Block a user