Scintilla 用解析器の書き方How to write a scintilla lexer

言語毎に用意される解析器は、指定範囲の文字列をどう色づけするべきかを定めます。解析器に求められることは「指定された」文字列に色を付けるということだけですので、解析器を書くのは比較的簡単な作業です。色づけの判断に必要な文字列はどれなのかを定めるという難しい作業は Scintilla 自身が行い、そうして解析器を呼び出します。

A lexer for a particular language determines how a specified range of text shall be colored. Writing a lexer is relatively straightforward because the lexer need only color given text. The harder job of determining how much text actually needs to be colored is handled by Scintilla itself, that is, the lexer's caller.

媒介変数Parameters

LLL と言う言語の解析器を書くための雛形は次のようになります。

The lexer for language LLL has the following prototype:


    static void ColouriseLLLDoc (
        unsigned int startPos, int length,
        int initStyle,
        WordList *keywordlists[],
        Accessor &styler);

スタイル付けを実際に行うのは Accessor オブジェクトです。解析器はこれを用いて色を付ける文字列にアクセスしなくてはなりません。解析器は styler.SafeGetCharAt(文字の位置) という方法で特定位置の文字を得ます。

The styler parameter is an Accessor object. The lexer must use this object to access the text to be colored. The lexer gets the character at position i using styler.SafeGetCharAt(i);

startPos と length は色を付け直す文字列の範囲を示しています。解析器はこの範囲の文字すべてに適切な色を定めなくてはなりません。startPos から startPos+length までがその範囲です。

The startPos and length parameters indicate the range of text to be recolored; the lexer must determine the proper color for all characters in positions startPos through startPos+length.

initStyle は startPos の直前の文字のスタイルを表し、同時に与えられた範囲の文字列の色づけ方法をも示しています。

The initStyle parameter indicates the initial state, that is, the state at the character before startPos. States also indicate the coloring to be used for a particular range of text.

注意: StartPos にある文字は行の先頭であると仮定されています。ですから、改行により initStyle の状態が終了するのであれば解析器は基本スタイルか initStyle に続くべきなんらかのスタイルに戻るべきということになります。

Note: the character at StartPos is assumed to start a line, so if a newline terminates the initStyle state the lexer should enter its default state (or whatever state should follow initStyle).

keywordlist は解析器が理解しなくてはならないキーワードの一覧です。Wordlist クラスオブジェクトはキーワード検出を簡単にするメソッドを持っています。既存の解析器はキーワードを検出するための classifyWordLLL という関数を持っています。これらの関数を調べれば keywordlist を使った検出がどのように行われるかがわかります。この文書ではキーワードについての詳細は記しません。

The keywordlists parameter specifies the keywords that the lexer must recognize. A WordList class object contains methods that make simplify the recognition of keywords. Present lexers use a helper function called classifyWordLLL to recognize keywords. These functions show how to use the keywordlists parameter to recognize keywords. This documentation will not discuss keywords further.

解析器のコードThe lexer code

解析器の役目は簡潔に要約できます。同じ色づけを範囲 r の文字に行う場合、解析器は次の呼び出しを行います。

The task of a lexer can be summarized briefly: for each range r of characters that are to be colored the same, the lexer should call


    styler.ColourTo(i, state)
        

i は範囲 r の最後の文字の位置です。変数 state には位置 i の色づけの内容を指定し、これを要求された範囲の全体に対して行います。

where i is the position of the last character of the range r. The lexer should set the state variable to the coloring state of the character at position i and continue until the entire text has been colored.

注意1: styler ( Accessor ) オブジェクトは同じ関数の前回の呼び出しにおける i の値を覚えていますので、終端の i だけを指定すれば文字の範囲を示すことができます。

Note 1: the styler (Accessor) object remembers the i parameter in the previous calls to styler.ColourTo, so the single i parameter suffices to indicate a range of characters.

注意2: styler.ColourTo(i,state) の呼び出しによる別の効果は、Scintilla が後続の呼び出しで initStyle を正しく設定できることです。これは範囲内の色づけ状態をすべて記憶できることによります。

Note 2: As a side effect of calling styler.ColourTo(i,state), the coloring states of all characters in the range are remembered so that Scintilla may set the initStyle parameter correctly on future calls to the lexer.

解析器の構成Lexer organization

解析器の構成としては、少なくとも二通りを考えることができます。既存の解析器は「文字ベース」の考え方を使っています。次の例で外側のループは文字単位の繰り返しを行っています。

There are at least two ways to organize the code of each lexer. Present lexers use what might be called a "character-based" approach: the outer loop iterates over characters, like this:


  lengthDoc = startPos + length ;
  for (unsigned int i = startPos; i < lengthDoc; i++) {
    chNext = styler.SafeGetCharAt(i + 1);
    // 特別な場合を処理する << handle special cases >>
    switch(state) {
      // ch と chNext についてだけ行う。
	  // 状態が変わったときは styler.ColorTo(i,state) を呼び出す。 
      // Handlers examine only ch and chNext.
      // Handlers call styler.ColorTo(i,state) if the state changes.
      case state_1: // state 1 を処理 << handle ch in state 1 >>
      case state_2: // state 2 を処理 << handle ch in state 2 >>
      ...
      case state_n: // state n を処理 << handle ch in state n >>
    }
    chPrev = ch;
  }
  styler.ColourTo(lengthDoc - 1, state);

