javascript

Понимание спецификации ECMAScript, часть 4

  • вторник, 15 октября 2024 г. в 00:00:08
https://habr.com/ru/articles/850392/

Понимание спецификации ECMAScript, часть 4

Привет, Хабр! Представляю вашему вниманию перевод четвертой статьи автора Marja Hölttä из цикла Understanding ECMAScript. Перевод первой части. Перевод второй части. Перевод третьей части.

Тем временем в другой части Сети

Джейсон Орендорфф из Mozilla опубликовал прекрасный своей глубиной анализ синтаксических причуд JS. Несмотря на различия в деталях реализации, каждый движок JS сталкивается с одинаковыми проблемами, связанными с этими особенностями.

Cover грамматики (grammars)

В этом выпуске мы подробно рассмотрим cover grammars. Это способ указать грамматику для синтаксических конструкций, которые на первый взгляд выглядят неоднозначно.

Опять же, мы не будем использовать shorthands [In, Yield, Await] для краткости, поскольку они не важны для этого выпуска. Объяснение их значения и использования приведено в третьем выпуске.

Просмотр на конечное число токенов (finite lookahead)

Как правило, анализаторы (parsers) решают, какой нетерминал использовать, основываясь на finite lookahead (фиксированное количество последовательных токенов).

В некоторых случаях следующий токен однозначно определяет нетерминал для использования. Например:

UpdateExpression :
  LeftHandSideExpression
  LeftHandSideExpression ++
  LeftHandSideExpression --
  ++ UnaryExpression
  -- UnaryExpression

Если мы парсим UpdateExpression и следующий токен ++ или --, мы сразу поймем, какой нетерминал нужно использовать. Если следующий токен не является ни тем, ни другим, это все равно не так уж плохо: мы можем распарсить LeftHandSideExpression, начиная с позиции, в которой мы находимся, и решить, что делать после того, как мы его распарсим.

Если токен, следующий за выражением LeftHandSideExpression, равен ++, то используем нетерминал UpdateExpression : LeftHandSideExpression ++. Случай с -- аналогичен. И если токен, следующий за выражением LeftHandSideExpression, не является ни ++, ни --, мы используем нетерминал UpdateExpression : LeftHandSideExpression.

Список параметров стрелочной (arrow function) или выражение, заключенное в круглые скобки?

Отличить список аргументов 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, но наше предположение может оказаться неверным.

Новый чрезвычайно гибкий символ: CPEAAPL

Спецификация решает эту проблему, вводя символ 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 в нетерминалах

Теперь мы можем использовать вседозволяющий 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-ов

Как мы видели ранее, грамматика нетерминалов для 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.

  • Supplemental Syntax

    При обработке экземпляра нетерминала ArrowParameters : CPEAAPL, интерпретация CPEAAPL уточняется с помощью такой грамматики:

    ArrowFormalParameters :
    ( UniqueFormalParameters )
    

Другие cover grammars

В дополнение к 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.