SGLR (scannerless generalized-LR parsing) と CAS+LR (context-aware scanning used with deterministic LR parsers) を比較している Context in Parsing: Techniques and Applications で参照されていた、SGLR を推す論文を読んでみた。
Pure and declarative syntax definition: paradise lost and regained [PDF]
これはなかなか面白い。
SGLR の利点がたくさんあげられていてそれぞれ納得できるものではあるが、 文法の曖昧さを静的に確認できないという問題はある。
また、いろいろあげられている利点には、CAS+LR に導入可能なものも多いと思う。
他にもいくつか興味深いところがある論文があったのであげておく
A trustworthy mechanized formalization of R [PDF]
GNU R は改行を無視するかどうかを lexer hack で実装している。
function () break + 1 で、function や + の直後に改行を挿入しても意味は変わらないが、break の直後に挿入すると意味が変わる。
Context-aware scanning and determinism-preserving grammar composition, in theory and practice
上の Context-Aware Scanning for Parsing Extensible Languages の著者の一人の D論で、 Background and related work のところにどういうのがいいのかという考え方や他の手法に関する見解がいろいろ書かれている。
事前に決定的に構文解析できる (ただひとつの構文木が得られる) ことを保証できるのが重要で、 GLR みたいに複数の構文木が得られる可能性があったり、 PEG のようにひとつの構文木が得られるにしてもそれが正しいものなのかの保証がないのはよろしくない、 という立場のようである。
Context in Parsing: Techniques and Applications
Context-Aware Scanning for Parsing Extensible Languages のもうひとりの著者による SGLR (scannerless generalized-LR parsing) と CAS+LR (context-aware scanning used with deterministic LR parsers) の比較。 (著者は CAS+LR を開発している。)
SGLR は文字をトークンとして GLR を使い、文法が曖昧で複数の構文木が出てくる場合には後でフィルタする。 フィルタにより確実にひとつの構文木が残るかどうかは保証されない。
CAS+LR は LALR parser の状態に応じて受付可能なトークンを lexer が生成する。 曖昧な文法は LALR でコンフリクトが検出される。 ただし曖昧でなくてもコンフリクトが検出されることはあり、そのような場合は文法を LALR で扱える形に変形する必要がある。
ということで、その変形のコストは静的な保証にみあうものなのか、という問題になるのだが、 この論文の著者は言語を作るのは言語の専門家を想定していて CAS+LR で必要になる変形は許容可能と想定しているのに対して、 SGLR (を使ったツールを開発している研究者) はもっと普通の開発者が言語を作ることを想定していてその変形はつらい、という立場の違いに起因するということのようだ。
Parsing and Analyzing C/C++ code in Eclipse
Eclipse はパフォーマンスの都合でヘッダファイルを読み込まずに C のコードをパースするので、 識別子が型か変数か判断できなくて構文解析での曖昧さを避けられないことがある。 ということでそういう場合は両方の場合の構文木を作って、両方を保持した曖昧なノードとして扱う。
もうひとつ興味深いものとして、C の構文解析の話をみつけた。 C の構文解析では typedef 名が問題というのはよく知られているが、ちゃんとやるのはそう簡単ではないようだ。
A Simple, Possibly Correct LR Parser for C11
これも Menhir を使っているのだが、Incremental API でトークンが受け付けられるかどうかを試すということはしておらず、 (いつもの) グローバル変数での parser から lexer へ情報を渡すということをしているようだ。
この論文でとくに面白いと思った例は以下である。
typedef int T; void f(void) { for(int T; ;) if(1); T x; x = 0; }
T という識別子がトップレベルでは型として宣言され、for 文では変数として宣言されている。 ここで for 文で行われた宣言はこの for 文の中だけで有効である。
で、for 文が終わった直後に T が使われていて、これは for 文の内部ではないので、変数ではなく、型として認識されなければならない。 そのためには、変数 T のスコープが終了したということを parser から lexer に伝えなければならない。 それは、for 文を reduce したときのアクションで伝えることになるだろう。 しかし、そのアクションが実行されるときにはすでにこの直後の T というトークンは先読みとして読まれているのである。 なんでかというと、if (1); の後にもし else が来ると文はまだ続くので、ここでは先読みして文が終わることを確認しないと reduce できないのである。
つまり、無理である。
が、暗黙の想定を壊せばどうにかなるもので、 論文ではこれを解決している。 どう解決しているかというと、識別子はひとつのトークンであるという想定を破壊し、識別子をトークンふたつに分けるということをしている。 NAME, TYPE, VARIABLE という 3つのトークンを用意し、 型は NAME TYPE という連続する 2つのトークンで表現し、 変数は NAME VARIABLE という連続する 2つのトークンで表現する。 こうすると、上の例で if 文を認識する reduce で先読みが起こるが、それは NAME トークンを先読みすることになる。 そして、(for 文を認識する) reduce のアクションでスコープの情報を lexer に渡すと、それに基づいて次のトークンが TYPE か VARIABLE か適切に判別される。
なんともトリッキーである。
なお、この文脈では型と変数の両方が受け付けられるので、シェルでやったような、parser にトークンが受け付けられるかどうか尋ねる、という方法では解決できない。 ではどうするのがきれいか考えると、parser で継承属性を扱えるようにして、スコープをそういう属性として定義し、そのスコープを参照して semantic predicate で型か変数か区別できるといいのかも。
Menhir の Incremental API を使ってシェルの構文解析を行うという話がすでにある。
Morbig: A Static Parser for POSIX Shell [PDF]
シェルでは、文脈に応じて、キーワードに見える文字列が、キーワードになったりならなかったりという動作がある。 以下は論文にあげられている例である。
for i in a b ; do echo $i ; done ls for i in a b
上記の1行めの "for i in a b ; do echo $i ; done" の中の for, in, do, done はキーワードであるが、 2行めの "ls for i in a b" の中の for, in はキーワードでない。
同様なことがコマンドに対する環境変数の設定でも起きる。
CC=gcc make make CC=cc
"CC=gcc make" の中の CC=gcc は make コマンドに対する環境変数 CC を gcc に設定するという意味だが、 "make CC=cc" の中の CC=cc は、make コマンドの第1引数に CC=cc を与えるという意味である。
shell の構文解析を lexer と parser に分割して実装するなら、 キーワードや環境変数設定を受け付けられる文脈かそうでないかを lexer が (parser の状態に依存して) 判定する、という実装が可能だろう。
ただ、受け付けられないキーワードを生成しなければならないこともある。 以下は、else キーワードは先頭には出現できないので、parse error にならなければならない。
else echo foo
しかし、キーワードを受け付けられるときには lexer がキーワードなトークンを生成するというやりかただと、 else は受け付けられない (parse error になる) ので else がキーワードにならず、else コマンドを実行するということになるだろう。
なので論文では、parser の状態 (LR スタック) を調べて、このときもキーワードにする細工をしているのだが、これは素直でない気がする。
ちょっと考えるに、キーワードをキーワードとして扱うのは、parser がキーワードを受け付けるというよりはもうちょっと大雑把に、 コマンドの先頭では常にそうする、という感じに扱った方がいいのではないかと思う。
なお、論文には他にもいろいろ厄介な話がかいてあるが、alias がいちばん問題というかどうにもならなさそう、と思った。
yacc や bison といった parser generator が生成する parser では、parser が lexer を呼んでトークンを得る。 Menhir (parser generator) でもデフォルトでは同じなのだが、Incremental API というものを使うと、違う形式で parser を利用できる。
Menhir の Incremental API では parser の状態が値として表現されて、 トークンを与えると次の状態を得られる、という形で利用できる。 これを使うとこのトークンを試してうまくいかなかったらあのトークンを試す、 といったことができそうである。
Menhir Reference Manual - 9.2 Incremental API
これについて調べていたところ、どうも Incremental API は Merlin (OCaml 用 language server) のために実装されたもののようである。
Merlin: A Language Server for OCaml (Experience Report)
Merlin は language server なので、人間が行うプログラムの変更に追随して高速に解析を行う必要があり、 そのためにインクリメンタルに動作させられる API が必要だったということだろう。 (たぶん、プログラムの途中におけるパーサの状態をいろいろとっておいて、変更より前の状態のうちでいちばん先のものからパースし直すのではないかと思う)
ただ、この Incremental API は他の用途にも使えて、以下の3つの用途があげられている。
興味があるのはふたつめの lexer hack であり、これは想定範囲の用途なのだろう、とわかる。
ここですでに使われた例として shell script の構文解析があげられている。 これをみてみよう。
eq の場合わけをうまく使うために b = ~~ c の右辺を変数にしたいわけだが、 等式の左右を逆にするのではなく、右辺の negb (~~) を剥がして左辺にもってくることも考えられる。
今回は Hb : b = ~~ b0 の b が変数だったので左右を逆にするだけで右辺を変数にできたが、b が変数でない項だったらそれではうまくいかず、negb を左辺にもってきたいということもあるかもしれない。
結局のところ b = ~~ c と ~~ b = c は等価なので、相互に変換できるはずという話なのだが、これは実際そうで、SSReflect には以下の補題がある。
About negbLR. (* forall [b c : bool], b = ~~ c -> ~~ b = c *) About negbRL. (* forall [b c : bool], ~~ b = c -> b = ~~ c *)
しかしこれらは opaque で計算が進まないので、自前で証明しなおし、negbRL' (negbLR' H) = H を証明しておく。
Lemma negbLR' : forall [b c : bool], b = ~~ c -> ~~ b = c. Proof. by case ; case. Defined. Lemma negbRL' : forall [b c : bool], ~~ b = c -> b = ~~ c. Proof. by case; case. Defined. Lemma negbRLR : forall [b c : bool] (H : b = ~~ c), negbRL' (negbLR' H) = H. Proof. move=> b c H. subst b. by case: c. Qed.
これらを使うと、以下のように証明できた。
Lemma case_EvenOddList'' (A : Type) (P : forall (b : bool) (x : EvenOddList A b), Prop) (Hnil : P true (nil A)) (Hcons : forall (b : bool) (a : A) (y : EvenOddList A b), P (~~ b) (cons A a b y)) (b : bool) (x : EvenOddList A b) : P b x. Proof. refine (match x as x' in EvenOddList _ b' return forall (Hb : b = b') (Hx : (rew Hb in x) = x'), P b x with | nil => _ | cons _ _ _ => _ end erefl erefl). move=> Hb. subst b => /= Hx. subst x. exact Hnil. move=> Hb. (* rew [EvenOddList A] Hb in x = cons A a b0 e -> P b x *) rewrite -(negbRLR Hb). (* rew [EvenOddList A] negbRL' (negbLR' Hb) in x = cons A a b0 e -> P b x *) Check negbLR' Hb. (* ~~ b = b0 *) destruct (negbLR' Hb) => /=. (* rew [EvenOddList A] negbRL' erefl in x = cons A a (~~ b) e -> P b x *) destruct b => /= Hx. subst x. exact (Hcons false a e). subst x. exact (Hcons true a e). Defined.
Hb を negbRL' (negbLR' Hb) に書き変えて、negbLR' Hb : ~~ b = b0 を destruct すると b0 が ~~ b に置換される (また negbLR' Hb が erefl になる)。
ただそこから、b : bool を場合分けしないと計算が進まない。 これは negbRL' が内部で引数を場合分けしているためで、具体的な値 (true あるいは false) にする必要がある。 b を場合分けして具体的な値にしてしまえば、あとは問題なく証明できる。
ただ、ひとつ疑問がある。 cons の場合のところで、コンテキストに Hb : b = ~~ b0 があって subst b がこれを片付けてくれる (b を ~~ b0 に置換すると同時に、Hb を erefl に置換してくれる)。 しかし、 b = ~~ b0 は eq b (~~ b0) であり、パラメータが b でインデックスが ~~ b0 なので、 eq の場合分けはインデックスをパラメータに置換するので、~~ b0 を b に置換しようとしてうまくいかないはずである。
実際に試してみると、subst b のかわりに destruct Hb とするとやはり失敗する。
Lemma case_EvenOddList' (A : Type) (P : forall (b : bool) (x : EvenOddList A b), Prop) (Hnil : P true (nil A)) (Hcons : forall (b : bool) (a : A) (y : EvenOddList A b), P (~~ b) (cons A a b y)) (b : bool) (x : EvenOddList A b) : P b x. Proof. refine (match x as x' in EvenOddList _ b' return forall (Hb : b = b') (Hx : (rew Hb in x) = x'), P b x with | nil => _ | cons _ _ _ => _ end erefl erefl). move=> Hb. subst b => /= Hx. subst x. exact Hnil. move=> Hb. (* Hb : b = ~~ b0 *) Fail destruct Hb. (* The command has indeed failed with message: Abstracting over the terms "b1" and "Hb" leads to a term fun (b2 : bool) (Hb0 : b = b2) => rew [EvenOddList A] Hb0 in x = cons A a b0 e -> P b x which is ill-typed. Reason is: Illegal application: The term "@eq" of type "forall A : Type, A -> A -> Prop" cannot be applied to the terms "EvenOddList A b2" : "Type" "rew [EvenOddList A] Hb0 in x" : "EvenOddList A b2" "cons A a b0 e" : "EvenOddList A (~~ b0)" The 3rd term has type "EvenOddList A (~~ b0)" which should be a subtype of "EvenOddList A b2". *)
subst に頼らずに自分でやるにはどうしたらいいか考えると、まぁ、等式の左右を逆にすればいいのである。 2回逆にするともとに戻るので、もともと Hb を使っていたところをそう書き変えて、1回逆にしたやつを destruct すればいいはずである。
逆にするのは SSReflect では esym であり、2回逆にすると戻るというのは esymK という補題がある。
About esym. (* forall {A : Type} {x y : A}, x = y -> y = x *) About esymK (* forall [T : Type] [x y : T], cancel esym esym *).
vanilla Coq なら Logic.eq_sym と eq_sym_involutive である。
About Logic.eq_sym. (* forall [A : Type] [x y : A], x = y -> y = x *) About eq_sym_involutive. (* forall [A : Type] [x y : A] (e : x = y), Logic.eq_sym (Logic.eq_sym e) = e *)
で、esymK を使ってやってみるとやはりできた。
(* rew [EvenOddList A] Hb in x = cons A a b0 e -> P b x *) Check (@esymK _ b (~~ b0) Hb). (* esym (esym Hb) = Hb *) rewrite -(@esymK _ b (~~ b0) Hb). (* rew [EvenOddList A] esym (esym Hb) in x = cons A a b0 e -> P b x *) Check (esym Hb). (* ~~ b0 = b *) destruct (esym Hb) => /= Hx. subst x. exact (Hcons b0 a e).
Defined.
この前の Proof Summit 2025 では、依存マッチで生成される連立方程式がコンストラクタと変数だけからなる等式なら単一化でどうにかできる (かも)、 という話をしたが、発表の中に出てきた (Lean のマニュアルからとってきた) EvenOddList はインデックスのところに negb というコンストラクタではない関数が入っているので その条件を満たさない。
これを場合分けできるかやってみよう。
From mathcomp Require Import all_ssreflect. Import EqNotations. Inductive EvenOddList (A : Type) : bool -> Type := | nil : EvenOddList A true | cons : A -> forall (even : bool), EvenOddList A even -> EvenOddList A (negb even). Lemma case_EvenOddList (A : Type) (P : forall (b : bool) (x : EvenOddList A b), Prop) (Hnil : P true (nil A)) (Hcons : forall (b : bool) (a : A) (y : EvenOddList A b), P (~~ b) (cons A a b y)) (b : bool) (x : EvenOddList A b) : P b x. Proof. refine (match x as x' in EvenOddList _ b' return forall (Hb : b = b') (Hx : (rew Hb in x) = x'), P b x with | nil => _ | cons _ _ _ => _ end erefl erefl). (* 2 goals A : Type P : forall b : bool, EvenOddList A b -> Prop Hnil : P true (nil A) Hcons : forall (b : bool) (a : A) (y : EvenOddList A b), P (~~ b) (cons A a b y) b : bool x : EvenOddList A b ______________________________________(1/2) forall Hb : b = true, rew [EvenOddList A] Hb in x = nil A -> P b x ______________________________________(2/2) forall Hb : b = ~~ b0, rew [EvenOddList A] Hb in x = cons A a b0 e -> P b x *) move=> Hb. subst b => /= Hx. subst x. exact Hnil. move=> Hb. (* Hb : b = ~~ b0 *) subst b => /= Hx. subst x. exact (Hcons b0 a e). Defined. Check case_EvenOddList. (* case_EvenOddList : forall (A : Type) (P : forall b : bool, EvenOddList A b -> Prop), P true (nil A) -> (forall (b : bool) (a : A) (y : EvenOddList A b), P (~~ b) (cons A a b y)) -> forall (b : bool) (x : EvenOddList A b), P b x *)
まぁ、できました。
発表スライド: Coq ユーザが Lean を調べてみた
Debian で、昔ながらの (network-manager を使わない) ネットワークの設定は /etc/network/interfaces にいろいろ書くのだが、 その形式は manpage として interfaces(5) に書いてある。
が、OPTIONS PROVIDED BY OTHER PACKAGES のところに記載があるように、ifupdown ではない他のパッケージによって提供されているオプションもあって、そういうのはこのマニュアルには書いていない。
ここで、wpasupplicant が提供しているものを調べようと思って、マニュアルを眺めてみたが見つからず、 (Debian パッケージの) ソースをとってきたら wpasupplicant.README.Debian というファイルがあってそこに書いてあった。
/etc/network/interfaces に wpasupplicant の設定を行うのは Debian 独自のやりかたなのかな。