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

35

Javascript的语法解析具有一定的复杂性。正斜杠可以表示多个不同的意思:除号运算符、正则表达式文本、注释引入器或行注释引入器。后两者很容易区分:如果斜杠后跟一个星号,则它开始一个多行注释。如果斜杠后跟另一个斜杠,则它是一条行注释。

但是,消除除法和正则表达式文本的歧义规则使我感到困惑。我在ECMAScript标准中找不到相关内容。在该标准中,词法语法明确分为两部分:InputElementDiv和InputElementRegExp,具体取决于斜杠的含义。但没有任何说明何时使用哪个。

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

有没有人有清晰代码的示例来对Javascript进行词法分析并得出答案?


1
还有除法赋值运算符 /= - Šime Vidas
从规范中看来,我认为解析器需要知道要获取什么类型的标记。这似乎是一个可怕的语法特性,但无论如何。这也似乎非常笨拙,因为在解析表达式时,语法必须尝试其中两个,并且还要尝试另一个“普通”标记的更“通用”请求。呃。如果我面对这个问题,我想我会回去修复语法 :-) - Pointy
3
我对JavaScript的理解是,你不能只写一个词法分析器,而不编写语法分析器,这与许多其他编程语言不同。 - MarkPflug
嗯,我无法想象词法分析器会以那种方式工作,但我相当简单。在我的(微小的)世界里,从词法分析器到解析器有一个单向流动。在这种设置下,词法分析器真的不知道它应该做什么。当其中一个有效时,尝试另一个几乎肯定会产生错误(特别是因为正则表达式语法可能会使词法分析器不必要地扫描大量输入文本)。 - Pointy
1
目前的回答都没有涉及到在斜杠前使用await,这可能会导致歧义 - 如果在async函数内部,则斜杆将被解析为正则表达式,否则,await将被解析为变量名,因此斜杆将被解析为除法。 - CertainPerformance
显示剩余2条评论
5个回答

20
实际上很容易,但需要使您的词法分析器比通常更智能。
除了表达式后必须跟随除法运算符外,正则表达式字面量不能跟随表达式,因此在所有其他情况下,您可以安全地假定正在查看一个正则表达式字面量。
如果你做得对的话,你已经要将标点符号识别为多字符字符串。所以看一下前一个标记,并查看它是否是以下任何一个:
. ( , { } [ ; , < > <= >= == != === !== + - * % ++ --
<< >> >>> & | ^ ! ~ && || ? : = += -= *= %= <<= >>= >>>=
&= |= ^= / /=

对于大多数情况,你现在知道你处于一个可以找到正则表达式字面量的上下文中。但是,在++ --的情况下,你需要做一些额外的工作。如果++--是前缀递增/递减,则其后面的/将开始一个正则表达式字面量;如果它是后缀递增/递减,则其后面的/将开始一个DivPunctuator。
幸运的是,你可以通过检查其前一个标记来确定它是否为“前缀”运算符。首先,后缀递增/递减是一个受限制的产生方式,因此如果++--之前有一个换行符,则你知道它是“前缀”的。否则,如果前一个标记是可以出现在正则表达式字面量之前的任何东西(递归万岁!),那么你就知道它是“前缀”的。在所有其他情况下,它是“后缀”的。
当然,) 标点符号并不总是表示表达式的结尾 - 例如 if (something) /regex/.exec(x)。这很棘手,因为它需要一些语义理解来分离。
可悲的是,这还不够。有一些运算符不是标点符号,还有其他值得注意的关键字。正则表达式字面量也可以跟随它们。它们是:
new delete void typeof instanceof in do return case throw else

如果你刚才读取的 IdentifierName 是以下其中一个,那么你就在查看正则表达式字面量;否则,它就是 DivPunctuator。

上述内容基于 ECMAScript 5.1 规范(可在 这里 找到),不包括语言的任何特定于浏览器的扩展。但如果你需要支持这些扩展,那么这应该提供了简单的指南,以确定你处于哪种上下文中。

当然,大多数情况下,以上都是非常愚蠢的包含正则表达式字面量的情况。例如,你实际上不能预增加一个正则表达式,即使它在语法上是允许的。因此,大多数工具可以通过简化真实应用程序的正则表达式上下文检查来轻松解决问题。JSLint 检查前一个字符是否为 (,=:[!&|?{}; 的方法可能已经足够了。但是,如果你在开发旨在词法分析 JS 的工具时采取这样的捷径,那么你应该确保注意到这一点。


1
这种方法适用于大多数实际代码,但无法正确地解析此示例:if (something) /regex/.exec(x); - JacquesB
@JacquesB exec没有副作用。有没有一个现实的例子,可以让正则表达式开始一条语句? - John Dvorak
1
@JanDvorak 此答案应适用于所有语法上有效的代码,无论其现实性如何。 - Tamzin Blake
new delete void typeof instanceof in do return case throw - 应该也包括 else 吧?if (true) {} else /regex/; - lexicore
1
我相信 of 也可以在正则表达式之前使用:for (const a of /foo/.exec('foo')) { - CertainPerformance
显示剩余5条评论

15
我目前正在使用JavaCC开发一个JavaScript/ECMAScript 5.1解析器RegularExpressionLiteralAutomatic Semicolon Insertion是ECMAScript语法中让我感到困惑的两个问题。这个问题和答案对于正则表达式问题非常宝贵。在这个答案中,我想把自己的发现整理在一起。 简而言之,在JavaCC中使用词法状态从解析器中切换

Thom Blake所写的内容非常重要:

除了正则表达式字面量外,除法运算符必须跟在一个表达式后面。因此,在其他情况下,您可以安全地假设您正在查看一个正则表达式字面量。

因此,实际上您需要在之前了解它是否为表达式。在解析器中这很简单,但在词法分析器中却非常困难。

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

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

case
delete
do
else
in
instanceof
new
return
throw
typeof
void

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

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

如果您有其中之一,并在此之后看到/ ... ,那么这绝不能是DivPunctuator,因此必须是RegularExpressionLiteral

接下来,如果您有:

/

接着,紧随其后的必须是一个RegularExpressionLiteral。如果这些斜杠之间没有空格(例如// ...),则必须将其视为SingleLineComment(“最大匹配”)。

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

]

所以接下来的/必须开始一个DivPunctuator
现在我们还有以下几种情况是不幸的二义性:
}
)
++
--

对于 }),你需要知道它们是否结束一个表达式,对于 ++--,它们会结束一个 后缀表达式 或者开始一个 一元表达式

