PEG.jsの構文規則の書き方


PEG.jsの構文規則の書き方

PEG.js公式ドキュメントの翻訳と要約です.

PEG.jsとは

Parser Generatorと呼ばれるもので,コンパイラ生成器(コンパイラのコンパイラ)です.

構文規則ファイル(.pegjs)を入力として与えると,コンパイラが出てきます.

今回は使い方は紹介しません.構文規則ファイルの書き方のみ翻訳していきます.

構文例1

JavaScriptに似ています.以下の構文は2*(3+4)のような計算式を計算するための構文規則です.

start
  = additive

additive
  = left:multiplicative "+" right:additive { return left + right; }
  / multiplicative

multiplicative
  = left:primary "*" right:multiplicative { return left * right; }
  / primary

primary
  = integer
  / "(" additive:additive ")" { return additive; }

integer "integer"
  = digits:[0-9]+ { return parseInt(digits.join(""), 10); }
  • /...//*...*/はコメントとして使えます
  • rule:構文規則の固まりのことをそれぞれruleと呼びます(今回の例では5つのruleがあります)
  • name:ruleにはJavaScriptの変数名のような名前をつけ,重複してはいけません.(例:integer
  • parsing expression:構文式は入力されたテキストに対する実際の規則を書いている部分です(例:digits:[0-9]+ { return parseInt(digits.join(""), 10); }
  • human-readable name:エラーメッセージで使用される別名をつけることもできます.JavaScriptのStringのように書きます(例:"integer"
  • 構文規則の入口のruleには必ずstartという名前をつけてください
  • ruleは空白,空行によって分けてください.セミコロン(;)で分けることも可能です
  • parser action:ruleの中にあらわれる{}で囲まれた部分です.マッチした結果をどのように変換するかを設定します.例だと,parseIntを使ってNumber型に変換しています(例:{ return parseInt(digits.join(""), 10); }

構文例2(initializer)

以下の例ようにinitializerを使用することもできます.最初の{}で囲まれたJavaScriptがinitializerです.

{
  function makeInteger(o) {
    return parseInt(o.join(""), 10);
  }
}

start
  = additive

additive
  = left:multiplicative "+" right:additive { return left + right; }
  / multiplicative

multiplicative
  = left:primary "*" right:multiplicative { return left * right; }
  / primary

primary
  = integer
  / "(" additive:additive ")" { return additive; }

integer "integer"
  = digits:[0-9]+ { return makeInteger(digits); }
  • initializerは構文解析器が作られる前に実行されてから,構文解析が実行されます.
  • initializerで定義されたすべての変数,関数がruleの中で使用できます.
  • initializerでは特別な変数optionsにもアクセスすることができます.

構文規則の書き方

  • "literal",'literal'

    String型の定数です.literalの後にiをつけると大文字と小文字を区別しなくなります.

  • .(ドット)

    任意の1文字です.

  • [characters]

    文字列の集合charactersの中の任意の1文字です.例えば,
    [a-z]なら任意の小文字1文字という意味になります.

  • rule

    ruleを再帰的に適用します.

  • ( expression )

    部分式を適用します.

  • expression *

    expressionの0以上の繰り返しです.

  • expression +

    expressionの1以上の繰り返しです.

  • expression ?

    expressionが0または1個です.

  • & expression

    expressionに適合した場合,undefinedを返します.(構文解析失敗)

  • ! expression

    expressionに適合しなかった場合,undefinedを返します.

  • & { predicate }

    predicateはJavaScriptコードで,関数のように実行されます.

    labeled expression
    を変数として内部で使うことができます.

    内部ではreturnを使って値を返します.

    返された値がtrueだった場合に,このexpressionの値はundefinedになります.

    predicateは中でinitializerで定義された変数や関数も使用できます.

    また,location informationもlocation関数を使って参照できます.

    location関数は以下のような値を返します.

    {
        start: { offset: 23, line: 5, column: 6 },
        end:   { offset: 23, line: 5, column: 6 }
    }
    

    startendは構文解析中の位置を表します.offsetは0始まりで,linecolumnは1始まりです.

    また,options変数も参照できます.

  • ! { predicate }

    上とは逆で,返した値がfalseならundefinedになります.

  • \$ expression

    expressionに適合したら,適合した文字列を返します.

  • label : expression

    expressionにラベルをつけます.ラベルをつけたexpressionはpredicateなどで使用することができます.

  • expression1 expression2 ... expression n

    配列をそれぞれexpressionで評価し,それぞれの返り値の配列を返します.

  • expression { action }

    expressionに適合したらactionが実行されて,その返り値を返します.actionの書き方はpredicateと同じです.

  • expression1 / expression2 / ... / expression n

    最初のexpression1から順番に評価していき,適合したらその値を返し,失敗したら次のexpressionを評価します.

おまけ(簡易Markdownの構文規則)

HeadingとParagraphだけのSimpleMarkdownのParserをPEG.jsで作ってみました.出力はhtmlです.これから改良していこうと思います.

SimpleMarkdown.pegjs
start 
    = SimpleMarkdown

SimpleMarkdown
    = b:Blocks {return "<html>\n<body>\n"+b+"</body>\n</html>\n"}

Blocks
    = b:Block bs:Blocks {return b + bs}
    / $ ""

Block
    = e:Element d:Devider {return e + d}

Element 
    = "###### " t:Heading6 {return t}
    / "##### " t:Heading5 {return t}
    / "#### " t:Heading4 {return t}
    / "### " t:Heading3 {return t}
    / "## " t:Heading2 {return t} 
    / "# " t:Heading1 {return t}
    / Paragraph

Heading1
    = t:InnerText {return "<h1>"+t+"</h1>"}

Heading2
    = t:InnerText {return "<h2>"+t+"</h2>"}

Heading3
    = t:InnerText {return "<h3>"+t+"</h3>"}

Heading4
    = t:InnerText {return "<h4>"+t+"</h4>"}

Heading5
    = t:InnerText {return "<h5>"+t+"</h5>"}

Heading6
    = t:InnerText {return "<h6>"+t+"</h6>"}

Paragraph
    = t:InnerText {return "<p>"+t+"</p>"}

InnerText
    = t1:[^(' '\t\r\n)] t2:[^\n]* [\n] t3:InnerText {return t1+t2.join("")+t3}
    / t1:[^(' '\t\r\n)] t2:[^\n]* {return t1 + t2.join("")} 

Devider
    = ( EOF
    / [\n] _ EOF
    / [\n] _ [\n] [' '\t\r\n]* ) {return "\n"}

_ "whitespace"
    = [' '\t\r]*

EOF "EOF"
    = !.

input.md
# 転生したらカニだった件 ~おれのカニみそが超絶美味すぎて人生無双~

## あらすじ

トラックにひかれた田中は,目が覚めると異世界のカニになっていた.
めちゃくちゃ美味なカニみそを駆使し,異世界を牛耳ることを考える田中.
するとそこに,カニの天敵タコが現れ...?!

## 価格

50円です.

### 税込み価格

55円です.(軽減税率適用外)

## 感想

最初はふざけてる話かなと思ったけど,
最後はとっても感動するのかなと思ったけど,
やっぱりふざけていた!(40代女性)

読めば読むほど意味が分からない話.
古本屋でも買い取ってくれない.(30代男性)

output.html
<html>
<body>
<h1>転生したらカニだった件 ~おれのカニみそが超絶美味すぎて人生無双~</h1>
<h2>あらすじ</h2>
<p>トラックにひかれた田中は,目が覚めると異世界のカニになっていた.めちゃくちゃ美味なカニみそを駆使し,異世界を牛耳ることを考える田中.するとそこに,カニの天敵タコが現れ...?!</p>
<h2>価格</h2>
<p>50円です.</p>
<h3>税込み価格</h3>
<p>55円です.(軽減税率適用外)</p>
<h2>感想</h2>
<p>最初はふざけてる話かなと思ったけど,最後はとっても感動するのかなと思ったけど,やっぱりふざけていた!(40代女性)</p>
<p>読めば読むほど意味が分からない話.古本屋でも買い取ってくれない.(30代男性)</p>
</body>
</html>