33

Javascript 有一个难以解析的语法。正斜杠可以表示许多不同的东西:除法运算符、正则表达式文字、注释介绍器或行注释介绍器。最后两个很容易区分:如果斜线后面跟着一个星号,它开始一个多行注释。如果斜线后面跟着另一个斜线,则它是行注释。

但是消歧除法和正则表达式文字的规则让我无法理解。我在ECMAScript 标准中找不到它。在那里,词法语法被明确分为两部分,InputElementDiv 和 InputElementRegExp,这取决于斜杠的含义。但是没有什么可以解释什么时候使用哪个。

当然,可怕的分号插入规则使一切变得复杂。

有没有人有一个明确的代码来解释 Javascript 的例子?

4

5 回答 5

20

这实际上相当容易,但它需要让你的词法分析器比平时更聪明一点。

除法运算符必须跟在表达式后面,而正则表达式文字不能跟在表达式后面,因此在所有其他情况下,您可以放心地假设您正在查看正则表达式文字。

如果您做得对,您已经必须将标点符号识别为多字符串。所以看看前面的token,看看是不是这些:

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

对于其中的大多数,您现在知道您处于可以找到正则表达式文字的上下文中。现在,在 的情况下++ --,您需要做一些额外的工作。如果++or--是一个前置增量/减量,那么/接下来它开始一个正则表达式文字;如果它是一个后增量/减量,那么/接下来它会启动一个 DivPunctuator。

幸运的是,您可以通过检查其先前的标记来确定它是否是“pre-”运算符。首先,后增量/减量是一种受限制的产生式,所以如果++or--之前有一个换行符,那么你就知道它是“pre-”。否则,如果前一个标记是任何可以在正则表达式文字之前的东西(耶递归!),那么你知道它是“前”。在所有其他情况下,它是“后”。

当然,)标点符号并不总是表示表达式的结尾——例如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 语法中疯狂的两件事。这个问题和答案对于正则表达式问题非常宝贵。在这个答案中,我想把我自己的发现放在一起。

TL;DR在 JavaCC 中,使用词法状态从解析器切换它们


非常重要的是汤姆布莱克写道:

除法运算符必须跟在表达式后面,而正则表达式文字不能跟在表达式后面,因此在所有其他情况下,您可以放心地假设您正在查看正则表达式文字。

所以你实际上需要了解它是否是一个表达式before。这在解析器中是微不足道的,但在词法分析器中却非常困难。

正如 Thom指出的那样,在许多(但不幸的是,并非全部)情况下,您可以通过“查看”最后一个标记来理解它是否是一个表达式。您必须考虑标点符号和关键字。

让我们从关键字开始。以下关键字不能在 a 之前DivPunctuator(例如,您不能有case /5),因此如果您/在这些关键字之后看到 a,则您有一个RegularExpressionLiteral

case
delete
do
else
in
instanceof
new
return
throw
typeof
void

接下来是标点符号。以下标点符号不能在 a 之前DivPunctuator(例如, { /a...在符号/中永远不能开始除法):

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

因此,如果您拥有其中一个并/...在此之后查看,则 this 永远不可能是 a DivPunctuator,因此必须是 a RegularExpressionLiteral

接下来,如果您有:

/

/...之后它也必须是一个RegularExpressionLiteral. 如果这些斜线(即 )之间没有空格 // ...,则必须将其作为SingleLineComment(“最大咀嚼”)处理。

接下来,以下标点符号只能结束一个表达式:

]

所以下面/必须开始一个DivPunctuator.

现在我们有以下剩余的情况,不幸的是,这些情况模棱两可:

}
)
++
--

对于}and)你必须知道他们是否结束一个表达式,因为++and --- 他们结束一个PostfixExpression或开始一个UnaryExpression.

我得出的结论是,很难(如果不是不可能的话)在词法分析器中找到。为了让您了解这一点,举几个例子。

在这个例子中:

{}/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中的以下问题:

解析器可以强制切换到新的词法状态吗?

是的,但是这样做很容易产生错误。

前瞻解析器可能已经在令牌流中走得太远(即已经读/作 aDIV或反之亦然)。

幸运的是,似乎有一种方法可以让切换词法状态更安全:

有没有办法让 SwitchTo 更安全?

这个想法是制作一个“备份”令牌流并再次推送在前瞻期间读取的令牌。

我认为这应该适用于}, ), ++--因为它们通常出现在 LOOKAHEAD(1) 情况下,但我不能 100% 确定这一点。在最坏的情况下,词法分析器可能已经尝试将/-starting 标记解析为 aRegularExpressionLiteral并失败,因为它没有被另一个 终止/

无论如何,我认为没有更好的方法可以做到这一点。下一件好事可能是完全放弃案例(就像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 节:

词汇语法有两个目标符号。InputElementDiv 符号用于允许使用前导除法 (/) 或除法赋值 (/=) 运算符的句法文法上下文中。InputElementRegExp 符号用于其他句法文法上下文。

注意没有句法语法上下文允许前导除法或除法分配以及前导RegularExpressionLiteral。这不受分号插入的影响(见 7.9);在以下示例中:

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

LineTerminator 后面的第一个非空白、非注释字符是斜杠 (/) 并且语法上下文允许除法或除法赋值,在 LineTerminator 处不插入分号。也就是说,上面的例子被解释为:

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

我同意,这很令人困惑,应该有一个顶级语法表达式而不是两个。


编辑:

但是没有什么可以解释什么时候使用哪个。

也许简单的答案就在我们面前:尝试一个,然后尝试另一个。由于它们不是都被允许的,因此最多一个会产生一个无错误的匹配。

于 2011-04-01T22:46:16.360 回答