我得出结论,在词法分析器中很难(如果不是不可能)找出。为了让你有所感觉,以下是几个例子。

在这个例子中:

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

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

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

向前看解析器可能已经在令牌流中走得太远了(即已将/读为DIV或反之亦然)。

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

有没有一种方法可以使SwitchTo更安全?

想法是创建一个“备份”令牌流,并将在向前看期间读取的令牌推回去。

我认为这应该适用于 })++--,因为它们通常出现在 LOOKAHEAD(1) 的情况下,但我不能百分之百确定。在最坏的情况下,词法分析器可能已经尝试将以 / 开头的标记解析为 RegularExpressionLiteral,但由于没有被另一个 / 终止而失败。
无论如何,我看不到更好的方法。下一个好的方法可能是完全放弃这种情况(就像 JSLint 和许多其他人所做的那样),文档化并且不解析这些类型的表达式。 {}/a/g 没有太多意义。

这是一个很棒的答案。关于最后一段,另一个选项是同时进行词法分析和语法分析,这是现在的标准做法。 - Tamzin Blake
谢谢@ThomBlake。同时进行词法分析和语法分析 - 你能否给我一些提示,我应该在Java中使用什么?现在我正在使用JavaCC。作为领域的新手,如果您能指点一下方向,我将不胜感激。谢谢。 - lexicore
我大约对Java一无所知,我写过的大多数解析器都是手写的。如果有帮助的话,Rhino是用Java编写的,你可以借鉴一些代码。 - Tamzin Blake
这并不难,{} /a/g/a/g 是一个正则表达式,因为它在语句上下文中,而 {} 是一个块语句。如果解析器假定它是一个表达式上下文,那么它将被视为除法。尽管我从你的答案中得到了一些想法,是的。 - user5066707
对于较新的ECMAScript版本,您可能还需要查找awaitdefault(如export default /a/g)、extendsyield...???. - lydell
此外,在模板插值中允许使用正则表达式。`${/a/}` - lydell

5

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

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

Rhino总是从词法分析器返回一个DIV斜杠标记。

4

1
在那个页面上有一个相当简单的规则,使用前一个标记来确定斜杠的含义。但这是一个js 2.0规则,所以它不适用于当前代码? - Ned Batchelder

3

请查看第7节:

There are two goal symbols for the lexical grammar. The InputElementDiv symbol is used in those syntactic grammar contexts where a leading division (/) or division-assignment (/=) operator is permitted. The InputElementRegExp symbol is used in other syntactic grammar contexts.

NOTE There are no syntactic grammar contexts where both a leading division or division-assignment, and a leading RegularExpressionLiteral are permitted. This is not affected by semicolon insertion (see 7.9); in examples such as the following:

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

where the first non-whitespace, non-comment character after a LineTerminator is slash (/) and the syntactic context allows division or division-assignment, no semicolon is inserted at the LineTerminator. That is, the above example is interpreted in the same way as:

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

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


编辑:

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

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


1
从问题描述中可以看出:“但没有解释何时使用哪个选项” - 我认为这是这个问题的主要问题。你能解决一下吗? - Šime Vidas
尽管您的引用确实说明了不存在两者都被允许的情境... - Šime Vidas
我读了这部分内容。它说没有重叠,但没有说明何时选择其中之一。 - Ned Batchelder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接