最近、こつこつと Chiffon というECMAScriptのパーサを書き進めています。
この Chiffon の主な解析方法は正規表現です。
というのも、前に書いたJavaScriptコードをトークンに分解する正規表現が発端で、これを改良し、es6 にも対応させようとしています。
ECMA-262 の仕様を調べながらパーサを書いて、今まであまり知らなかったことがでてきたり、
いろんな構文パターンをテストしていくうちに、正規表現のミスや修正が必要なものがでてきました。
ECMA-262 11.8.3 Numeric Literals をあらためて見ると表現が足りないことに気付きます。
DecimalLiteral はドットから始まることができるので、
また、0x の他に BinaryIntegerLiteral (0bXXX)、OctalIntegerLiteral (0oXXX) があります。
バックスラッシュでエスケープされたシングルクォート
ただし、この正規表現は不完全で、改行にもマッチしてしまいます。
改行にはマッチしないが、バックスラッシュでエスケープされた改行にはマッチしたい。
ただ、この正規表現にはまだ罠があります。
ECMA-262 11.3 Line Terminators では <CR><LF> の2文字で改行扱いされます。
<CR><LF> にも正しくマッチさせるには、単純に
※ECMA-262 11.3 Line Terminators は他にもありますが、この記事では簡略化しています。
これは大抵の場合、正しくマッチしますが、
ECMA-262 11.8.5 Regular Expression Literals では文字グループ
文字グループ内はバックスラッシュのエスケープなしでスラッシュ
RegularExpressionFlags も
ただし、ECMAScript には
このようなコードに対して誤ったマッチをしてしまいます。
もし JavaScript に否定後読み
ECMAScript の正規表現リテラルにマッチする正規表現は、少なくとも JavaScript の正規表現では100% 確実には無理なので、
Chiffon では正しいマッチかどうかバックトラックして判断しています。
テンプレートリテラルは、
ECMA-262 11.8.6 Template Literal Lexical Components にあるように、改行 (LineTerminatorSequence) が含まれます。
しばらくはこの正規表現で問題ないと思っていたのですが、
テンプレートリテラルの Expression 部分
その中にさらにテンプレートリテラルが書けるので、これを表現するのは正規表現の再帰が必要になり、無理ではないかと思っています。
そこで Chiffon では、有限ではあるものの、ある程度のネストを考慮して、
このパターンは、
ただし再帰パターンは JavaScript では書けないため、
ただ、通常ここまで深くなるテンプレートリテラルを書かないと思うので、そこまで問題視していません。
また、
今までせいぜい数えられるくらいしか出会ってないのと、使う機会もないものですが、
部分的に表すこともできるので、
これがなかなか遅いので、どうにかならないかとあれこれ試して、
いくらビルトインメソッドとはいえ exec はループで回すとなると扱いが難しいです。
なんとなく書き始めた Chiffon ですが、
ECMA-262 の仕様を知るいいきっかけになってくれています。
まだ tokenizer (字句解析) までの実装で厳密なパーサではないですが、日々進めていこうと思ってます。
この Chiffon の主な解析方法は正規表現です。
というのも、前に書いたJavaScriptコードをトークンに分解する正規表現が発端で、これを改良し、es6 にも対応させようとしています。
ECMA-262 の仕様を調べながらパーサを書いて、今まであまり知らなかったことがでてきたり、
いろんな構文パターンをテストしていくうちに、正規表現のミスや修正が必要なものがでてきました。
数値リテラル
数値リテラルにマッチする正規表現は/0(?:[xX][0-9a-fA-F]+|[0-7]+)|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|[1-9]\d*/g
ECMA-262 11.8.3 Numeric Literals をあらためて見ると表現が足りないことに気付きます。
DecimalLiteral はドットから始まることができるので、
.1 などにマッチする必要があります。
また、0x の他に BinaryIntegerLiteral (0bXXX)、OctalIntegerLiteral (0oXXX) があります。
/0(?:[xX][0-9a-fA-F]+|[oO][0-7]+|[bB][01]+)|(?:\d+(?:[.]\d*)?|[.]\d+)(?:[eE][+-]?\d+)?|[1-9]\d*|0[0-7]+/g
文字列リテラル
/'(?:\\.|[^'\\])*'/g
バックスラッシュでエスケープされたシングルクォート
\' に対して正しくマッチします。
ただし、この正規表現は不完全で、改行にもマッチしてしまいます。
改行にはマッチしないが、バックスラッシュでエスケープされた改行にはマッチしたい。
/'(?:\\[\s\S]|[^'\r\n\\])*'/g
'aaa\
bbb\
ccc'
ただ、この正規表現にはまだ罠があります。
ECMA-262 11.3 Line Terminators では <CR><LF> の2文字で改行扱いされます。
'aaa\
bbb\
ccc'
\<CR>\<LF> と言った具合に、<CR><LF> の各文字の前にバックスラッシュがあればマッチすることになります。
<CR><LF> にも正しくマッチさせるには、単純に
\\\r\n を追加して、
/'(?:\\\r\n|\\[\s\S]|[^'\r\n\\])*'/g
※ECMA-262 11.3 Line Terminators は他にもありますが、この記事では簡略化しています。
正規表現リテラル
正規表現リテラルは以前こう書いていました。/\/(?!\*)(?:\\.|[^\/\r\n\\])+\/(?:[gimy]{0,4}|\b)/g
これは大抵の場合、正しくマッチしますが、
ECMA-262 11.8.5 Regular Expression Literals では文字グループ
[...] 内に限りスラッシュ記号を直接書くことができます。
/[/]/
文字グループ内はバックスラッシュのエスケープなしでスラッシュ
/ を書けることを考慮し、
RegularExpressionFlags も
g, i, m, u, y に直します。
/\/(?![*\/])(?:\\[\s\S]|\[(?:\\[\s\S]|[^\]\r\n\\])*\]|[^\/\r\n\\])+\/(?:[gimuy]+\b|)/g
ただし、ECMAScript には
/ や /= といった演算子があり、
正規表現リテラルじゃないものにまでマッチする可能性があります。
var a = 1, b = 2, c = [1, 2], g = 2, i = 3;
a /= a / b / c[2/g] /iこのようなコードに対して誤ったマッチをしてしまいます。
もし JavaScript に否定後読み
(?<!...)、もしくは肯定後読み (?<=...) があったらいけそうな気がしますが、
おそらく解決できないでしょう。
if (1) /a/if (((a||b)&&c)||d) /a/.test('a') ? ... : ...if, while, for, with などが前にある場合、括弧を超えて判断する必要があるからです。
ECMAScript の正規表現リテラルにマッチする正規表現は、少なくとも JavaScript の正規表現では100% 確実には無理なので、
Chiffon では正しいマッチかどうかバックトラックして判断しています。
テンプレートリテラル
テンプレートリテラルの正規表現は、/`(?:\\[\s\S]|[^`\\])*`/g
` ... ` バックスラッシュのエスケープも考慮してマッチします。
refiddleデモ
テンプレートリテラルは、
ECMA-262 11.8.6 Template Literal Lexical Components にあるように、改行 (LineTerminatorSequence) が含まれます。
しばらくはこの正規表現で問題ないと思っていたのですが、
テンプレートリテラルの Expression 部分
${ ... } には、
その中にさらにテンプレートリテラルが書けるので、これを表現するのは正規表現の再帰が必要になり、無理ではないかと思っています。
そこで Chiffon では、有限ではあるものの、ある程度のネストを考慮して、
/`(?:\\[\s\S]|\$\{(?:\\[\s\S]|[^{}\\]|\{(?:[^{}]*(?:\{[^{}]*\})?)*\})*\}|[^`\\])*`/g
このパターンは、
${ から } までを別でマッチさせて、
ある程度の深さまでなら正しく扱えるようになっています。
ただし再帰パターンは JavaScript では書けないため、
` ${
      `${
           `${
                1 + 1
             }`
       }`
   }
`` ${
      `${
           `${
                `${
                     1 + 1
                 }`
             }`
       }`
   }
`
ただ、通常ここまで深くなるテンプレートリテラルを書かないと思うので、そこまで問題視していません。
<!--, --> でコメントになる
ECMA-262 B.1.3 HTML-like Comments をみると、<!-- や --> がコメント扱いになることがわかります。
詳しい仕様まではあまり一般的ではないように思いますが、
var a = 1, b = a<!--a;
console.log(b); // 1<!-- 以降がコメント扱いされているため、
var a = 1, b = a<!--a;b = a で終わってしまっています。
また、
--> に関しては、
<!-- から --> の間がコメントになるのではなく、
それ以降の行末までがコメントになります。
var a = 1;
--> a = 2;
console.log(a); // 1--> a = 2 がコメントになり、1 が console.log されます。
--> は、大雑把に言うと行頭にない場合コメントになりません。
var a = 1, b = a-->a;
console.log(b); // trueIdentifier に UnicodeEscapeSequence が使える
ECMA-262 11.6 Names and Keywords によると、 IdentifierName に UnicodeEscapeSequence が使えます。今までせいぜい数えられるくらいしか出会ってないのと、使う機会もないものですが、
\u0074\u0079\u0070\u0065\u006F\u0066 \u307B\u3052'undefined' と結果が返ると思います。
typeof ほげ部分的に表すこともできるので、
typeof ほ\u3052 でも同じです。
var \u307B\u3052 = 1;
console.log(ほげ); // 1RegExp.prototype.exec から string.match(regex) で高速化
Chiffon はもともと、RegExp#exec を使って、var m;
while ((m = re.exec(source)) != null) {
  // ...
}これがなかなか遅いので、どうにかならないかとあれこれ試して、
var m = source.match(re);g フラグをつけて、String#match を使ったら、約250% 速くなりました。
いくらビルトインメソッドとはいえ exec はループで回すとなると扱いが難しいです。
なんとなく書き始めた Chiffon ですが、
ECMA-262 の仕様を知るいいきっかけになってくれています。
まだ tokenizer (字句解析) までの実装で厳密なパーサではないですが、日々進めていこうと思ってます。
0 件のコメント:
コメントを投稿