33

Javascript には、解析するのにトリッキーな文法があります。スラッシュは、除算演算子、正規表現リテラル、コメント イントロデューサー、または行コメント イントロデューサーなど、さまざまなことを意味します。最後の 2 つは簡単に区別できます。スラッシュの後に星印が続くと、複数行のコメントが始まります。スラッシュの後に別のスラッシュが続く場合、それは行コメントです。

しかし、除算と正規表現リテラルのあいまいさを解消するためのルールは私を逃れています。ECMAScript 標準では見つかりません。そこでは、字句文法は、スラッシュの意味に応じて、InputElementDiv と InputElementRegExp の 2 つの部分に明示的に分割されます。しかし、いつどれを使用するかについては何も説明されていません。

そしてもちろん、恐ろしいセミコロン挿入規則がすべてを複雑にしています。

答えのあるJavascriptを字句解析するための明確なコードの例はありますか?

4

5 に答える 5

20

実際にはかなり簡単ですが、レクサーを通常より少し賢くする必要があります。

除算演算子は式の後に続く必要があり、正規表現リテラルは式の後に続くことはできません。したがって、他のすべてのケースでは、正規表現リテラルを見ていると安全に想定できます。

正しく行っている場合は、句読点を複数文字の文字列として識別する必要があります。前のトークンを見て、次のいずれかであるかどうかを確認します。

. ( , { } [ ; , < > <= >= == != === !== + - * % ++ --
<< >> >>> & | ^ ! ~ && || ? : = += -= *= %= <<= >>= >>>=
&= |= ^= / /=

これらのほとんどについて、正規表現リテラルを見つけることができるコンテキストにいることがわかりました。の場合、++ --追加の作業を行う必要があります。++or--がプリインクリメント/デクリメントの場合、次/の it は正規表現リテラルを開始します。ポストインクリメント/デクリメントの場合/は、DivPunctuator を開始します。

幸いなことに、前のトークンを確認することで、それが「前」演算子であるかどうかを判断できます。まず、post-increment/decrement は制限された生成であるため、++or--の前に改行がある場合、それが「pre-」であることがわかります。それ以外の場合、前のトークンが正規表現リテラルの前に置くことができるもののいずれかである場合 (イェーイ再帰!)、それが「前」であることがわかります。それ以外の場合はすべて「post-」です。

もちろん、)句読点は必ずしも式の終わりを示すわけではありません。たとえば、if (something) /regex/.exec(x). もつれを解くには意味を理解する必要があるため、これは注意が必要です。

残念ながら、それだけではありません。句読点ではない演算子がいくつかあり、その他の注目すべきキーワードがあります。正規表現リテラルもこれらに続くことができます。彼らです:

new delete void typeof instanceof in do return case throw else

消費したばかりの IdentifierName がこれらのいずれかである場合、正規表現リテラルを見ています。それ以外の場合は、DivPunctuator です。

上記は ECMAScript 5.1 仕様 (ここにあるように) に基づいており、言語に対するブラウザー固有の拡張機能は含まれていません。しかし、それらをサポートする必要がある場合、これは、あなたがどのような状況にあるかを判断するための簡単なガイドラインを提供するはずです.

もちろん、上記のほとんどは、正規表現リテラルを含めるための非常にばかげたケースを表しています。たとえば、構文上は許可されていても、実際には正規表現を事前にインクリメントすることはできません。そのため、ほとんどのツールは、実際のアプリケーションの正規表現コンテキスト チェックを簡素化することで解決できます。先行する文字をチェックする JSLint の方法(,=:[!&|?{};はおそらく十分です。しかし、JS を字句解析するためのツールであるはずのものを開発するときにそのようなショートカットを使用する場合は、その点に注意する必要があります。

于 2012-08-01T19:31:59.063 に答える
14

現在、JavaCC を使用してJavaScript/ECMAScript 5.1 パーサーを開発しています。RegularExpressionLiteral自動セミコロン挿入は、ECMAScript 文法で私を夢中にさせる 2 つのことです。この質問と回答は、正規表現の質問にとって非常に貴重でした。この回答では、私自身の調査結果をまとめたいと思います。

TL;DR JavaCC では、字句状態を使用し、パーサーから切り替えます


非常に重要なのは、トム・ブレイクが書いたことです。

除算演算子は式の後に続く必要があり、正規表現リテラルは式の後に続くことはできません。したがって、他のすべての場合では、正規表現リテラルを見ていると安全に想定できます。

したがって、それが式であったかどうかを実際に理解する必要があります。これはパーサーでは些細なことですが、レクサーでは非常に困難です。

Thomが指摘したように、多くの場合 (残念ながら、すべてではありません)、最後のトークンを「見る」ことによって、それが式であったかどうかを理解できます。句読点とキーワードを考慮する必要があります。

キーワードから始めましょう。次のキーワードを a の前にDivPunctuator置くことはできません (たとえば、を使用することはできませんcase /5)。したがって、/これらの後に a が表示される場合はRegularExpressionLiteral

case
delete
do
else
in
instanceof
new
return
throw
typeof
void

次に句読点。次の句読点は a の前に置くことはできませんDivPunctuator(例 :{ /a...記号内では/除算を開始することはできません):

{       (       [   
.   ;   ,   <   >   <=
>=  ==  !=  === !== 
+   -   *   %       
<<  >>  >>> &   |   ^
!   ~   &&  ||  ?   :
=   +=  -=  *=  %=  <<=
>>= >>>=    &=  |=  ^=
    /=

したがって、これらのいずれかを持っていて、/...この後を見ると、これは になることはDivPunctuatorありません。したがって、 でなければなりませんRegularExpressionLiteral

次に、次の場合:

/

/...その後、それも . である必要がありますRegularExpressionLiteral。これらのスラッシュの間にスペースがなかった場合 (つまり // ...)、これはSingleLineComment(「最大マンチ」) として処理されたに違いありません。

次に、次の句読点は式を終了するだけです。

]

したがって、次/DivPunctuator.

残念ながら、あいまいな次のケースが残っています。

}
)
++
--

}とが式を終了するか)どうかを知る必要があります。++--PostfixExpressionUnaryExpression

そして、字句解析器で見つけるのは非常に難しい (不可能ではないにしても) という結論に達しました。その感覚をつかむために、いくつかの例を示します。

この例では:

{}/a/g

/a/gですがRegularExpressionLiteral、これでは:

+{}/a/g

/a/g区分です。

)あなたが部門を持つことができる場合:

('a')/a/g

だけでなくRegularExpressionLiteral:

if ('a')/a/g

残念ながら、レクサーだけでは解決できないようです。または、レクサーに非常に多くの文法を導入する必要があるため、もはやレクサーではありません。

これは問題です。


さて、考えられる解決策は、私の場合は JavaCC ベースです。

他のパーサージェネレーターに同様の機能があるかどうかはわかりませんが、JavaCC には字句状態DivPunctuator機能があり、これを使用して「期待する」状態と「期待する」状態を切り替えることができますRegularExpressionLiteral。たとえば、この文法では、NOREGEXP状態は「ここに期待しない」ことを意味しRegularExpressionLiteralます。

これにより、問題の一部は解決されますが、あいまいな)}++およびは解決されません--

