Понимание спецификации ECMAScript, часть 4
Тем временем в других частях Веба
Джейсон Орендорф из Mozilla опубликовал отличный подробный анализ синтаксических особенностей JS. Несмотря на различия в деталях реализации, все движки JS сталкиваются с одинаковыми проблемами, связанными с этими особенностями.
Покрывающие грамматики
В этом эпизоде мы глубже изучим покрывающие грамматики. Это способ описания грамматики для синтаксических конструкций, которые на первый взгляд кажутся неоднозначными.
Снова пропустим нижние индексы для [In, Yield, Await]
для краткости, так как они не важны для этого поста в блоге. Смотрите часть 3 для объяснения их значения и использования.
Конечный просмотр вперед
Обычно анализаторы решают, какое производство использовать, основываясь на конечном просмотре вперед (фиксированное количество следующих токенов).
В некоторых случаях следующий токен однозначно определяет производство. Например:
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);
Содержимое скобок может быть произвольно длинным - мы не можем знать, что это такое, исходя из конечного количества токенов.
Давайте представим на мгновение, что у нас есть следующие прямолинейные продукции:
AssignmentExpression :
...
ArrowFunction
ParenthesizedExpression
ArrowFunction :
ArrowParameterList => ConciseBody
Теперь мы не можем выбрать производство, которое использовать, с конечным просмотром вперед. Если нам нужно разобрать AssignmentExpression
и следующий токен — это (
, как бы мы решили, что разбирать дальше? Мы могли либо разобрать ArrowParameterList
, либо ParenthesizedExpression
, но наша догадка могла бы быть неверной.
Очень разрешительный новый символ: CPEAAPL
Спецификация решает эту проблему посредством введения символа CoverParenthesizedExpressionAndArrowParameterList
(сокращенно CPEAAPL
). CPEAAPL
— это символ, который на деле является ParenthesizedExpression
или ArrowParameterList
, но мы пока не знаем, какой именно.
Продукции для CPEAAPL
очень разрешительные, они допускают все конструкции, которые могут встретиться в ParenthesizedExpression
и в ArrowParameterList
:
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
это параметр со значением по умолчанию. Числа и другие PrimaryExpressions
, которые не являются допустимыми именами параметров (или шаблонами деструктуризации параметров), могут встречаться только в ParenthesizedExpression
. Однако они все могут встречаться внутри CPEAAPL
.
Использование 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
, нам пока не нужно смотреть, является ли он допустимым списком параметров стрелочной функции — это можно сделать позже. (Парсеры в реальном мире могут предпочесть проверку допустимости сразу, но с точки зрения спецификации это не обязательно.)
Ограничение CPEAAPL
Как мы видели ранее, грамматические производства для CPEAAPL
очень разрешительные и позволяют конструкции (такие как (1, ...a)
), которые никогда не являются допустимыми. После того как мы разобрали программу согласно грамматике, необходимо запретить соответствующие недопустимые конструкции.
Спецификация делает это, добавляя следующие ограничения:
Статическая семантика: ранние ошибки
PrimaryExpression : CPEAAPL
Это синтаксическая ошибка, если
CPEAAPL
не покрываетParenthesizedExpression
.
При обработке экземпляра производства
PrimaryExpression : CPEAAPL
интерпретация
CPEAAPL
уточняется с использованием следующей грамматики:
ParenthesizedExpression : ( Expression )
Это означает: если CPEAAPL
появляется на месте PrimaryExpression
в синтаксическом дереве, это фактически ParenthesizedExpression
, и это его единственное допустимое производство.
Expression
никогда не может быть пустым, так что ( )
не является допустимым ParenthesizedExpression
. Списки, разделённые запятыми, такие как (1, 2, 3)
, создаются при помощи оператора запятая:
Expression :
AssignmentExpression
Expression , AssignmentExpression
Точно так же, если CPEAAPL
появляется на месте ArrowParameters
, применяются следующие ограничения:
Статическая семантика: ранние ошибки
ArrowParameters : CPEAAPL
Это синтаксическая ошибка, если
CPEAAPL
не покрываетArrowFormalParameters
.
Когда производство
ArrowParameters : CPEAAPL
распознаётся, используется следующая грамматика для уточнения интерпретации
CPEAAPL
:
ArrowFormalParameters :
( UniqueFormalParameters )
Другие покрытия грамматики
Помимо CPEAAPL
, спецификация использует покрытие грамматики для других конструкций, выглядящих неоднозначно.
ObjectLiteral
используется как покрытие грамматики для ObjectAssignmentPattern
, который появляется внутри списков параметров стрелочной функции. Это означает, что ObjectLiteral
допускает конструкции, которые не могут появляться внутри фактических объектных литералов.
ObjectLiteral :
...
{ PropertyDefinitionList }
PropertyDefinition :
...
CoverInitializedName
CoverInitializedName :
IdentifierReference Initializer
Initializer :
= AssignmentExpression
Например:
let o = { a = 1 }; // синтаксическая ошибка
// Стрелочная функция с параметром-деструктуризацией с значением по умолчанию:
// значение:
let f = ({ a = 1 }) => { return a; };
f({}); // возвращает 1
f({a : 6}); // возвращает 6
Асинхронные стрелочные функции также выглядят неоднозначно с конечным заглядыванием вперед:
let x = async(a,
Это вызов функции под названием async
или асинхронная стрелочная функция?
let x1 = async(a, b);
let x2 = async();
function async() { }
let x3 = async(a, b) => {};
let x4 = async();
Для этой цели грамматика определяет символ покрытия грамматики CoverCallExpressionAndAsyncArrowHead
, который работает аналогично CPEAAPL
.
Сводка
В этом эпизоде мы рассмотрели, как спецификация определяет покрывающие грамматики и использует их в случаях, когда мы не можем идентифицировать текущую синтаксическую конструкцию на основе конечного прогноза.
В частности, мы рассмотрели различие между списками параметров стрелочной функции и выражениями в скобках, а также то, как спецификация использует покрывающую грамматику для первоначального разрешающего анализа неоднозначно выглядящих конструкций и последующего ограничения их с помощью статических семантических правил.