在解析Javascript时,是什么决定了斜杠的含义?

Ned*_*der 29 javascript lexer

Javascript有一个棘手的语法来解析.正斜杠可以表示许多不同的东西:除法运算符,正则表达式文本,注释引入者或行注释引入者.最后两个很容易区分:如果斜线后跟一个星号,则会启动多行注释.如果斜杠后跟另一个斜杠,则为行注释.

但消除歧义和正则表达式字面意义的规则正在逃避我.我在ECMAScript标准中找不到它.词汇语法明确分为两部分,InputElementDiv和InputElementRegExp,具体取决于斜杠的含义.但没有什么可以解释何时使用哪个.

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

有没有人有一个明确的代码为lexing Javascript有答案?

Tam*_*ake 16

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

除法运算符必须遵循表达式,并且正则表达式文字不能跟随表达式,因此在所有其他情况下,您可以安全地假设您正在查看正则表达式文字.

如果你做得对,你必须将Punctuators识别为多字符串.所以看看前面的标记,看看它是否是以下任何一个:

. ( , { } [ ; , < > <= >= == != === !== + - * % ++ --
<< >> >>> & | ^ ! ~ && || ? : = += -= *= %= <<= >>= >>>=
&= |= ^= / /=
Run Code Online (Sandbox Code Playgroud)

对于大多数这些,您现在知道您处于可以找到正则表达式文字的上下文中.现在,在这种情况下++ --,你需要做一些额外的工作.如果++或是--一个预增量/减量,那么/下面它启动一个正则表达式文字; 如果是后递增/递减,则/接下来启动DivPunctuator.

幸运的是,您可以通过检查先前的令牌来确定它是否是"预先"运算符.首先,后递增/递减是限制生产,因此如果++--之前有换行符,那么您知道它是"预先".否则,如果前一个标记是正则表达式文字之前的任何事物(yay recursion!),那么你知道它是"pre-".在所有其他情况下,它是"后 - ".

当然,)标点符号并不总是表示表达式的结尾 - 例如if (something) /regex/.exec(x).这很棘手,因为它确实需要一些语义理解才能解开.

可悲的是,这并不是全部.有些运营商不是标点符号,还有其他值得注意的关键字.正则表达式文字也可以遵循这些.他们是:

new delete void typeof instanceof in do return case throw else
Run Code Online (Sandbox Code Playgroud)

如果您刚刚使用的IdentifierName就是其中之一,那么您正在查看正则表达式文字; 否则,它是一个DivPunctuator.

以上内容基于ECMAScript 5.1规范(如此处所示),不包括该语言的任何特定于浏览器的扩展.但是,如果您需要支持这些,那么这应该提供简单的指导方针来确定您所处的上下文类型.

当然,上面的大多数代表了包含正则表达式文字的非常愚蠢的情况.例如,即使在语法允许的情况下,也无法实际预先增加正则表达式.因此,大多数工具都可以通过简化实际应用程序的正则表达式上下文检查来实现.JSLint检查前一个字符的方法(,=:[!&|?{};可能就足够了.但是如果你在开发什么应该是lexing JS的工具时采取这样的捷径,那么你应该注意这一点.


lex*_*ore 8

我目前正在使用JavaCC 开发JavaScript/ECMAScript 5.1解析器.RegularExpressionLiteralAutomatic Semicolon Insertion是让我在ECMAScript语法中疯狂的两件事.对于正则表达式问题,这个问题和答案是非常宝贵的.在这个答案中,我想把自己的发现放在一起.

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


Thom Blake写的非常重要:

除法运算符必须遵循表达式,并且正则表达式文字不能跟随表达式,因此在所有其他情况下,您可以安全地假设您正在查看正则表达式文字.

所以,你真的需要明白,如果它是一个表达式或不.这在解析器中是微不足道的,但在词法分析器中非常难.

正如Thom 指出的那样,在许多(但不幸的是,并非所有)案例中,你可以通过"查看"最后一个标记来理解它是否是一个表达式.你必须考虑标点符号和关键字.

让我们从关键字开始吧.以下关键字不能位于a之前DivPunctuator(例如,您不能拥有case /5),因此如果您看到以下关键字/,则您有RegularExpressionLiteral:

case
delete
do
else
in
instanceof
new
return
throw
typeof
void
Run Code Online (Sandbox Code Playgroud)

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

{       (       [   
.   ;   ,   <   >   <=
>=  ==  !=  === !== 
+   -   *   %       
<<  >>  >>> &   |   ^
!   ~   &&  ||  ?   :
=   +=  -=  *=  %=  <<=
>>= >>>=    &=  |=  ^=
    /=
Run Code Online (Sandbox Code Playgroud)

因此,如果你有其中一个,并/...在此之后看到,那么这永远不会是一个DivPunctuator,因此必须是一个RegularExpressionLiteral.

接下来,如果你有:

/
Run Code Online (Sandbox Code Playgroud)

/...之后,它也必须是一个RegularExpressionLiteral.如果这些斜杠之间没有空格(即 // ...),则必须将其作为SingleLineComment("最大蒙克")处理.

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

]
Run Code Online (Sandbox Code Playgroud)

所以以下/必须开始DivPunctuator.

现在我们有以下剩余的案例,不幸的是,这些案例含糊不清:

}
)
++
--
Run Code Online (Sandbox Code Playgroud)

对于})你要知道,如果他们最终的表达与否,对于++---他们最终的PostfixExpression或启动UnaryExpression.

我得出的结论是,在词法分析器中找到它是非常困难的(如果不是不可能的话).为了让你对此有所了解,举几个例子.

在这个例子中:

{}/a/g
Run Code Online (Sandbox Code Playgroud)

/a/g是一个RegularExpressionLiteral,但在这一个:

+{}/a/g
Run Code Online (Sandbox Code Playgroud)

/a/g 是一个分裂.

如果)你有分工:

('a')/a/g
Run Code Online (Sandbox Code Playgroud)

以及RegularExpressionLiteral:

if ('a')/a/g
Run Code Online (Sandbox Code Playgroud)

所以,不幸的是,看起来你无法单独使用词法分析器来解决它.或者你必须在词法分析器中输入这么多的语法,所以它不再是词法分析器了.

这是个问题.


现在,一个可能的解决方案,在我的案例中基于JavaCC.

我不确定你是否在其他解析器生成器中有类似的功能,但JavaCC有一个词法状态功能,可用于在"我们期望一个DivPunctuator"和"我们期望一个RegularExpressionLiteral"状态之间切换.例如,在这个语法中,NOREGEXP状态意味着"我们不期望在RegularExpressionLiteral这里".

这解决了问题的一部分,而不是模棱两可的),},++--.

