Понимание спецификации ECMAScript, часть 4
- вторник, 15 октября 2024 г. в 00:00:08
Привет, Хабр! Представляю вашему вниманию перевод четвертой статьи автора Marja Hölttä из цикла Understanding ECMAScript. Перевод первой части. Перевод второй части. Перевод третьей части.
Джейсон Орендорфф из Mozilla опубликовал прекрасный своей глубиной анализ синтаксических причуд JS. Несмотря на различия в деталях реализации, каждый движок JS сталкивается с одинаковыми проблемами, связанными с этими особенностями.
В этом выпуске мы подробно рассмотрим cover grammars. Это способ указать грамматику для синтаксических конструкций, которые на первый взгляд выглядят неоднозначно.
Опять же, мы не будем использовать shorthands [In, Yield, Await]
для краткости, поскольку они не важны для этого выпуска. Объяснение их значения и использования приведено в третьем выпуске.
Как правило, анализаторы (parsers) решают, какой нетерминал использовать, основываясь на finite lookahead (фиксированное количество последовательных токенов).
В некоторых случаях следующий токен однозначно определяет нетерминал для использования. Например:
UpdateExpression :
LeftHandSideExpression
LeftHandSideExpression ++
LeftHandSideExpression --
++ UnaryExpression
-- UnaryExpression
Если мы парсим UpdateExpression
и следующий токен ++
или --
, мы сразу поймем, какой нетерминал нужно использовать. Если следующий токен не является ни тем, ни другим, это все равно не так уж плохо: мы можем распарсить LeftHandSideExpression
, начиная с позиции, в которой мы находимся, и решить, что делать после того, как мы его распарсим.
Если токен, следующий за выражением LeftHandSideExpression
, равен ++
, то используем нетерминал UpdateExpression : LeftHandSideExpression ++
. Случай с --
аналогичен. И если токен, следующий за выражением LeftHandSideExpression
, не является ни ++
, ни --
, мы используем нетерминал UpdateExpression : LeftHandSideExpression
.
Отличить список аргументов arrow function от выражений, заключенных в круглые скобки, сложнее.
Например:
let x = (a,
Является ли это началом arrow function, подобной этой?
let x = (a, b) => { return a + b };
Или, может быть, это выражение, заключенное в круглые скобки? Как в этой конструкции:
let x = (a, 3);
Заключенное в скобки "что бы это ни было" может быть сколь угодно длинным - мы не можем знать, что это такое, основываясь на ограниченном числе токенов.
Давайте на мгновение представим, что у нас были следующие простые постановки:
AssignmentExpression :
...
ArrowFunction
ParenthesizedExpression
ArrowFunction :
ArrowParameterList => ConciseBody
Теперь мы не можем выбрать нетерминал для использования с помощью finite lookahead. Если бы нам нужно было распарсить выражение AssignmentExpression
, а следующим токеном был бы (
, как бы мы решили, что парсить следующим? Мы могли бы начать парсить ArrowFunctionParameterList
или ParenthesizedExpression
, но наше предположение может оказаться неверным.
Спецификация решает эту проблему, вводя символ CoverParenthesizedExpressionAndArrowParameterList
(сокращенно CPEAAPL). CPEAAPL - это символ, который на самом деле является ParenthesizedExpression
или ArrowParameterList
, но мы пока не знаем, каким именно.
Нетерминалы для 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, )
Висящая запятая (trailing comma) и токен ...
могут встречаться только в ArrowParameterList
. Некоторые конструкции, например, b = 1
, могут встречаться в обоих вариантах, но они имеют разные значения: внутри ParenthesizedExpression
- присваивание, внутри ArrowParameterList
- параметр со значением по умолчанию (default). Числа и другие PrimaryExpressions
, которые не валидны в качестве имён параметров (или в деструктурирующем присваивании (parameter destructuring patterns)), могут встречаться только в ParenthesizedExpression
. Но все они могут встречаться внутри CPEAAPL.
Теперь мы можем использовать вседозволяющий CPEAAPL в нетерминалах AssignmentExpression
.
(Примечание: 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
// ^^
// токен следующий после CPEAAPL
let x = (a, 3);
// ^^^^^^
// CPEAAPL
// ^
// токен следующий после CPEAAPL
На этом этапе мы можем оставить CPEAAPL как есть и продолжить парсинг остальной части программы. Например, если CPEAAPL находится внутри ArrowFunction
, нам пока не нужно проверять, является ли это валидным параметром arrow function или нет - это можно сделать позже. (В настоящих парсерах может быть выбрана реализация, которая проверяет валидность сразу, но, с точки зрения спецификации, в этом нет необходимости.)
Как мы видели ранее, грамматика нетерминалов для CPEAAPL очень свободная и допускает конструкции (такие как (1, ...a)
), которые всегда невалидны. После того как мы распарсим программу в соответствии с грамматикой, нам нужно запретить соответствующие недопустимые конструкции.
Спецификация делает это, добавляя следующие ограничения:
Статический анализ семантики: предварительные ошибки (Static Semantics: Early Errors)
PrimaryExpression : CPEAAPL
Синтаксической ошибкой (Syntax Error) является CPEAAPL, не подходящий под ParenthesizedExpression
.
Добавочный синтаксис (Supplemental Syntax)
При обработке экземпляра нетерминала PrimaryExpression : CPEAAPL
интерпретация CPEAAPL уточняется с помощью такой грамматики:
ParenthesizedExpression : ( Expression )
Что означает:
Если CPEAAPL встречается на месте PrimaryExpression
в синтаксическом дереве, то на самом деле это ParenthesizedExpression
, и это его единственный валидный нетерминал.
Expression
никогда не может быть пустым (empty), поэтому ( )
невалидное ParenthesizedExpression
. Списки, разделенные запятыми, такие как (1, 2, 3)
, создаются с помощью оператора запятой (comma operator):
Expression :
AssignmentExpression
Expression , AssignmentExpression
Аналогично, если вместо ArrowParameters
используется CPEAAPL, применяются следующие ограничения:
Static Semantics: Early Errors
ArrowParameters : CPEAAPL
Синтаксической ошибкой (Syntax Error) является CPEAAPL, не подходящий под ArrowFormalParameters
.
При обработке экземпляра нетерминала ArrowParameters : CPEAAPL
, интерпретация CPEAAPL уточняется с помощью такой грамматики:
ArrowFormalParameters :
( UniqueFormalParameters )
В дополнение к CPEAAPL, спецификация использует cover grammars для других неоднозначно выглядящих конструкций.
ObjectLiteral
используется в качестве cover grammar для ObjectAssignmentPattern
, который встречается внутри списков параметров arrow function. Это означает, что ObjectLiteral
допускает конструкции, которые не могут встречаться внутри реальных объектных литералов.
ObjectLiteral :
...
{ PropertyDefinitionList }
PropertyDefinition :
...
CoverInitializedName
CoverInitializedName :
IdentifierReference Initializer
Initializer :
= AssignmentExpression
Например:
let o = { a = 1 }; // Syntax Error
// arrow function с деструктуризацией параметра
// со значением по умолчанию
let f = ({ a = 1 }) => { return a; };
f({}); // возвращает 1
f({a : 6}); // возвращает 6
Асинхронные arrow function для finite lookahead выглядят неоднозначно.
let x = async(a,
Является ли это вызовом, с названием async
, или асинхронной arrow function?
let x1 = async(a, b);
let x2 = async();
function async() { }
let x3 = async(a, b) => {};
let x4 = async();
С этой целью, грамматика определяет cover grammar символ CoverCallExpressionAndAsyncArrowHead
, который работает схоже с CPEAAPL.
В этом выпуске мы рассмотрели, как спецификация определяет cover grammars и использует их в случаях, когда мы не можем идентифицировать текущую синтаксическую конструкцию с помощью finite lookahead.
В частности, мы рассмотрели, как отличить списки параметров arrow function от выражений, заключенных в круглые скобки, и как спецификация использует cover grammars, чтобы сначала распарсить неоднозначные конструкции вольно, а потом ограничить их правилами static semantics.