もう一つの方法は「状態ベース」の考え方を使います。外側のループは状態ごとの繰り返しとなっています。

An alternative would be to use a "state-based" approach. The outer loop would iterate over states, like this:


  lengthDoc = startPos+lenth ;
  for ( unsigned int i = startPos ;; ) {
    char ch = styler.SafeGetCharAt(i);
    int new_state = 0 ;
    switch ( state ) {
      // 判定コードが次の状態を検出していたら new_state が設定されている 。
      // scanners set new_state if they set the next state.
      case state_1: /* state 1 の終わりを探す << scan to the end of state 1 >> */ break ;
      case state_2: /* state 2 の終わりを探す << scan to the end of state 2 >> */ break ;
      case default_state:
        // 次の基本状態ではない部分を探して new_state を設定する。
        << scan to the next non-default state and set new_state >>
    }
    styler.ColourTo(i, state);
    if ( i >= lengthDoc ) break ;
    if ( ! new_state ) {
      ch = styler.SafeGetCharAt(i);
      // 基本状態にある ch に基づいた設定を行う。
      << set state based on ch in the default state >>
    }
  }
  styler.ColourTo(lengthDoc - 1, state);

こちらの方が自然に見えるかもしれません。状態検出は文字の検出よりもやることが少なく単純になります。例えば C 言語の注釈部分を処理している中で C 言語文字列の開始部分であるかどうかを調べる必要がありません。またこの方法によけちば複数の検出部が共用できる、例えば scanToEndOfLine といったものを自然な形で定義できます。

This approach might seem to be more natural. State scanners are simpler than character scanners because less needs to be done. For example, there is no need to test for the start of a C string inside the scanner for a C comment. Also this way makes it natural to define routines that could be used by more than one scanner; for example, a scanToEndOfLine routine.

一方で、状態ベースの考え方によるメインループでは、状態検出器のそれぞれが特殊な形を扱わなくてはならないでしょう。従って、どちらにも長所があります。特殊な形というのは次のものです。

(訳注:第一文はおそらく原文の character-based がまちがっている。)

However, the special cases handled in the main loop in the character-based approach would have to be handled by each state scanner, so both approaches have advantages. These special cases are discussed below.

特殊ケース: DBCS の先行オクテットSpecial case: Lead characters

先行オクテット(lead byte)とは日本語のような言語の DBCS 処理の一部です。Shift_JIS のようなエンコーディングは 16 ビットに拡張された文字が先行オクテットと後続オクテット(trail byte)に分けてコード化されます。

Lead bytes are part of DBCS processing for languages such as Japanese using an encoding such as Shift-JIS. In these encodings, extended (16-bit) characters are encoded as a lead byte followed by a trail byte.

先行オクテットは解析上意味を持つことは滅多になく、通常は文字列か注釈にのみ現れます。これらの処理の中では styler.IsLeadByte(ch) が FALSE を返したときに ch を無視すべきです。

(訳注: 第二文、これも原文が逆じゃないかと思ったわけだが。)

Lead bytes are rarely of any lexical significance, normally only being allowed within strings and comments. In such contexts, lexers should ignore ch if styler.IsLeadByte(ch) returns TRUE.

注意: UTF-8 は Shift_JIS よりも簡単な構造なので特殊な扱いは必要ありません。UTF-8 の拡張文字はすべて 128 以上のコードであり、プログラミング言語の文法解析で意味を持つことはありません。現在の所、プログラミング言語の演算子や注釈を示す符号などはすべて ASCII 文字で書かれているからです。

Note: UTF-8 is simpler than Shift-JIS, so no special handling is applied for it. All UTF-8 extended characters are >= 128 and none are lexically significant in programming languages which, so far, use only characters in ASCII for operators, comment markers, etc.

(訳注/個人的な意見: Scintilla の解説という観点からは少々脱線した話になるが、母語による識別子を使える人が一部(英語が母語の人)に限られるのはアンフェアではないか?という点を「英語を母語としない人」ならば一度はよく考えてみてほしい。)

特殊ケース: 折りたたみSpecial case: Folding

折りたたみは解析器の関数内で処理することができます。やっかいな相互作用を避ける意味で、文字装飾の処理と折りたたみの処理は分離して実装するほうがよいのです。折りたたみが有効であれば、折りたたみ関連の関数は解析関数よりも後で実行されます。この節では解析関数のなかでどのようにして折りたたみの実装を行うかを解説します。

Folding may be performed in the lexer function. It is better to use a separate folder function as that avoids some troublesome interaction between styling and folding. The folder function will be run after the lexer function if folding is enabled. The rest of this section explains how to perform folding within the lexer function.

初期化の中で折りたたみに対応する解析器は次の設定を行います。

During initialization, lexers that support folding set


    bool fold = styler.GetPropertyInt("fold");
        

エディタで折りたたみが有効になれば fold は TRUE となり、各行の終わりと終了の直前に解析器は次の呼び出しをすべき状態になります。

