Подтвердить что ты не робот

При анализе Javascript, что определяет значение косой черты?

Javascript имеет сложную грамматику для разбора. Прямоугольные косые черты могут означать несколько разных вещей: оператор разделения, литерал регулярных выражений, средство представления комментариев или средство представления комментария строки. Последние два легко отличить: если за чертой следит звезда, начинается многострочный комментарий. Если за косой чертой следует другая косая черта, это комментарий строки.

Но правила для устранения неоднозначности деления и регулярного выражения ускользают от меня. Я не могу найти его в стандарте ECMAScript. Там лексическая грамматика явно делится на две части: InputElementDiv и InputElementRegExp, в зависимости от того, что будет означать слэш. Но там ничего не объясняет, когда использовать, который.

И, конечно, правила вставки с запятнанной точкой с запятой усложняют все.

Есть ли у кого-нибудь пример четкого кода для лексинга Javascript, который имеет ответ?

4b9b3361

Ответ 1

На самом деле это довольно легко, но для этого требуется, чтобы ваш лексер был немного умнее обычного.

Оператор деления должен следовать выражению, а литерал регулярного выражения не может следовать за выражением, поэтому во всех остальных случаях можно смело предположить, что вы смотрите на литерал регулярных выражений.

Вы уже должны идентифицировать Punctuators как многосимвольные строки, если вы делаете это правильно. Итак, посмотрите на предыдущий токен и посмотрите, есть ли это:

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

Для большинства из них вы теперь знаете, что находитесь в контексте, где вы можете найти литерал регулярных выражений. Теперь, в случае ++ --, вам нужно будет выполнить дополнительную работу. Если ++ или -- является предварительным приращением/декрементом, то / после него запускает литерал регулярного выражения; если это пост-приращение/декремент, то / после него запускает DivPunctuator.

К счастью, вы можете определить, является ли это "пред-" оператором, проверив его предыдущий токен. Во-первых, post-increment/decment является ограниченным производством, поэтому, если ++ или -- предшествует linebreak, то вы знаете, что это "pre-". В противном случае, если предыдущий токен - это любая из вещей, которые могут предшествовать литералу регулярных выражений (yay рекурсия!), То вы знаете, что это "pre-". Во всех остальных случаях это "пост".

Конечно, пунктуатор ) не всегда указывает конец выражения - например if (something) /regex/.exec(x). Это сложно, потому что это требует некоторого смыслового понимания для распутывания.

К сожалению, это не совсем все. Есть некоторые операторы, которые не являются пунктуаторами и другими заметными ключевыми словами для загрузки. Литералы регулярных выражений также могут следовать этим. Это:

new delete void typeof instanceof in do return case throw else

Если имя IdentifierName, которое вы только что использовали, является одним из них, то вы смотрите на литерал регулярных выражений; в противном случае это DivPunctuator.

Вышеописанная информация основана на спецификации ECMAScript 5.1 (найденной здесь) и не содержит каких-либо расширений для данного языка для браузера. Но если вам нужно их поддерживать, тогда это должно быть легким руководством для определения того, в каком контексте вы находитесь.

Конечно, большая часть из них представляет собой очень глупые случаи для включения литерала регулярных выражений. Например, вы не можете предварительно вводить регулярное выражение, даже если оно синтаксически разрешено. Таким образом, большинство инструментов может ускользнуть от упрощения проверки контекста регулярного выражения для реальных приложений. Вероятно, достаточно JSLint-метода проверки предыдущего символа для (,=:[!&|?{};. Но если вы возьмете такой ярлык при разработке того, что должно быть инструментом для lexing JS, тогда вы должны обязательно отметить это.

Ответ 2

Кажется, что JSLint ожидает регулярное выражение, если предыдущий токен является одним из

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

Rhino всегда возвращает маркер DIV из лексера.

Ответ 3

В настоящее время я разрабатываю JavaScript/ECMAScript 5.1 parser с помощью JavaCC. RegularExpressionLiteral и Автоматическая вставка с запятой - это две вещи, которые делают я сумасшедший в грамматике ECMAScript. Этот вопрос и ответы были неоценимы для вопроса о регулярном выражении. В этом ответе я бы хотел собрать свои собственные выводы.

TL; DR В JavaCC используйте лексические состояния и переключить их из анализатора.


Очень важно то, что написал Thom Blake:

Оператор деления должен следовать выражению, а регулярный Литерал выражения не может следовать выражению, поэтому во всех остальных случаях вы можете смело предположить, что вы смотрите на литерал регулярных выражений.

Итак, вам действительно нужно понять, было ли это выражение или нет раньше. Это тривиально в парсере, но очень сложно в лексере.

Как отмечал Thom , во многих (но, к сожалению, не во всех) случаях вы можете понять, было ли это выражение, "глядя" на последний токен. Вы должны учитывать преципиенты, а также ключевые слова.

Начните с ключевых слов. Следующие ключевые слова не могут предшествовать DivPunctuator (например, вы не можете иметь case /5), поэтому, если после этого вы увидите /, у вас есть RegularExpressionLiteral:

case
delete
do
else
in
instanceof
new
return
throw
typeof
void

Далее, пунктуаторы. Следующие пунктуаторы не могут предшествовать DivPunctuator (например, в { /a... символ / никогда не может начать деление):

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

Итак, если у вас есть один из них и после этого < <29 > , это никогда не будет DivPunctuator и поэтому должно быть RegularExpressionLiteral.

Далее, если у вас есть:

/

И /... после этого также должен быть RegularExpressionLiteral. Если между этими косыми чертами не было пробела (т.е. // ...), это должно было быть обработано как SingleLineComment ( "максимальный munch" ).

Далее, следующий пунктуатор может только закончить выражение:

]

Итак, следующее / должно начинаться с DivPunctuator.

Теперь мы имеем следующие оставшиеся случаи, которые, к сожалению, неоднозначны:

}
)
++
--

Для } и ) вам нужно знать, заканчивают ли они выражение или нет, для ++ и -- - они завершают PostfixExpression или запустите UnaryExpression.

