性能文章>跟着V8引擎读懂 ECMAScript 规范(四)>

跟着V8引擎读懂 ECMAScript 规范(四)原创

2年前
365723

在「跟着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 的产生式非常宽松,允许所有可能出现在 ParenthesizedExpressionsArrowParameterLists 中的构造:

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 不变并继续解析程序的其余部分。 例如,如果 CPEAAPLArrowFunction 中,我们还不需要查看它是否是有效的箭头函数参数列表——这可以稍后完成。 (现实世界的解析器可能会选择立即进行有效性检查,但从规范的角度来看,并没有要求。)

 

限制 CPEAAPL

 

正如我们之前看到的,CPEAAPL 的语法产生式是非常宽松的,并且允许(例如 (1, ...a))这种永远无效的构造。 根据语法完成程序解析后,我们需要禁止相应的非法构造。

 

规范通过添加以下限制来做到这一点:

 

Static Semantics: Early Errors

PrimaryExpression : CPEAAPL

It is a Syntax Error if CPEAAPL is not covering a ParenthesizedExpression.

 

Supplemental Syntax

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.

 

Supplemental Syntax

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)识别当前句法结构的情况下使用它们。我们研究了将箭头函数参数列表与带括号的表达式区分开来,以及规范如何使用覆盖语法先允许解析看起来模棱两可的结构,然后再用静态语义规则限制它们的定义。

点赞收藏
分类:标签:
风之石
请先登录,查看2条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
3
2