If folding is enabled in the editor, fold will be TRUE and the lexer should call:


    styler.SetLevel(line, level);
        

at the end of each line and just before exiting.

line は改行が見える単純な数です。初期値は styler.GetLine(startPos) で、改行が出てくるたびに styler.SetLevel 呼び出し毎に増えていきます。

The line parameter is simply the count of the number of newlines seen. It's initial value is styler.GetLine(startPos) and it is incremented (after calling styler.SetLevel) whenever a newline is seen.

level の下位 12 ビットには要求された字下げの深さが入っています。上位 4 ビットはフラグです。字下げの深さは言語に依存します。C++ 向けには、解析器が '{' を検出したときに増え、'}' を検出したときに減る値となります。もちろん文字列や注釈内の { や } は別の扱いにします。

The level parameter is the desired indentation level in the low 12 bits, along with flag bits in the upper four bits. The indentation level depends on the language. For C++, it is incremented when the lexer sees a '{' and decremented when the lexer sees a '}' (outside of strings and comments, of course).

Scintilla.h に定義されているフラグビットは flags 変数にセットあるいはクリアされています。
SC_FOLDLEVELWHITEFLAG は解析器がホワイトスペースしか含んでいない行だと判断したときにセットされます。
SC_FOLDLEVELHEADERFLAG は対象の行が折りたたみ基準点であることを示します。通常、これは次の行がより深い折りたたみ階層になることを意味しますが、解析器は別の基準で折りたたみを定義することもできます。例えば、関数定義の最後の行ではなく最初の行を折りたたみ基準点にすることができます。

The following flag bits, defined in Scintilla.h, may be set or cleared in the flags parameter. The SC_FOLDLEVELWHITEFLAG flag is set if the lexer considers that the line contains nothing but whitespace. The SC_FOLDLEVELHEADERFLAG flag indicates that the line is a fold point. This normally means that the next line has a greater level than present line. However, the lexer may have some other basis for determining a fold point. For example, a lexer might create a header line for the first line of a function definition rather than the last.

SC_FOLDLEVELNUMBERMASK マスクは level 変数の下位の 12 ビットにある深さの値の所在を表しています。フラグと折りたたみ深さの値を分離することに利用できます。

The SC_FOLDLEVELNUMBERMASK mask denotes the level number in the low 12 bits of the level param. This mask may be used to isolate either flags or level numbers.

C++ 解析器は改行が出てきたときに次のようなコードを使っています。

For example, the C++ lexer contains the following code when a newline is seen:


  if (fold) {
    int lev = levelPrev;
    // 空行の場合は「すべてホワイトスペース」を意味するビットを立てる。
    // Set the "all whitespace" bit if the line is blank.
    if (visChars == 0)
      lev |= SC_FOLDLEVELWHITEFLAG;

    // 必要なら「ヘッダ」ビットを立てる。
    // Set the "header" bit if needed.
    if ((levelCurrent > levelPrev) && (visChars > 0))
      lev |= SC_FOLDLEVELHEADERFLAG;
      styler.SetLevel(lineCurrent, lev);

    // 現在行についての折りたたみ変数を再初期化する
    // reinitialize the folding vars describing the present line.
    lineCurrent++;
    visChars = 0;  // その行のホワイトスペースではない文字の数
                   // Number of non-whitespace characters on the line.
    levelPrev = levelCurrent;
  }

C++ 解析器では終了直前に次のようなコードがあります。

The following code appears in the C++ lexer just before exit:


  // 現在のフラグで次の行の実際の折りたたみの深さ情報を満たす。
  // これは後から満たす代わりに行われる。
  // Fill in the real level of the next line, keeping the current flags
  // as they will be filled in later.
  if (fold) {
    // 深さ情報をマスクして落とし、以前のフラグのみを残す。
    // Mask off the level number, leaving only the previous flags.
    int flagsNext = styler.LevelAt(lineCurrent);
    flagsNext &= ~SC_FOLDLEVELNUMBERMASK;
    styler.SetLevel(lineCurrent, levelPrev | flagsNext);
  }
        

性能の心配はしなくてよいDon't worry about performance

解析器のコードを書くときに性能の考慮はしなくても構いません。画面の再描画コストは関数呼び出しなどに比べて何倍も大きいものです。その上、Scintilla は重要な最適化のすべてを行います。Scintilla は解析器が色づけのやり直しのみのために呼び出され、それだけが要求されることを保証します。他方 styler.ColourTo の余分な呼び出しを気に懸ける必要もありません。styler オブジェクトのバッファは ColourTo に呼び出され、画面の多重更新を防ぎます。

The writer of a lexer may safely ignore performance considerations: the cost of redrawing the screen is several orders of magnitude greater than the cost of function calls, etc. Moreover, Scintilla performs all the important optimizations; Scintilla ensures that a lexer will be called only to recolor text that actually needs to be recolored. Finally, it is not necessary to avoid extra calls to styler.ColourTo: the sytler object buffers calls to ColourTo to avoid multiple updates of the screen.

このページは Edward K. Ream さんにより寄贈されました。

Page contributed by