RHGの逆襲 第6回

ラクリナックスにて。毎回会場をお貸ししてくださるミラクリナックス社さんとよしおかさんに感謝します。

今回は第10章「パーサ」ということで、第2部に入りました。区切りもいいから途中参加を狙っている人はどうぞーというアナウンスがメーリングリストにあったことや、Yuguiさんの「はじめてのRubyトークセッションでも紹介されたそうで、初参加の方がたくさんいて人数も30人弱くらいだったかな?最後にちょっと自己紹介でもしましょうかってことになって、全員少しずつ喋る機会があってこれは良かった。東京Rails勉強会ではポジションペーパーを書くことになっていますよね。ポジションペーパーはいまいち何書いていいのかわからなくて、あと自宅は印刷環境が貧弱だとか少々敷居が高く感じられるけれど、勉強会の最初にどういう目線で参加しているのかってことを話す時間があるのはいいかもしれませんね。

内容は、まずさわださんが RHG 10章の内容と照らし合せつつ parse.y を拾い読みする発表*1と、星一さんが 1.8 から 1.9 への文法の変更点と parse.y 上の変更を調べてみたという発表。

以下ほぼメモからそのまま箇条書きで転載。

  • parse.y 10000 行あるよ。魔境だ。。。
  • Yugui さんによる「3分でわかる yacc 速修」
    • BNF 記法
    • 状態遷移とかユーザアクションとか……いやすみません良く聴いてなかったこのへん
    • スキャナのこと
    • というわけで今日は ruby の parse.y を見ていこう
    • ruby のソースについてくるパーサ関係のユーティリティ
      • sample/exyacc.rb
      • ext/ripper/tools/preproc.rb
  • さわださん発表
    • parser.y のユーザアクション部にみられる /*%%%*/ とか /*% ... %*/ というコメントは、ripper と ruby 本体で文法定義を共有するために、両方のユーザアクションを記述して、前処理で切り替えるようになっているらしい。知らなかった。
    • おおざっぱな構成
      • program
      • stmt
      • expr
      • arg
      • primary
      • あと term という要素がある。普通 term っていうと「項」のことだけど、ruby では「文末のデリミタ(ターミネータ)」の意で使われている(改行もしくはセミコロン)
    • stmt
      • alias
      • if修飾子
      • 右辺がかっこなしメソッド呼び出しの代入
      • expr 規則
    • expr
      • and/or
      • !/not
      • arg 規則
    • arg
    • primary
      • リテラル
      • 変数参照
      • メソッド呼び出し
      • begin ... end
      • 制御構造
      • クラス定義、メソッド定義
    • %parse-param でパーサの情報をグローバル変数じゃなくてヒープにとれるようにしている。このへんはオライリーyacc/lex入門がいいと思う。ちょっと古いけど。あとは bison の info くらいしか情報がなかった気がする。
    • ついでにそれを struct RData でラップして ruby のオブジェクトにしている。そういえば後でゴルフ場オーナーこと id:shinichiro_h さんがこれくらい自分で管理すればいいんじゃね、ということをおっしゃってましたが、 struct parser_param の中に VALUE 型で ruby のオブジェクトを参照しているものがあるので GC の mark フェーズでそれらもマークしてあげないといけないので、それを一番簡単に実現する方法が struct RData で wrap することなんだと思います。確か作成する構文木のノード自体が GC 対象になると思うので(ってこれは 1.8 の時代の話かも)、パーサ用構造体内部を ruby のメモリ管理から完全に独立させるのは面倒そう。
    • スキャナの実装について(yylex)
      • ファイルのパースの時と文字列(メモリ上)のパースの時で使用する関数が違う。関数テーブルでポインタを切り替えることで多態化しているという kernel の vfs とかでよく見られるあれで多態化している。
    • 文字列リテラルの中の式の埋め込み対応のあれこれ。
      • 最初のクオートを読んだらすぐに「文字列リテラル開始」というトークンだけ返す
      • パーサの状態を変更することで「リテラル読み込み中状態」になる
      • 文字列内で "#{" をみつけたら埋め込みトークン(tSTRING_DBEG)を返して、パーサの埋め込みアクション部分でパーサの状態を変更して、compstmt ルール と '}' をパースさせて、また埋め込みアクションで状態を戻すと。このへん前からどうやってるのか気になってたので読めて良かった。
    • ヒアドキュメントもちょっと面倒(RHG10章末尾のとおり)
      • heredoc_restore とか
  • 星一さん
    • 1.8 と 1.9 の差について
      • ハッシュリテラル
        • キーがシンボルの時は json っぽく書ける
      • ラムダ式
        • ->{...} という文法
      • 正規表現
        • エンジンが鬼車に変更。しかし parse.y はあまり変わってない
        • エラーメッセージがちょっと良くなってる
      • エンコーディング
        • マジックコメント
          • なにか条件判定が非効率らしい。直すかも。
        • あと個人的に見てたところでは、?の後に文字を入れる表現で全角空白などが使えるようになってた
      • 他にブロック引数など

1.8 *2の時代から既にパーサは魔境だったのでできるだけ避けていました。自分で何度か yacc(bison) や racc を使った今はちょっとはわかるようになっていて安心しました。それでも条件付きのスキャナとかなんかすごく頑張ってて、読めば読めそうだけど非常に面倒そう。あれだけのパーサで、shift/reduce conflict が1つもないとか凄すぎる。

次回は 12章「構文木の構築」。予定は 8/24(日)。

勉強会後は新しいメンバたくさんで懇親会になったようですが、明日は久々の客先で体調を万全にしておきたかったので今日は欠席。また次回お会いしましょう。

*1:Yugui さんがところどころ yacc に慣れ親しんでいない人向けの注釈を加えつつ

*2:というか最初に触れたのは 1.4