プログラミング言語: パターンマッチの意味解析
プログラミング言語のパターンマッチにおける意味解析の論文やアルゴリズムをつまみ食い
- 論文 Warnings for pattern matching
- もっとも興味があるのは usefulness を判定するアルゴリズムなので、そこと前半の定義を中心に。
- 遅延評価とかは無視
- これを基に実装されている Rust Analyzer のコード
- そのうち A Generic Algorithm for Checking Exhaustivity of Pattern Matching (スライド)
Introduction
パターンマッチングは ML 系プログラミング言語の重要な機能のひとつです。
パターンマッチングは、場合分けによる推論(およびプログラミング)に適しています。このような推論スタイルでは、ふたつの基本的なチェックが必要です。すべてのケースが考慮されているか? そして、どのケースも他のケースに包含されているか?
これは、パターンマッチングの用語を使うと「すべての節 clauses が有用である網羅的なパターンマッチング式を書きたい」ということです。ML のコンパイラは、通常、これらふたつの基本的な要求を満たさないパターンマッチング式を異常 anomalies とみなして、警告やエラーを報告すべきです。そうすることで、コンパイラはプログラマがプログラム中のエラーを発見するための助けとなります。
パターンマッチングをコンパイルする方法は、決定木を対象とするか、バックトラック・オートマトンを対象とするかによって、ふたつのクラスに分類されます。両者を診断能力の点で比較すると、決定木の手法は、決定木が完全であり、不要なコードを含まないため、決定木に対してチェックを行うことができるという利点があります。これに対して、バックトラック・オートマトンは簡単には解析できません。
しかし、この論文の範囲を超えた理由 [1] により,一部のコンパイラ設計者はバックトラック・オートマトンを選択しています。たとえば、Objective Caml [2] の場合がそうであり、私たちの最初の動機は、このコンパイラのパターンマッチングのアルゴリズムを抽出することでした。つまり、最初のアイデアは、パターンマッチングの決定木へのコンパイルの簡易版を使用することでした。
しかし、パターンマッチングのチェックは、パターンマッチングのコンパイルよりもはるかに少ない労力で済むことが分かりました。最終的に得られたアルゴリズムは、ただ単に決定木へのコンパイルを簡略化した以上のものになりました。さらに、チェックに関する研究により、非常に一般的な解を得ることができました。私たちのアナライザは,意味論がまったく異なる ML と Haskell 両方に対して有効な答えを与えることができます。
本研究はふたつのパートに分かれています。第 1 部では、パターンマッチングのふたつの異常 anomalies を定義し、両方の異常を検出するアルゴリズムを紹介し、正格評価、遅延評価、および Haskell のセマンティクスに関して、このアルゴリズムの正しさを証明します。
第 2 部では、我々のアルゴリズムの実装について説明します。つまり、ユーザーに正確な警告を提供するという初期の実用的な問題に対して、我々のアルゴリズムをどのように改良し、適応させるかを示します。より具体的には、セクション 5 では、マッチしない値の例を提供することで、非網羅性の診断をより分かりやすくする方法を示します。次に、セクション 6 では、無効なケースの検出の重要な改良である無効なパターンの検出を検討します。この改良は、同じアクションを持つ節をグループ化する便利な機能である or-pattern を実装するときに自然に現れます。
最後に、我々の実装の効率性を分析し、結論を述べます。
-
F. Le Fessant and L. Maranget. Optimizing Pattern Matching. In International Conference on Functional Programming. ACM press, 2001. ↩︎
-
X. Leroy, D. Doligez, and et al. The Objective Caml Language (version 3.07). http://caml.inria.fr, 2003. ↩︎
第 1 部 異常検知のアルゴリズム
2. パターン、値、その他
ML の値のほとんどは、いくつかのシグネチャ上の基底項として定義できます。シグネチャは,データ型 data type の定義によって導入されます.例えば、以下のコード [1] では、
type mylist = Nil | One of int | Cons of int * mylist
型 mylist
の値は、以下の 3 つのコンストラクタ constructor で構成されています。
-
Nil
(0 引数のコンストラクタ = 定数コンストラクタ) -
One
(1 引数) -
Cons
(2 引数)
ML の値のほとんどはこのように定義されます。真偽値はふたつの定数コンストラクタをもつ型であり、整数型は無限個(または ","
で記述)などです。
より一般的には、値はいくつかのコンストラクタ上の(基底)項として定義されます。次のように明示的に定義しましょう。
以降では、定数コンストラクタの ()
は省略し、Objective Caml の文法に合わせます (NIl
, true
, 0
, など)。[2]
このシンプルな定義は、値呼び出し call-by-value の ML 言語におけるパターンマッチングの異常検知を研究する目的では十分です。しかし、
- 遅延評価の言語におけるパターン・マッチングの適切なセマンティクスは、部分的な値を定義する必要があり、これはセクション 4 で行います。
- 整数や文字列をはじめとする多くの値は、既知のシグネチャ(おそらく無限の大きさ)の上に構築された項と見なすことができますが、すべての値がそのように見なすことができるわけではありません。例えば、パラメトリックな多相性を示す型の関数や値を考えてみましょう。しかし、我々は、ML のすべての型や値の正確な性質には興味がありません。その代わりに、次のような非公式な公理に頼ることにします。任意の型 t が与えられたとき、t を型として持つ少なくともひとつの値が存在する、と仮定します。
厳密に言えば、この公理は成り立ちません。少なくとも、OCaml では値をもたない型を定義することができます。
type t_empty
そして、この型を使って、以下のようなヴァリアントをもつデータ型を定義し、それをパターンマッチに使うことができます。
type t = C of t_empty
let f x = match x with C y -> y
もちろん、上記の C y -> y
は無効 useless なはずです。パターン C y
にマッチする値は存在しません。しかし、前述の通り、今回のアルゴリズムでは t_empty
に少なくともひとつの値が存在すると仮定しているため、これを無効とはみなしません。
同様の理由で、以下の例を網羅的ではない non-exhaustive と判定します。
type tt = A | B of t_empty
let g x = match x with A -> true
すでにお分かりの通り、この判定は間違っています。
t_empty
型の A -> true
節だけで、型 tt
のありうる値すべてを網羅しているはずです。しかし、ほとんどの型に対して少なくともひとつの値が存在する公理が成立することを考えると、この問題は些細なことだと言えます。
パターンは値を識別するために使用されます。より正確には、パターンは共通の接頭辞を持つ値の集合を記述します。つまり、パターンは変数を持つ項であり、与えられたパターン
実際には、パターンの定義に変数は登場しません。変数はワイルドカード
さらに、パターンが型付けされていることも重要です。つまり、パターンは、データ型の宣言によって強制されるルールに従っていることを前提にできます。Objective Caml のコンパイラは、型付けのあとにパターンマッチング解析を行うので、パターンは型アノテーションを持っています。以降では説明を簡単にするために、これらのアノテーションをすべて明示することは避けています。しかし、適切な場合には、型アノテーションを
通常の項の理論では、
定義 1 (インスタンス関係)
任意のパターン
ここでパターンと値は型付けされていることを改めて強調しておきます。
特に
上記の定義 1 では、
さて、ベクトルのインスタンス関係を定義しました。次に、パターン
以降では、行列とベクトルという便利な枠組みの中で,ML のパターンマッチングの定義を思い出してみましょう。
- パターンと値は型をもつ
- すべての型は必ず値をもつ(公理)
ベクトルと行列の記法
記法 | 意味 |
---|---|
パターンの行ベクトル |
|
値の行ベクトル |
|
|
|
行のない行列 |
|
空行の行列 |
|
行列 |
定義 2. ML 言語のパターンマッチング
(p_1^i...p_n^i) \preceq (v_1...v_n) \forall j \lt i, (p_1^j...p_n^j) \npreceq (v_1...v_n)
また、このとき「
つまり,ベクトル
-
(訳註)パターンが値にマッチしている、かつ、それ以前のパターンがいずれもマッチしない場合 ↩︎
定義 2. 例1
型 mylist
である
実際に、いくつかのベクトルがどのようにマッチするか試してみます。
- ベクトル
は\vec{v} = (\mathtt{Nil}, \mathtt{Nil}) の最初の行にマッチします。なぜならP ((\mathtt{Nil}, \mathtt{\_}) \preceq (\mathtt{Nil}, \mathtt{Nil}) ) かつp_1^1 = \mathtt{Nil} \preceq \mathtt{Nil} = v_1 であるからです。p_2^1 = \mathtt{\_} = v_2 - ベクトル
は\vec{w} = (\mathtt{One(0)}, \mathtt{Nil}) の 2 番目の行にマッチします。P -
ここから、(\mathtt{\_}, \mathtt{Nil}) \preceq (\mathtt{One(0)}, \mathtt{Nil}) は行列\vec{w} の 2 番目の行のインスタンスです。P -
((\mathtt{Nil}, \mathtt{\_}) \npreceq (\mathtt{One(0)}, \mathtt{Nil}) ) ここから、p_1^1 = \mathtt{Nil} \npreceq \mathtt{One(0)} = w_1 は行列\vec{w} の 1 番目の行のインスタンスではありません。P
-
- ベクトル
は\vec{z} = (\mathtt{One(1)}, \mathtt{One(1)}) の 5 番目の行にマッチします。P - ベクトル
は\vec{z} のインスタンスです(このパターンは型\vec{p^5} = (\mathtt{\_}, \mathtt{\_}) mylist
のあらゆる値にマッチします)。 - くわえて、ベクトル
はその他のいずれのパターンにもマッチしません。たとえば、\vec{z} なので\mathtt{0} \npreceq \mathtt{1} となり、ベクトル\mathtt{One(0)} \npreceq \mathtt{One(1)} は 3 番目と 4 番目のパターンにはマッチしません。\vec{z}
- ベクトル
すでに述べたようにパターンはそのインスタンスの集合として考えることができます。同じように、パターンの行列は各行のインスタンス集合の和集合である、と考えることができます。
定義 3. 行列におけるインスタンス関係
ML 言語のパターンマッチングはこの新しい定義によって以下のように定式化できます。
3. The useful clause problem
ここからは、パターンマッチングにおける異常を行列の枠組みで定義します。
定義 4. 網羅性 Exhaustiveness
パターン行列
定義 5. 無効な節 Useless clause
パターン行列
例 2
次のようなパターン行列 mylist
です。
行列
一方、行列 mylist
がとりうる値を考えてみましょう。
|
|
---|---|
|
1 |
|
2 |
|
3 |
4 | |
5 |
ここでは,パターン・マッチングの ML 言語での定義(定義 2)を用いており、上記の定義 4, 5 は,一般的に理解されている「網羅的なマッチ」と「無効な節 clause」を表しています。しかし、この 2 つの問題が非常に似ていることは直感的に明らかであり,実際には以下のような定義で表現することができます。
定義 6. 有効な節 Useful clause
上記の式を
つまり、
命題 1
-
が偽なら、行列\mathcal{U}(P, \_ \cdots \_) は網羅的であるP -
が偽なら、行列\mathcal{U}(P^{[i\dots i)}, \vec{p^i}) の行番号P は無効であるi
証明
定義のとおり。
ふたつの定義 2 と 3 より、パターンマッチングの異常性に関する限り、マッチング述語が単純化できることがわかります。より正確には、
-
「
のインスタンスではなく、P のインスタンスである値ベクトル\vec{q} が存在する場合」 ↩︎\vec{v}
3.1 Solving the useful clause problem
このセクションでは
そこで、
基底ケース 列がひとつも存在しない場合 (
-
が行をもつ (P ) 場合、m \gt 0 は\mathcal{U}\mathrm{rec}(\begin{pmatrix}&\\&\end{pmatrix}, ()) false
です -
の場合、m = 0 は\mathcal{U}\mathrm{rec}(\emptyset, ()) true
です。不要ではありますが、より一般的には、 はあらゆるサイズ\mathcal{U}\mathrm{rec}(\emptyset, \vec{q}) のn で\vec{q} true
になります。
以上をまとめるとこうなります。
帰納 列が存在する場合 (
1. q_1 がコンストラクタパターンの場合
パターン
No row | |
与えられた行
以下の
また、
最後に、
2. q_1 がワイルドカードパターンの場合
集合
集合
この通り、
(a) 集合
(b) 集合
パターン行列
No row | |
行列
3. q_1 が or-パターン (r_1 \mid r_2) の場合
ここで、行列の特殊化 (以下の 1) とデフォルト行列 (以下の 2 から 4) のいくつかの「重要な」特性を証明していきます。
基本的に、特殊化の重要な特性は、第一成分に
補題 1 (重要な特性)
任意の行列
任意の値ベクトルについて、
更に、任意の行列
であり、逆に
になります。
証明
定義による。
上記では、
命題 2
任意の行列
証明
(省略)
3.2 Detecting the anomaries
ここまでで
更に、
PART 2 Implementation
\mathcal{U} の特殊化
5. 網羅性チェックのための 網羅性チェックにおいて、単に「ありうるパターンが網羅されていない」と警告するだけではプログラマを困惑させるだけでしょう。しかし、警告とともに「マッチしない値」の例があれば「確かに、マッチしないパターンを書いてしまった」と納得してもらえるだけでなく、コードを修正するのにも大いに役立ちます。
このような例(または反例)は、マッチしない例の集合を表すパターンとして表現するのが最適です。プログラマーは、この「反例」をパターンマッチの最後に追加することで、パターンが網羅的になることを期待します。簡単な例を挙げてみましょう。
let nilp = function [] -> true
Warning: this pattern-matching is not exhaustive.
Here is an example of a non-matching value:
_::_
与えられたパターンマッチングは網羅的ではなく、パターン
これらの例は、アルゴリズム
新しいアルゴリズム
基底ケース
まず、
であり、より一般的には、
帰納
1.集合
集合
もし、すべての計算結果が
そうではなく、
2.集合
まず、
簡単な例でアルゴリズム mylist
型の値にマッチするパターンマッチの網羅性をチェックするとします。
match ... with One 1 -> ...
従って、ここでは
ここで、特殊化された行列を再帰的に計算し、すべてのありうるパターン・ベクトルを導き出せばいいのではないか、と思う人もいるかもしれません。しかし、たとえば、整数を考えてみても、それの取りうる値は無限に存在するため、これが不可能なことが分かります。また、実際のところ、マッチしない例をひとつ示すだけで十分です。警告の原因となったコードを修正するのはプログラマの責任であり、コンパイラはできるだけコストをかけずに適切な警告を提供するだけでいいのです。上記の例では、
\mathcal{U} の特殊化
6. 無効な節チェックのための 一見すると無効な節チェックのためにはアルゴリズム
mylist
型 (Section 2) の値のうち、最初の要素が 1
かどうかを判別する関数を書いているとしましょう。
let f = function
| One x | Cons (x,_) -> x=1
| Nil | One _ | Cons (_,_) -> false
直感的に何かがおかしいことに気づきますね。最後のパターンは必要以上に複雑に見えます。そして、実際に、次のコードの方がより簡潔です。
let f = function
| One x | Cons (x,_) -> x=1
| Nil -> false
残念ながら、アルゴリズム
そして、or-パターンが展開された次のコードであれば、アルゴリズム
let f = function
| One x | Cons (x,_) -> x=1
| Nil -> false
| One _ -> false
| Cons (_,_) -> false
この場合、コンパイラは最後のふたつのパターンが無効であることを検知できるので、プログラマはこれらを削除することができます。
では、最初の例ではどうして検知できなかったのでしょうか?
let f = function
| One x | Cons (x,_) -> x=1
| Nil | One _ | Cons (_,_) -> false
Nil | One _ | Cons (_,_) -> false
節は Nil
が含まれており、このために「有用である」と判定されています。しかし、それ以外の One _
, Cons (_, _)
は冗長であり、削除しても関数 f
の挙動は変わりません。さらに、こうした冗長なパターンの定義は、or-パターンを展開することによって簡単に構築することができます。
実際のところ、Objective Caml のコンパイラが警告を出すときも、冗長な節 自体ではなく、冗長なパターンそれぞれについて警告を出すようになっています。
let f = function
| One x | Cons (x,_) -> x=1
| Nil | One _ | Cons (_,_) -> false
^^^^^ ^^^^^^^^^^
Warning: this pattern is unused.
Warning: this pattern is unused.
コード中には多くの or-パターンが含まれる可能性があるため、そのうちのひとつを展開することでコードサイズが指数的に増えることを防ぎます。例えば、以下のようなコードだと、
let f = function
| (1|2), (3|4), (5|6), ..., (2k−1|2k) -> true
| _ -> false
or-パターン (2k−1|2k)
をチェックするためには、以下のようにひとつだけ展開するので十分です。
let f = function
| (1|2), (3|4), (5|6), ..., 2k−1 -> true
| (1|2), (3|4), (5|6), ..., 2k -> true
| _ -> false
既知のアルゴリズ 2k-1
と 2k
は有効であると断言できます。逐次的な展開を行うことで、指数的なコードサイズの増加もありません。
or-パターンの展開では、
let f1 = function (1|_) -> true
and f2 = function (_|1) -> true
and f3 = function (1|1) -> true
and f4 = function (_|_) -> true
展開してみると分かるように、or-パターンの右側の選択肢は、f1
では有効ですが、それ以降は無効です。
let f1 = function 1 -> true | _ -> true
and f2 = function _ -> true | 1 -> true
and f3 = function 1 -> true | 1 -> true
and f4 = function _ -> true | _ -> true
良い診断結果を出すためには「or-パターン引数のマッチングを試みる順序を指定しない(ただし、Haskell のマッチングは違います)」という定義を見直さなければいけない、と思うかもしれません。しかし、実際には、正格かつ Laville のマッチングでは、このような順序を指定する必要はありません。これらのパターンマッチングの定義は、どの行がマッチングされるかに依存し、どのようにマッチングされるかには依存しません。とはいえ、実際には、診断結果と生成されたコードの一貫性を保つため(or-パターンに変数があるため、実行するとマッチした選択肢が見えてしまいます)、コンパイラは左から右への順序を考慮しなければなりません。これは、SML/NJ コンパイラ (Appel & MacQueen, 1991) などパターンマッチの前に展開を実行するコンパイラのように、パターンマッチの過程で展開を実行する(正格な)コンパイラ (Le Fessant & Maranget, 2001) でも、容易に達成できます。
次のセクションではアルゴリズ
定理 4 (パターン展開の有用性)
3 つすべてのパターンマッチの定義において、
ここで
証明
(割愛)
6.3 Rules for finding useless patterns
TBD
6.4 Computation of useless patterns
TBD
パターンマッチの検証では、ある型に対するパターンのリストが与えられたとき、以下を判定するアルゴリズムが必要。
- (a) それぞれのパターンが到達可能かどうか reachability
- (b) パターンがすべてのありうる値をカバーしているかどうか exhaustiveness
アルゴリズムの核は usefulness
- 同じ型のパターン
p
とq
があり、q
にマッチしp
にマッチしない値があるとき、q
はp
と比べて useful である - これは複数の
p
に対しても一般化される- 同じ型のパターン
p_1, p_2 ... p_n
があり、q
にマッチしp_i
にマッチしない値があるとき、q
はp
と比べて useful である -
usefulness(p_1 .. p_n, q)
はそのような値 (witness) のリストを返す関数
- 同じ型のパターン
- usefulness 関数があれば reachability は計算可能
- あるパターンは、それよりも前のパターンすべてに関して useful であれば到達可能である
match x {
Some(_) => ...,
None => ..., // 到達可能。None がこのパターンにマッチする
Some(0) => ..., // 到達不可能。このパターンがマッチするすべての値は、すでに Some(_) でマッチ済みである
}
exhaustiveness も計算可能
- あるマッチは、ワイルドカードパターン
_
が usefull でなければ exhaustive
match x {
Some(0) => ...,
None => ...,
// 網羅的ではない。ワイルドカードパターン _ が useful (Some(1) にマッチするので)
}
compute_match_usefulness 関数
- それぞれのブランチの到達可能性
- すべてのマッチの網羅性
コンストラクタとフィールド
- ctor = constructor の略
- あらゆる値はコンストラクタをいくつかの引数(フィールド)に適用することで生成される
- 例:
Some
,None
-
(,)
2 要素タプル -
Foo{...}
Foo 構造体のコンストラクタ -
2
数値 2 のコンストラクタ -
None
や2
は引数(フィールド)をひとつも取らないコンストラクタ
- 例:
- こうすることで、あらゆる値はコンストラクタの木構造で表すことができる。
- 例えば、
Foo { bar: None, baz: Ok(0) }
はFoo
,None
,Ok
,0
で構成されている
- 例えば、
- パターンも同様
- すべての値コンストラクタに加えて、パターン特有のコンストラクタもある
- たとえば、ワイルドカード
_
- 整数 Range
0..=10
- 任意長のスライス
[x, ..]
- Or-パターン
Ok(0) | Err(_)
- 変数を束縛するパターン (e.g.
Some(x)
) はワイルドカードと同義
- ある値があるパターンにマッチするかどうかは、コンストラクタをひとつずつ見ていく。
- パターン
p
と値v
が与えられたときmatches!(v, p)
を計算する- 先頭のコンストラクタを比較して、同じであれば、そのフィールドを再帰的に比較していく。
- パターン
典型的な例
matches!(v, _) // true
matches!((v0, v1), (p0, p1)) // matches!(v0, p0) && matches!(v1, p1)
matches!(Foo { bar: v0, baz: v1 }, Foo { bar: p0, baz: p1 }) // matches!(v0, p0) && matches!(v1, p1)
matches!(Ok(v0), Ok(p0)) // matches!(v0, p0)
matches!(Ok(v0), Err(p0)) // false - incompatible variants
matches!(v, 1..=100) // matches!(v, 1) || ... || matches!(v, 100)
matches!([v0], [p0, .., p1]) // false - incompatible length
matches!([v0, v1, v2], [p0, .., p1]) // matches!(v0, p0) && matches!(v2, p1)
matches!(v, p0 | p1) // matches!(v, p0) || matches!(v, p1)
- コンストラクタ、フィールドと関連する操作は
super::deconstruct_pat
モジュールにある - Note: このコンストラクタ、フィールドの定義はすべての Rust の型に適用できるわけではない
-
Rc<u64>
はこの方法では分解できない -
&str
は無限のコンストラクタを持つことになる - 他にもいろいろ微妙な点は多い
- ただ、コンストラクタのアイデアはこれらを処理できるように拡張可能である
-
- コンストラクが他のコンストラクタをカバーできるかどうかは
Constructor::is_covered_by
で判定できる
Specialization
-
usefulness(p_1 .. p_n, q)
を計算したい -
p_1 .. p_n
とq
が与えられた時に、q
にマッチし、p_i
にマッチしない値- これらの値を witnesses と呼ぶ
- とはいえ、すべてのありうる値を列挙するわけではない
前段の話の通り、与えられた型を構成するコンストラクタをひとつずつ見ていくことになる。ここで見るのは、それぞれの値コンストラクタにおいて、このコンストラクタから始まり、パターン q
にマッチし、かつ、パターン p_i
にマッチしない値があるかどうか、である。以前にも分かった通り、候補となる値の最初の (つまり、木構造のルートの) コンストラクタを見るだけでも、多くのことが分かる。
match x {
Enum::Variant1(_) => {} // `p1`
Enum::Variant2(None, 0) => {} // `p2`
Enum::Variant2(Some(_), 0) => {} // `q`
}
ここに候補となる値 v
があるとき、v
が Variant1
から始まるのであれば q
にはマッチしない。しかし、もしも v
が v = Variant2(v0, v1)
であれば、p2
か q
にマッチする可能性があり、それは v0
と v1
の値次第である。実際、値 v
が q
の usefulness を判定するときの witness になるのは、タプル (v0, v1)
が以下のような簡略化されたコードにおいて q
の witness になるときである。
match x {
(None, 0) => {} // `p2'`
(Some(_), 0) => {} // `q'`
}
これが、usefulness の計算において、我々が specialization (特殊化) と呼ぶ新しいステップを導入した理由である。
特殊化のステップでは、値コンストラクタにマッチするパターンのリストをフィルタリングしてから、コンストラクタのフィールドを調べます。これにより、usefulness を再帰的に計算することができます。
各行の単一のパターンを処理するのではなく、各行のパターンのリストを考えることにし、そのようなリストを_pattern-stack_と呼ぶことにします。
- pattern-stack は先頭のコンストラクタを pop して、そのフィールドを push したり、
- pop したコンストラクタを push し直して、元に戻したりすることができます。
pattern-stack を単に [p_1 ... p_n]
と記すことにします。ここでは、何が起こっているかを説明するために、パターン・スタックのリストの特殊化のシーケンスを示します。
以下のような pattern-stack のリスト (Matrix) があったとして、
[Enum::Variant1(_)]
[Enum::Variant2(None, 0)]
[Enum::Variant2(Some(_), 0)]
これを Variant2
に対して特殊化します。
[None, 0]
[Some(_), 0]
Some
に対して特殊化します。
[_, 0]
タプルの最初の要素の型が bool だとして、_
に対して特殊化します。
[0]
最後に、0
に対して特殊化します。
[]
関数 specialize(c, p)
は値コンストラクタ c
とパターン p
を受け取り、0 個以上の pattern-stack を返します。
- もし、値コンストラクタ
c
とパターンp
の最初のコンストラクがマッチしない場合は何も返しません。 - そうでなければ、コンストラクタのフィールドを返します。
- ひとつ以上の pattern-stack を返すのは、
p
がパターン特有のコンストラクタを持つときのみです。
さらに、以下のようなケースがあります。
誤ったコンストラクタの特殊化は何も返しません。
specialize(None, Some(p0)) // []
正しいコンストラクタの特殊化は単一の行にコンストラクタのフィールドを含む Matrix を返します。
specialize(Variant1, Variant1(p0, p1, p2)) // [[p0, p1, p2]]
specialize(Foo{..}, Foo { bar: p0, baz: p1 }) // [[p0, p1]]
Or-パターンに対しては、それぞれの分岐を特殊化し、結果を結合します。
specialize(c, p0 | p1) // specialize(c, p0) ++ specialize(c, p1)
同様に、他のパターンコンストラクタも、ありうるパターンすべてを列挙した Or-パターンのように扱います。
specialize(c, _) // specialize(c, Variant1(_) | Variant2(_, _) | ...)
specialize(c, 1..=100) // specialize(c, 1 | ... | 100)
specialize(c, [p0, .., p1]) // specialize(c, [p0, p1] | [p0, _, p1] | [p0, _, _, p1] | ...)
もしも、コンストラクタ c
がパターン特有のコンストラクタの場合は、specialize 関数はそれぞれの場合わけで実装されています。
また、この関数は pattern-stack を入力として取るように拡張されています(各行の最初のカラムのみ処理し、他の要素は変更しない)。
- Matrix を処理する特殊化は
Matrix::specialize_constructor
で実装されています。ただし、最初のカラムが Or-パターンの場合は matrix に格納される前に展開されます。 - 単一の pattern-stack を処理する特殊化は、
Constructor::is_covered_by
とPatStack::pop_head_constructor
を組み合わせて実装されています(Fields
構造体に詳しい)
usefulness の計算
これで usefulness を計算できる
- 入力
- pattern-stack
p_1 ... p_n
のリスト (ただし、1行にひとつ) - 新しい pattern-stack
q
- なお、pattern-stack のリストを matrix と呼ぶ
- これらはすべて同じ数の列を持ち、与えられた列の中のパターンはすべて同じ型でなければなりません
- pattern-stack
-
usefulness
は witness のリスト(空かもしれません)を pattern-stack で返します。 -
base case:
n_columns == 0
- 空の pattern-stack は Unit type
()
のようなもの - したがって、
q
はその上に行がない場合、つまりn == 0
の場合に useful です
- 空の pattern-stack は Unit type
-
inductive case:
n_columns > 0
- 試してみたいコンストラクタをリストアップする方法が必要です。ここでは最初の列の型に対するすべての値のコンストラクタをリストアップするものとします。次の節でもっとスマートな方法を紹介します。
手順
- それぞれのコンストラクタ
c
について- それぞれのパターン
q
についてspecialize(c, q)
の返り値q'
について-
usefulness(specialize(c, p_1) ... specialize(c, p_n), q')
を計算する
-
- それぞれの見つかった witness について、
c
を push し直して特殊化を "revert" します
- それぞれのパターン
- 見つかった witness をひとつのリストに結合して返します。
例
[Some(true)] // p_1
[None] // p_2
[Some(_)] // q
//==>> try `None`: `specialize(None, q)` returns nothing
//==>> try `Some`: `specialize(Some, q)` returns a single row
[true] // p_1'
[_] // q'
//==>> try `true`: `specialize(true, q')` returns a single row
[] // p_1''
[] // q''
//==>> base case; `n != 0` so `q''` is not useful.
//==>> go back up a step
[true] // p_1'
[_] // q'
//==>> try `false`: `specialize(false, q')` returns a single row
[] // q''
//==>> base case; `n == 0` so `q''` is useful. We return the single witness `[]`
witnesses:
[]
//==>> undo the specialization with `false`
witnesses:
[false]
//==>> undo the specialization with `Some`
witnesses:
[Some(false)]
//==>> we have tried all the constructors. The output is the single witness `[Some(false)]`.
これらの処理は is_useful 関数にあります。
実際には、到達可能性を計算するときに witness のリストは必要ありません。witness が存在するかどうかだけを知る必要があります。 witness のリストは、網羅性を計算するときに、ユーザーに報告するために使います。
Constructor splitting
usefulness アルゴリズムで、値コンストラクタを列挙するとき、
- 単純にすべてのあり得る値を列挙するのは非現実的
-
u64
や&str
でどうなるか...
-
- まず、パターン
q
の先頭のコンストラクタがカバーする値コンストラクタを列挙すれば十分である- 先頭のコンストラクタが値コンストラクタであれば、それだけで良い
- パターンコンストラクタであれば、Constructor splitting という手法でコンストラクタ群をグループ分けする
コンストラクタ c
と matrix (pattern-stack のリスト) が与えられたとき、 c
がカバーするすべての値のコンストラクタを順番に特殊化し、それぞれについて usefulness を計算したい。すべてのコンストラクタを列挙するのは困難なので、それらの値のコンストラクタを可能な限りグループ化します。
match (0, false) {
(0 ..=100, true) => {} // `p_1`
(50..=150, false) => {} // `p_2`
(0 ..=200, _) => {} // `q`
}
上記のコードで q
を特殊化するとき、素朴な方法では、0..=200
の範囲にあるすべての数を試すことになります。
しかし、例えば 0
と 1
は全く同じ行にマッチし、同等の witness が返されます。実際、0..50
はすべてそうなります。このように、4つのコンストラクタに限定して探索することができます。0..50
, 50..=100
, 101..=150
, 151..=200
の4つのコンストラクタに絞ることができます。これで十分であり、限りなく扱いやすいものです。
関数 split(p_1 ... p_n, c)
は、 c
がカバーするコンストラクタ c'
のリストを返します。 c'
がカバーするすべての値コンストラクタ c''
は、特殊化して usefulness を計算すると、同等の witness のセットを返すことを要求します。上の例では、 0..50
がカバーする c''
によって特殊化した場合の witness は、その最初の要素だけが異なることになります。
また、通常は c'
を合わせて、元の c
をすべてカバーすることを求めます。しかし、結果として得られる witness のリストが空であるかどうかを変えない限り、いくつかのコンストラクタをスキップすることを許可しています。これはワイルドカードの _
の場合に使用します。
Constructor splitting は Constructor::split
関数で実装されています。Or-パターンの分割は行いません。その代わり、選択肢を1つずつ試していきます。ワイルドカードの分割については SplitWildcard
を、整数の範囲については SplitIntRange
を、スライスについては SplitVarLenSlice
を参照してください。
usefulness.rs atcompiler/rustc_mir_build/src/thir/pattern/usefulness.rs
-
compute_match_usefulness()
- usefulness アルゴリズムのエントリーポイント
- 入力となる arm の各パターンはあらかじめ
MatchVisitor::lower_pattern()
でコンストラクタとフィールドの組 (DeconstructedPat
) に変換しておく必要がある。
deconstruct_pat.rs at compiler/rustc_mir_build/src/thir/pattern/deconstruct_pat.rs
-
Constructor::split()
- constructor splitting の実装- ワイルドカード
- 整数パターン
- (可変長)配列パターン
-
IntRange
- 整数またはその範囲パターン
2
,2..=5
or2..5
- 整数のリテラルパターンも含まれることに注意
- 整数またはその範囲パターン