跟着V8引擎读懂 ECMAScript 规范(四)原创
在「跟着V8引擎读懂 ECMAScript 规范(三)」中,我们开始阅读ECMAScript 语法相关的定义,本文我们将继续更进一步的研究,深入了解一下「覆盖语法」(cover grammar)。起初,它们是为那些看起来模棱两可的句法结构指明语法的一种方法。
覆盖语法 cover grammar
「覆盖语法」通常是指可以识别多种输入类型的规则,以便以后消除歧义。 例如,尝试解析“类型或表达式”在特定语言中可能是模棱两可的,因此您可能会设计一种“覆盖语法”,它可以接受其中任何一种,而无需知道它是哪一种。 然后解析器外部的后续分析可以确定它应该是哪一个。
有限前瞻 Finite lookaheads
通常,解析器根据有限的前瞻(固定数量的后续标记)决定使用哪个产生式。
在某些情况下,下一个标记明确确定要使用的产生式。 例如:
UpdateExpression :
LeftHandSideExpression
LeftHandSideExpression ++
LeftHandSideExpression --
++ UnaryExpression
-- UnaryExpression
如果我们正在解析一个 UpdateExpression 并且下一个标记是 ++ 或 --,我们就知道要立即使用的产生式。 如果下一个标记都不是,那还不算太糟糕:我们可以从所在的位置开始解析一个 LeftHandSideExpression,然后在解析完它之后弄清楚要做什么。
如果 LeftHandSideExpression 后面的标记是 ++,则使用的产生式是 UpdateExpression : LeftHandSideExpression ++。 -- 的情况类似。 如果 LeftHandSideExpression 后面的标记既不是 ++ 也不是 --,我们使用产生式 UpdateExpression : LeftHandSideExpression。
箭头函数参数列表或带括号的表达式呢?
将箭头函数参数列表与带括号的表达式区分开来更加复杂。如:
let x = (a,
这是箭头函数的开始吗?像这样
let x = (a, b) => { return a + b };
又或者它是一个带括号的表达式?像这样
let x = (a, 3);
括号中的 “what-it-is” 可以任意长——我们无法根据有限数量的令牌知道它是什么。
想象一下,如果我们有以下简单的产生式:
AssignmentExpression :
...
ArrowFunction
ParenthesizedExpression
ArrowFunction :
ArrowParameterList => ConciseBody
现在我们不能选择使用有限前瞻的产生式。 如果我们必须解析一个 AssignmentExpression 并且下一个标记是 (,我们将如何决定接下来要解析什么?我们可以解析 ArrowParameterList 或 ParenthesizedExpression,但我们的猜测可能会出错。
非常宽松的新符号:CPEAAPL
ECMAScript 规范通过引入符号 CoverParenthesizedExpressionAndArrowParameterList(简称 CPEAAPL)解决了这个问题。 CPEAAPL 是一个符号,在幕后实际上是一个括号表达式或箭头参数列表,但我们还不知道是哪一个。
CPEAAPL 的产生式非常宽松,允许所有可能出现在 ParenthesizedExpressions 和 ArrowParameterLists 中的构造:
CPEAAPL :
( Expression )
( Expression , )
( )
( ... BindingIdentifier )
( ... BindingPattern )
( Expression , ... BindingIdentifier )
( Expression , ... BindingPattern )
例如,以下表达式是有效的 CPEAAPL:
// 有效的括号表达式ParenthesizedExpression和箭头参数列表ArrowParameterList:
(a, b)
(a, b = 1)
// 有效的括号表达式ParenthesizedExpression:
(1, 2, 3)
(function foo() { })
// 有效的箭头参数列表ArrowParameterList:
()
(a, b,)
(a, ...b)
(a = 1, ...b)
// 不是有效的括号表达式和箭头参数列表,但仍然属于 CPEAAPL:
(1, ...b)
(1, )
尾随逗号和 ... 只能出现在 ArrowParameterList 中。 某些结构,例如 b = 1 可以同时出现在两者当中,但它们却具有不同的含义:在 ParenthesizedExpression 内部它是一个赋值,在 ArrowParameterList 内部它是一个具有默认值的参数。 数字和其他不是有效参数名称(或参数解构模式)的 PrimaryExpression ,只能出现在 ParenthesizedExpression 中。 但它们都可能出现在 CPEAAPL 中。
在产生式中应用 CPEAAPL
现在我们可以在 AssignmentExpression 产生式中使用非常宽松的 CPEAAPL。 (注:ConditionalExpression 会通过一个长的产生式链导致 PrimaryExpression,这里我们不以展示。)
AssignmentExpression :
ConditionalExpression
ArrowFunction
...
ArrowFunction :
ArrowParameters => ConciseBody
ArrowParameters :
BindingIdentifier
CPEAAPL
PrimaryExpression :
...
CPEAAPL
想象一下,我们再次处于需要解析一个 AssignmentExpression 并且下一个标记是 (。现在我们可以解析一个 CPEAAPL 并确定稍后要使用什么产生式的情况。无论我们是在解析 ArrowFunction 还是 一个 ConditionalExpression,下一个要解析的符号无论如何都是 CPEAAPL!
在我们解析 CPEAAPL 之后,我们可以决定将哪个产生式用于原始的 AssignmentExpression(包含 CPEAAPL 的产品)。 该决定是基于位于 CPEAAPL 之后的标记做出的。
如果该标记是 =>,则使用以下产生式:
AssignmentExpression :
ArrowFunction
如果标记是其他的内容,则用下面的产生式:
AssignmentExpression :
ConditionalExpression
如:
let x = (a, b) => { return a + b; };
// ^^^^^^
// CPEAAPL
// ^^
// The token following the CPEAAPL
let x = (a, 3);
// ^^^^^^
// CPEAAPL
// ^
// The token following the CPEAAPL
那时我们可以保持 CPEAAPL 不变并继续解析程序的其余部分。 例如,如果 CPEAAPL 在 ArrowFunction 中,我们还不需要查看它是否是有效的箭头函数参数列表——这可以稍后完成。 (现实世界的解析器可能会选择立即进行有效性检查,但从规范的角度来看,并没有要求。)
限制 CPEAAPL
正如我们之前看到的,CPEAAPL 的语法产生式是非常宽松的,并且允许(例如 (1, ...a))这种永远无效的构造。 根据语法完成程序解析后,我们需要禁止相应的非法构造。
规范通过添加以下限制来做到这一点:
Static Semantics: Early Errors
PrimaryExpression : CPEAAPL
It is a Syntax Error if CPEAAPL is not covering a ParenthesizedExpression.
When processing an instance of the production
PrimaryExpression : CPEAAPL
the interpretation of the CPEAAPL is refined using the following grammar:
ParenthesizedExpression : ( Expression )
这意味着:如果 CPEAAPL 出现在语法树中 PrimaryExpression 的位置,它实际上是一个 ParenthesizedExpression,这是它唯一有效的产生式。
表达式永远不能为空,因此 ( ) 不是有效的括号表达式。 逗号分隔的列表(如 (1, 2, 3))由逗号运算符创建:
Expression :
AssignmentExpression
Expression , AssignmentExpression
同样,如果 CPEAAPL 出现在 ArrowParameters 的位置,则适用以下限制:
Static Semantics: Early Errors
ArrowParameters : CPEAAPL
It is a Syntax Error if CPEAAPL is not covering an ArrowFormalParameters.
When the production
ArrowParameters : CPEAAPL
is recognized the following grammar is used to refine the interpretation of CPEAAPL:
ArrowFormalParameters :
( UniqueFormalParameters )
其他覆盖语法
除了 CPEAAPL 之外,该规范还为其他看起来模棱两可的结构使用了覆盖语法。
ObjectLiteral 用作 ObjectAssignmentPattern 的覆盖语法,它出现在箭头函数参数列表中。 这意味着 ObjectLiteral 允许那些在实际对象文字中不能出现的构造。
ObjectLiteral :
...
{ PropertyDefinitionList }
PropertyDefinition :
...
CoverInitializedName
CoverInitializedName :
IdentifierReference Initializer
Initializer :
= AssignmentExpression
例如:
let o = { a = 1 }; // syntax error
// Arrow function with a destructuring parameter with a default
// value:
let f = ({ a = 1 }) => { return a; };
f({}); // returns 1
f({a : 6}); // returns 6
异步箭头函数在有限前瞻中也显得模棱两可:
let x = async(a,
这是对名为 async 的函数或 async 箭头函数的调用吗?
let x1 = async(a, b);
let x2 = async();
function async() { }
let x3 = async(a, b) => {};
let x4 = async();
为此,该语法定义了一个覆盖语法符号 CoverCallExpressionAndAsyncArrowHead,其工作方式与 CPEAAPL 类似。
小结
在本文中,我们研究了 ECMAScript 规范如何定义覆盖语法(cover grammar),并在我们无法基于有限前瞻(finite lookahead)识别当前句法结构的情况下使用它们。我们研究了将箭头函数参数列表与带括号的表达式区分开来,以及规范如何使用覆盖语法先允许解析看起来模棱两可的结构,然后再用静态语义规则限制它们的定义。