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

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

4天前
118601

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

分类:
标签:
请先登录,再评论

暂无回复,快来写下第一个回复吧~

为你推荐

一次 Node.js http 连接无法复用的问题排查
一次压测中阿里云 SLB 的并发连接数被打满了,导致服务之间的 HTTP 调用延迟很大。当时 SLB 的并发连接数情况如下图所示。登录容器终端查看,发现某个前端 Node.js 服务中的单个容器的 E
Chrome V8 javascript engine C++ 高性能垃圾收集器 Oilpan
这篇文章是 Oilpan 系列文章,它将概述 Oilpan 的核心原理及其 C++ API。 在这篇文章中,我们将介绍一些支持的特性,解释它们如何与垃圾收集器的各种子系统交互,并深入探讨在清扫器中并发回收对象。
深入了解 Chrome V8 javascript engine 的垃圾收集引擎
V8 使用分代垃圾收集器,将 Javascript 堆拆分为用于新分配对象的小型年轻代和用于长期存活对象的大型老年代。 由于大多数对象在年轻时死亡,这种分代策略使垃圾收集器能够在较小的年轻代(称为清除)中执行常规的、短时间的垃圾收集,而无需跟踪年老代中的对象。
Chrome V8 javascript engine 的3种垃圾回收算法
从 v6.2 开始,V8 将用于收集年轻代的默认算法切换为并行 Scavenger,本文介绍V8的3种垃圾回收算法:V8 年轻代切尼半空间复制垃圾回收算法、V8 年轻代并行 Mark-Evacuate 垃圾回收算法、V8 年轻代并行 Scavenger 垃圾回收算法
跟着V8引擎读懂 ECMAScript 规范(一)
在本文中,我们通过阅读分析一个简单的方法 —— Object.prototype.hasOwnProperty —— 以及它调用的抽象操作,熟悉了速记 ?和 !与错误处理有关,了解了语言类型、规范类型、内部属性和内部方法。对大家更好地阅读理解 ECMAScript 规范 打下很好的基础。
跟着V8引擎读懂 ECMAScript 规范(二)
本文我们研究了ECMAScript规范如何定义语言特征,在本例中了解到了原型链遍历,跨越所有不同的层:触发特征的句法结构和定义它的算法。
跟着V8引擎读懂 ECMAScript 规范(三)
在本文,我们熟悉了词法语法、句法语法以及用于定义句法语法的简写。 我们研究了在异步函数中禁止使用 await 作为标识符,但在非异步函数中它却可以使用的相关定义。
跟着V8引擎读懂 ECMAScript 规范(四)
在本文中,我们研究了 ECMAScript 规范如何定义覆盖语法(cover grammar),并在我们无法基于有限前瞻(finite lookahead)识别当前句法结构的情况下使用它们。我们研究了将箭头函数参数列表与带括号的表达式区分开来,以及规范如何使用覆盖语法先允许解析看起来模棱两可的结构,