青木です。

さきほど Ripper の本体を CVS HEAD にコミットしました。
Ripper は単なる拡張ライブラリではなく ruby 本体の parse.y
を置き換えるので、それなりに大きい変更です。そこで重要な
点について解説しておきます。


== 現在のパーサとの互換性について

#undef RIPPER で cpp を通した場合、以下の点を除いてオリジナルの
parse.y とテキストレベルで同等になることを確認しました。

* enum lex_state → enum lex_state_e

  Ripper ではマクロ lex_state が定義されるためです。

* YYSTYPE 共用体に VALUE val メンバを追加

  Ripper のパーサで使うためです。

* yacc の定義するテーブル yyline が違う

  ソースコードの行番号がズレているからです。

* nd_nest 関連
  #define nd_nest u3.id → u3.cnt   ※ unsigned long → long
  tokadd_string() 第四引数の nest を int* → long* に変更

  意図がよくわからなかったのですが、long に統一しました。

* static 関数 lvar_defined() が定義されている

  Ripper の処理と共通化するときに、
  関数になっていたほうが都合がよかったためです。


== parse.y の記法制限について

文法規則中で #ifdef を使うとあまりに見にくかったのと、規則まるごと
(ブレースごと) 消さなければならない部分があるため、特殊なコメント
記法と Ruby で書いた専用プリプロセッサを使っています。
以下に例を示します。

program         :  {
                    /*%%%*/
                        lex_state = EXPR_BEG;
                        top_local_init();
                        if (ruby_class == rb_cObject) class_nest = 0;
                        else class_nest = 1;
                    /*%
                        lex_state = EXPR_BEG;
                        class_nest = !parser->toplevel_p;
                        $$ = Qnil;
                    %*/
                    }

見れば明らかなんですが、/*%%%*/ と /*%〜%*/ がそれです。
ripper.so を作っている場合は次のようになります。

program         :  {
#if 0
                        lex_state = EXPR_BEG;
                        top_local_init();
                        if (ruby_class == rb_cObject) class_nest = 0;
                        else class_nest = 1;
#endif
                        lex_state = EXPR_BEG;
                        class_nest = !parser->toplevel_p;
                        $$ = Qnil;

                    }

行番号が変わらないので、Ripper のデバッグをするときに便利です。

記法はもう一種類あります。

stmts           : none
                    /*%c%*/
                    /*%c
                    {
                        $$ = dispatch2(stmts_add, dispatch0(stmts_new),
                                                  dispatch0(void_stmt));
                    }
                    %*/

これがさっき言った「アクションまるごと消す」やつで、
preproc.rb を通すと次のようになります。

stmts           : none
/*
*/
                    {
                        $$ = dispatch2(stmts_add, dispatch0(stmts_new),
                                                  dispatch0(void_stmt));
                    }

こちらも行番号は変わりません。


== parse.y 編集時の注意

  * 上記の仕掛けを実現するため、アクションのRipper側では
    コメントが使えません (ruby 側では問題ありません)。

  * parse.y の ruby 側でグローバル変数になっている名前は
    Ripper ではすべてマクロとして定義されます。
    従ってグローバル変数の識別子と重なる名前は一切使えません。

    例: enum lex_state は不可

  * 文法を変更する場合は、とりあえず /*% 〜 %*/ だけ書いて
    おいてもらえれば ripper 側のコードはこちらで追加します。
    (書いてくださっても全然構いませんけど)


== Ripper の構造について

Ripper の仕組みについてポイントを説明します。

  * 核になるデータ構造は struct ripper_params。
    yyparse と yylex の引数として渡ってくる。

  * Ripper では ruby が単に捨ててしまう空白やコメントも
    扱う必要があるため、トークンの取りかたがかなり違う。
    以下は Ripper の parse.y より抜粋。

/*
    Structure of Lexer Buffer:

 lex_pbeg    old_lex_p              lex_p           lex_pend
    |            |                     |               |
    |------------+----------+----------+---------------|
                 |<-------->|<-------->|
                    space   | non-space token
                            |
                            token_head
*/

    Ruby プログラムは常に「長さゼロ以上の空白 + 非空白トークン」
    のリストで構成されることに注目し、このペアを「一単位」として扱う。
    わずかな例外を除いて一単位の終了時には yylex() に制御が戻るので、
    このときにスキャナイベントを発生する。一単位中の空白と非空白の
    区切りは、空白文字のスキャン部分を改造して検出する。

  * 上記の単位を構成しない例外はコメントとヒアドキュメントである。
    幸いどちらのコードも一ヶ所にかたまっているので #ifdef で
    がんばって対処する。


== Known Bug

  * Ruby の入力バッファは CR LF の CR を捨てるので、ripper に
    かけると CR が消える。


== 高速化のためのノート

現在は定義されていないイベントも含めて全部メソッドを呼んでいるが、
あらかじめ必要なイベントのリストがわかっていれば全部呼ぶ必要はない。
method_missing に注意する必要があるが、パース開始前に respond_to?
で必要なイベントを列挙しておけば重い処理が減る。

-------------------------------------------------------------------
青木峰郎