このためには、パーサーから字句状態を切り替えることができる必要があります。これは可能です。JavaCC FAQの次の質問を参照してください。

パーサーは強制的に新しい字句状態に切り替えることができますか?

はい。ただし、そうすると非常に簡単にバグが作成されます。

先読みパーサーは、トークン ストリーム内で既に行き過ぎている可能性があります (つまり、既に a として読み取ら/れているか、DIVまたはその逆)。

幸いなことに、字句状態の切り替えを少し安全にする方法があるようです:

SwitchTo をより安全にする方法はありますか?

アイデアは、「バックアップ」トークン ストリームを作成し、ルックアヘッド中に読み取られたトークンを再度プッシュすることです。

通常、LOOKAHEAD(1) の状況で見られるため}、これは , ),++で機能すると思いますが、100% 確実ではありません。--最悪の場合、レクサーはすでに/-starting トークンを として解析しようとしていRegularExpressionLiteralて、別の によって終了されなかったため失敗した可能性があります/

いずれにせよ、それを行うより良い方法はありません。次の良いことは、おそらくケースを完全に削除し (JSLint他の多くのケースと同様に)、文書化して、これらのタイプの式を解析しないことです。{}/a/gとにかくあまり意味がありません。

于 2014-11-25T06:27:41.017 に答える
5

前のトークンが次のいずれかである場合、JSLint は正規表現を期待しているように見えます

(,=:[!&|?{};

Rhino は常にレクサーから DIV (スラッシュ) トークンを返します。

于 2011-04-04T08:44:09.923 に答える
4

/ を解釈する方法を知るには、構文パーサーも実装する必要があります。どの lex パスが有効な解析に到達しても、文字の解釈方法が決まります。どうやら、これは彼らが修正を検討していたものの、修正しなかったものです。詳細はこちら: http://www-archive.mozilla.org/js/language/js20-2002-04/rationale/syntax.html#regular-expressions

于 2011-04-01T23:24:25.787 に答える
3

セクション 7 を参照してください。

語彙文法には 2 つの目標記号があります。InputElementDiv 記号は、先頭の除算 (/) または除算代入 (/=) 演算子が許可されている構文文法コンテキストで使用されます。InputElementRegExp 記号は、他の構文文法コンテキストで使用されます。

注意 先頭の部または部代入と先頭の正規表現リテラルの両方が許可される構文文法コンテキストはありません。これは、セミコロンの挿入の影響を受けません (7.9 を参照)。次のような例では:

a = b 
/hi/g.exec(c).map(d); 

LineTerminator の後の最初の非空白文字、非コメント文字がスラッシュ (/) であり、構文コンテキストで分割または分割代入が許可されている場合、LineTerminator にセミコロンは挿入されません。つまり、上記の例は次のように解釈されます。

a = b / hi / g.exec(c).map(d); 

私は同意します。紛らわしいので、トップレベルの文法表現は 2 つではなく 1 つにする必要があります。


編集:

しかし、いつどれを使用するかについては何も説明されていません。

おそらく、単純な答えは、私たちをじっと見つめていることです。1 つ試してから、もう 1 つ試してみてください。どちらも許可されていないため、多くてもエラーのない一致が得られるのは 1 つだけです。

于 2011-04-01T22:46:16.360 に答える