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

时间:2011-04-01 22:38:08

标签: javascript lexer

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

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

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

有没有人有一个明确的代码来解决具有答案的leavascript Javascript?

5 个答案:

答案 0 :(得分:15)

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

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

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

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

对于其中大部分内容,您现在知道自己处于可以找到正则表达式文字的上下文中。现在,在++ --的情况下,您需要做一些额外的工作。如果++--是预增量/减量,则跟随它的/启动正则表达式文字;如果是后递增/递减,则跟随它的/启动DivPunctuator。

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

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

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

new delete void typeof instanceof in do return case throw else

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

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

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

答案 1 :(得分:7)

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

TL; DR 在JavaCC中,使用lexical statesswitch them from the parser


Thom Blake写的非常重要:

  

除法运算符必须遵循表达式和常规表达式   表达式文字不能跟随表达式,因此在所有其他情况下都是如此   您可以放心地假设您正在查看正则表达式字面值。

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

作为Thom pointed out,在许多(但不幸的是,不是全部)案例中,你可以理解它是否是一个表达式,并且#34;在最后一个令牌。你必须考虑标点符号和关键字。

让我们从关键字开始。以下关键字不能位于DivPunctuator之前(例如,您不能拥有case /5),因此如果您看到/之后的RegularExpressionLiteral,则会有case delete do else in instanceof new return throw typeof void

DivPunctuator

接下来,标点符号。以下标点符号不能位于{ /a...之前(例如/符号{ ( [ . ; , < > <= >= == != === !== + - * % << >> >>> & | ^ ! ~ && || ? : = += -= *= %= <<= >>= >>>= &= |= ^= /= 永远不能开始除法):

/...

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

接下来,如果你有:

/...

之后RegularExpressionLiteral它也必须是// ...。如果这些斜杠之间没有空格(即SingleLineComment),则必须将其作为] 处理(&#34;最大咀嚼&#34;)。

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

/

因此,以下DivPunctuator必须启动} ) ++ --

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

}

对于)++,您必须知道他们是否结束表达式,--PostfixExpression - 他们结束UnaryExpression或开始{}/a/g

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

在这个例子中:

/a/g

RegularExpressionLiteral+{}/a/g ,但在这一个:

/a/g

)是一个部门。

如果是('a')/a/g ,您可以进行分组:

RegularExpressionLiteral

以及if ('a')/a/g

DivPunctuator

所以,不幸的是,看起来你无法单独使用词法分析器来解决它。或者你必须在词法分析器中引入如此多的语法,因此它不再是词法分析器了。

这是一个问题。


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

我不确定你在其他解析器生成器中是否有类似的功能,但是JavaCC有一个lexical states功能,可用于切换&#34;我们期望RegularExpressionLiteral&#34;和&#34;我们期待NOREGEXP&#34;状态。例如,在this grammar中,RegularExpressionLiteral州意味着&#34;我们不希望)在这里&#34 ;.

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

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

  

Can the parser force a switch to a new lexical state?

     

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

前瞻解析器可能已在令牌流中走得太远(即已将DIV读作},反之亦然。)

幸运的是,似乎有办法让切换词汇状态更安全一些:

  

Is there a way to make SwitchTo safer?

这个想法是做一个&#34;备份&#34;令牌流和推送令牌在前瞻期间再次读回。

我认为这应该适用于)++--/,因为它们通常在LOOKAHEAD(1)情况下找到,但我不是100确定无疑。在最坏的情况下,词法分析者可能已经尝试解析RegularExpressionLiteral - 将标记作为/启动并失败,因为它未被另一个JSLint终止。

无论如何,我认为没有更好的办法。下一个好处可能是完全放弃案例(如{}/a/g和其他许多人所做的那样),记录并且不解析这些类型的表达式。 {{1}}无论如何都没有多大意义。

答案 2 :(得分:5)

如果前面的标记是

之一,则JSLint似乎期望正则表达式
(,=:[!&|?{};

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

答案 3 :(得分:4)

您只能通过实现语法分析器来了解如何解释/。无论哪个lex路径到达有效的解析,都会决定如何解释该字符。显然,这是他们考虑修复的东西,但没有。 更多阅读: http://www-archive.mozilla.org/js/language/js20-2002-04/rationale/syntax.html#regular-expressions

答案 4 :(得分: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); 

我同意,这很混乱,应该有一个顶级语法表达而不是两个。


编辑:

  

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

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