为此,您需要能够从解析器切换词法状态.这是可能的,请参阅JavaCC FAQ中的以下问题:

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

是的,但通过这样做很容易创建错误.

先行解析器可能已经在令牌流中走得太远(即已经读取/为a DIV或反之亦然).

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

有没有办法让SwitchTo更安全?

我们的想法是制作一个"备份"令牌流,并在前瞻期间再次推送令牌.

我认为这应该工作},),++,--因为它们在LOOKAHEAD(1)正常情况下发现的,但我不知道的100%.在最坏的情况下,词法分析器可能已经尝试解析 - 启动/令牌作为a RegularExpressionLiteral并失败,因为它没有被另一个终止/.

无论如何,我认为没有更好的方法.下一个好处可能是完全放弃案例(像JSLint许多其他人一样),文档并且不解析这些类型的表达式.{}/a/g反正没有多大意义.


Vin*_*jip 5

如果前面的标记是其中之一,JSLint似乎期望正则表达式

(,=:[!&|?{};
Run Code Online (Sandbox Code Playgroud)

Rhino总是从词法分析器返回一个DIV令牌.


Jas*_*n S 3

参见第 7 节:

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

注意 不存在允许使用前导除法或除法赋值以及前导正则表达式文字的语法上下文。这不受分号插入的影响(见 7.9);在如下示例中:

a = b 
/hi/g.exec(c).map(d); 
Run Code Online (Sandbox Code Playgroud)

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

a = b / hi / g.exec(c).map(d); 
Run Code Online (Sandbox Code Playgroud)

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


编辑:

但没有任何解释何时使用哪个。

也许简单的答案就在我们面前:尝试一种,然后尝试另一种。由于不允许同时使用它们,因此至多其中一个会产生无错误的匹配。