このgistは今年読んだ一番好きな論文2017 Advent Calendarの1日目の記事です。
去年、覚え書きのような記事で参戦したら、歓迎の言葉に加えて参加賞まで貰ってしまった者です。あれで終わらせるのは申し訳ない以前に悔しいので、今回はよそゆきの文章でガッツリ書いてリベンジすることで感謝の言葉に替えたいと思います。
この記事では、読者としてC言語に触れた経験がある人を想定しています。他の言語でもプログラミング経験があればさほど難しくはないと思いますが、「プログラミング言語って何?」という人が理解するのは厳しいかもしれません。
- タイトル
- 著者
-
-
Andrei Stefanescu
-
Daejun Park
-
Shijiao Yuwen
-
Yilong Li
-
Grigore Rosu
-
- 出典
去年発表のもので申し訳ないですが、11月開催なのでほぼ今年みたいなものでしょう。
OOPSLAは プログラミング言語学会四天王[1]なんて呼ばれたことがあるようなトップカンファレンス[2]なので、十分に期待できるものだと思っていいでしょう。
ざっくりした内容としては「複数のプログラミング言語に対応可能な共通検証基盤を作って実験しました」といったところでしょうか。複数言語対応というところがミソで、この研究が初ではないでしょうか[3]。
論文の内容に入る前に、背景となるツールを紹介したいと思います。個人的にはこれだけでも覚えて帰ってもらえれば満足です。
筆者らの研究室で作っている K framework というプログラミング言語作成ツールがあります。公式の言葉を借りれば「rewrite-based executable semantic framework」とのこと。
ひどい名前[4]に反して実は画期的なツールであることを、プログラム言語のつくりと共に説明しましょう。
プログラミング言語は、形を表す シンタックス(文法) と動作を表す セマンティクス(意味論) からできています。例えば、「 if(b) B1 else B2
というコード片はbが数値、B1とB2がブロックで、全体として文を表す」というのがシンタックスで、「 if(b) B1 else B2
はbが0でないときはB1、それ以外はB2と同じ意味である」というのがセマンティクスです。
一般に、シンタックスはyaccやPEGといったツールによって記述されます。広く使われる形式であるため一度書いてしまえば再利用もし易く、扱い易いと言っていいでしょう。一方、セマンティクスは普通、数十〜数百ページに及ぶ仕様書や、コンパイラ・インタプリタといった処理系(つまりプログラム)によって定められ、形式的な定義は必要になった時に後から作られる場合がほとんどです。
Kでは、シンタックスとセマンティクスの両方を定められた形式で書くことで言語を作っていきます。
シンタックスとセマンティクスが揃えば言語定義はもうできたと言っていいので、処理系やデバッガといった諸々のツールを作るのは 自動で やってもらおう、というのがKのコンセプトです。筆者らの研究室のウェブサイトでは、このコンセプトの表現に以下のような図を用いています[5]。
この図における『verifier(検証器)』の生成機能を作ろう、というのがK側から見たこの研究の目的となります。
ちなみに、Kの開発チームは言語定義作成をかなり頑張っていて、githubを見ると20近い既存言語についてリポジトリがあります。大雑把に言えば、この研究で20言語分の検証器を一度に作成したということですね。
プログラム検証 とは、プログラムを検証することです。形式検証とも呼ばれます。
世間ではプログラムの動作を保証したい時に、実際に入力を流し込んで期待する出力が得られるか確認するテストがよく用いられます。これは多数の入出力例を用意する必要があり、また普通は実際にプログラムを動かさなければ検査できません[6]。
一方でプログラム検証は、無限通りの入力パターンがあっても漏れなく確認でき、また実行することなく検証可能であることが強みです。その分手法として重かったりはしますし、単純にどちらが強いと言える話ではありませんが。
世の中にプログラム検証器は既に色々ありまして、例えばC言語ならVCCとかFrama-CとかVerifastとか……本当に色々あります。
これらのツールは、基本的に中間言語を使います。つまり、プログラムを検証し易い言語に変換し、検証したいことを同じく分かり易いように変換して、汎用的な検証器に投げるというわけです。ただし、この変換は自明なものではなく、バグを生みがちです。
プログラミング言語のセマンティクスをそのまま流用できるような論理体系を構築し、その中で検証したい条件を書くことができれば、そんな間違いは起こらないはず……というのが、検証側から見たこの論文の目的です。
以下に、二分木構造nodeと、そこに値を挿入する関数insertを定義したC言語のプログラム片を示します。これが今回の検証対象です。ポインタを扱っているあたりが挑戦的ですね。
struct node{
int value;
struct node *let, *right;
}
struct node* new_node(int v){
struct node *node;
node = (struct node *)malloc(sizeof(struct node));
node->value = v;
node->left = NULL;
node->right = NULL;
return node;
}
struct node* insert(int v, struct node *t){
if(t == NULL) return new_node(v);
if(v < t->value) t->left = insert(v,t->left);
else if(v > t->value) t->right = insert(v,t->right);
return t;
}
このinsertは、引数の木が二分探索木(BST)だった場合、返り値もBSTになるように作られています。例えば、次の図はinsertを[8,3,6,10,14,1,13,7,4]の順で行った時にできるBSTです。
となれば当然、insertが本当にBSTを作るのか検証したくなりますね。……なりますよね?
本研究で作成した検証器では、このことを以下のような仕様として書くことができます。
rule <functions>FUNCTIONS:Map</functions> <structs>STRUCTS:Map</structs> <mem>...MEM:Map(tree(L1, T1:Tree) ⇒∀ tree(?L2, ?T2:Tree))...</mem> <threads> <thread>...<k> insert(tv(V:Int, int), tv(L1:Loc, struct node)) ⇒∀ tv(?L2:Loc, struct node)... </k>...</thread> </therads> requires bst(T1) ∧ -2147483648 ≦ V ∧ V ≦ 2147483648 ensures bst(?T2) ∧ tree_keys(?T2) = {V}∪tree_keys(T1)
いきなりXMLが出てきて面食らったかもしれません。これはシステムの状態を表すK特有の記法ですが、プログラムを表す <k>〜</k>
に注目すれば雰囲気が掴みやすいかもしれません。
この仕様は端的にいえば、BSTであるL1[7]に対してinsert(L1,V)を行った時、L1にVを加えたBSTになるということです。214783648はint型で表現できる範囲を表しており、?Xは「その条件を満たすようなXが存在する」くらいの意味で捉えてください。
ここで一つ重要なのは、 <mem>〜</mem>
の部分でメモリの状態を直接示していることです。このmemは(検証器が用いる論理体系ではなく)KにおけるC言語のセマンティクスで定義されたもので、名前も中身も各言語が好きに決められます。
論文では他言語対応であることを示すために、さらにJavaとJavaScriptで同様のプログラムと仕様を書いていますが、ここでは割愛します。
この論文の中核にあるのは、 Matching Logic と呼ばれる論理体系です。これは以下のような特徴を持ちます。1はMatching Logicの使い方の話で、2は定義上の話、3は2から導かれる性質なので、ちょっと不揃いではありますが。
-
configurationと呼ばれる、システムの状態を表す論理式を持つ
-
上で見たXMLに相当するもので、実行途中のプログラム・変数の値・関数テーブルなど、それぞれの言語に必要な情報を全て含むことができる
-
-
真理値ではなく値の集合を論理式に割り当てる
-
configurationの場合、そのプログラムが取りうる状態の集合になる
-
<k> 2 * ?N </k>
なら{0,2,4…}とかいうノリ
-
-
configurationとそれが持つ条件を並べて書ける
-
<k> 2 * ?N </k> ∧ (?N < 10)
とか
-
先ほどの仕様例で言えば bst(T1) ∧ 〜
などがMatching Logicの一つの論理式になります。
そして、Matching Logicにおける論理式間の関係reachabilityを定義することで、Reachability Logicが生まれます。reachabilityとは簡単に言えば「論理式φからルールに従って論理式φ’にすることができるか」というもので、 φ ⇒∀ φ'
のように書きます。
φが状態を表すので、「φという状態から実行するとφ’になるか[8]」という仕様を表すことができるわけです。
Reachability Logicの特徴としては以下のものがあります。
-
セマンティクスをほぼそのままの形で使える
-
セマンティクスにおけるルール定義がそのままReachability Logicのルール定義になる
-
-
非決定的なプログラムに対して検証可能
-
現実に評価順などが不定な言語仕様は多い
-
セマンティクスをそのまま使えて仕様が書けるような論理体系を作ることができたので、あとはこれに対して自動推論することができれば、それが検証器になります。
Matching LogicやReachability Logicの詳細は推敲前の版で書いているので、気になる方はどうぞ。
冒頭で述べた通り、この理論を用いた検証器生成器がKに実装されています。2.5人年[9]かけて3万行ほどのJavaを書いたとのこと。基本はシンボリック実行[10]なので、SMTソルバZ3を利用しています。
評価としては、連結リストおよび木に対する18の基本的操作について、その性質を検証して時間を計測しています。例として示したinsertもその一つですね。
結果を見ると、検証にかかる時間は1秒未満から5分程度。それなりに複雑なプログラムも入っていることを考えると、まずまずの結果ではないでしょうか。筆者らはこの結果から、本システムは十分に実用的であると結論しています。
続いて、Kにおける各言語のセマンティクスおよび今回の仕様の記述コストについて。表でまとめます[11]。
C | Java | JavaScript | |
---|---|---|---|
セマンティクス開発期間(ヶ月) |
40 |
20 |
4 |
セマンティクス行数 |
17,791 |
13,417 |
6,821 |
セマンティクス修正期間(日) |
7 |
4 |
5 |
セマンティクス修正行数 |
468 |
95 |
49 |
仕様数 |
36 |
31 |
31 |
定義数 |
27 |
27 |
27 |
各言語のセマンティクスは今回の研究とは無関係に開発されたものですので、こちらに要した手間はあまり気にしないことにしましょう。
今回使うにあたってバグに気付いたとか検証しにくかったといった理由からセマンティクスに一部変更を加えており、それが『セマンティクス修正』の項です。仕様数は読んで字の如く。定義数というのは、上であげたbstなどの定義をいくつ書いたかを意味します。
Kに対象言語のセマンティクスが存在するという前提下においては、1週間もあれば検証器が作れてしまうわけです。「言語非依存の検証基盤を作る」という目的は十分に達成されたのではないでしょうか。
次のことについて見てきました。
-
セマンティクスを重視したプログラミング言語開発ツールK
-
セマンティクスとプログラム仕様を同じところに落とし込むReachability Logic
-
それらを利用した言語非依存の検証基盤の実用性
個人的に、3つのアイデアはそれぞれ非常に興味深く、かつ実用性もあり、「今年読んだ一番面白い論文」に相応しいと思っています。
実際にこの検証器を使いたいかというと……今後に期待ですね。K自体については勉強中なので、そのうち何かしらアウトプットできるかもしれません。
今年読んだ一番好きな論文2017 Advent Calendarは現時点でまだ募集中のようです。異様に豪華な賞品ラインナップにも関わらず、前半スッカスカで寂しいです。
全く面識の無い人間が当日に飛び込んでも歓迎されることは去年実証されているので、明日以降、これを読んでいる皆さんの論文紹介に出会えることを楽しみにしています[12]。
ここまでお付き合いいただきありがとうございます。お粗末様でした。