И я пришел к выводу, что очень сложно (если не невозможно) узнать в лексере. Чтобы дать вам представление об этом, несколько примеров.

В этом примере:

{}/a/g

/a/g является RegularExpressionLiteral, но в этом:

+{}/a/g

/a/g является делением.

В случае ) вы можете иметь деление:

('a')/a/g

а также RegularExpressionLiteral:

if ('a')/a/g

Итак, к сожалению, похоже, что вы не можете решить проблему с помощью lexer. Или вам придется вложить столько грамматики в лексер, чтобы больше не было lexer.

Это проблема.


Теперь, возможное решение, которое в моем случае основано на JavaCC.

Я не уверен, что у вас есть аналогичные функции в других генераторах парсеров, но JavaCC имеет лексические состояния, которые могут использоваться для переключения между "мы ожидаем a DivPunctuator" и "ожидаем состояния a RegularExpressionLiteral". Например, в эта грамматика состояние NOREGEXP означает "мы не ожидаем здесь RegularExpressionLiteral".

Это решает часть проблемы, но не двусмысленные ), }, ++ и --.

Для этого вам нужно будет переключить лексические состояния из синтаксического анализатора. Это возможно, см. Следующий вопрос в Часто задаваемые вопросы JavaCC:

Может ли синтаксический анализатор принудительно переключиться на новое лексическое состояние?

Да, но создавать ошибки можно очень просто.

Анализатор взглядов, возможно, уже зашел слишком далеко в поток токенов (т.е. уже прочитал / как DIV или наоборот).

К счастью, похоже, что способ переключения лексических состояний немного безопаснее:

Есть ли способ сделать SwitchTo более безопасным?

Идея состоит в том, чтобы создать "резервную" токенную ленту и снова нажать токены, читаемые во время просмотра.

Я думаю, что это должно работать для }, ), ++, --, поскольку они обычно находятся в ситуациях LOOKAHEAD (1), но я не уверен в этом на 100%. В худшем случае лексер, возможно, уже попытался разобрать маркер / -starting как RegularExpressionLiteral и не смог, поскольку он не был завершен другим /.

В любом случае, я не вижу лучшего способа сделать это. Следующим хорошим было бы, вероятно, вообще отказаться от дела (например, JSLint и многие другие), документировать и просто не разбирать эти типы выражений. {}/a/g не имеет никакого смысла.

Ответ 4

Вы можете знать только, как интерпретировать /, также используя синтаксический синтаксический анализатор. Каким бы ни был путь lex к действительному синтаксическому анализу, он определяет, как интерпретировать символ. По-видимому, это то, что они считали фиксацией, но этого не делал. Подробнее читайте здесь: http://www-archive.mozilla.org/js/language/js20-2002-04/rationale/syntax.html#regular-expressions

Ответ 5

См. раздел 7:

Для лексической грамматики есть два символа цели. Символ InputElementDiv используется в тех синтаксических контекстах грамматики, где разрешен оператор ведущего деления (/) или разделения (/=). Символ InputElementRegExp используется в других контекстах синтаксической грамматики.

ПРИМЕЧАНИЕ. Нет синтаксических контекстов грамматики, где допускаются как ведущее деление, так и разделение, и ведущий RegularExpressionLiteral. Это не влияет на вставку с запятой (см. 7.9); в таких примерах, как следующее:

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

где первый символ без пробелов, без комментария после строки LineTerminator является косой чертой (/), а синтаксический контекст позволяет деление или назначение разделения, точка с запятой не вставлена ​​в LineTerminator. То есть приведенный выше пример интерпретируется в так же, как:

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

Я согласен, это запутывает и должно быть одно выражение грамматики верхнего уровня, а не два.


изменить:

Но ничего не объясняет, когда использовать.

Возможно, простой ответ смотрит нам в лицо: попробуйте один, а затем попробуйте другой. Поскольку они не разрешены, не более чем один даст ошибку без ошибок.