Перейти к основному содержимому

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

· 11 мин. чтения
[Марья Хёлтта](https://twitter.com/marjakh), наблюдатель за спекулятивными спецификациями

Все эпизоды

В этом эпизоде мы углубимся в определение языка ECMAScript и его синтаксис. Если вы не знакомы с контекстно-свободными грамматиками, сейчас самое время изучить основы, так как спецификация использует контекстно-свободные грамматики для определения языка. Ознакомьтесь с главой о контекстно-свободных грамматиках в "Crafting Interpreters" для доступного введения или со страницей Википедии для более математического определения.

Грамматики ECMAScript

Спецификация ECMAScript определяет четыре грамматики:

Лексическая грамматика описывает, как кодовые точки Unicode преобразуются в последовательность входных элементов (токенов, символов окончания строки, комментариев, пробелов).

Синтаксическая грамматика определяет, как синтаксически правильные программы состоят из токенов.

Грамматика регулярных выражений описывает, как кодовые точки Unicode преобразуются в регулярные выражения.

Грамматика числовых строк описывает, как строки преобразуются в числовые значения.

Каждая грамматика определяется как контекстно-свободная грамматика, состоящая из набора продукций.

Грамматики используют немного различную нотацию: синтаксическая грамматика использует LeftHandSideSymbol :, тогда как лексическая грамматика и грамматика регулярных выражений используют LeftHandSideSymbol ::, а грамматика числовых строк — LeftHandSideSymbol :::.

Далее мы подробнее рассмотрим лексическую грамматику и синтаксическую грамматику.

Лексическая грамматика

Спецификация определяет исходный текст ECMAScript как последовательность кодовых точек Unicode. Например, имена переменных не ограничены символами ASCII, но могут также включать другие символы Unicode. Спецификация не говорит о фактическом кодировании (например, UTF-8 или UTF-16). Предполагается, что исходный код уже был преобразован в последовательность кодовых точек Unicode согласно использованному кодированию.

Невозможно заранее токенизировать исходный код ECMAScript, что делает определение лексической грамматики немного сложнее.

Например, мы не можем определить, является ли / оператором деления или началом регулярного выражения, не посмотрев на больший контекст, в котором он встречается:

const x = 10 / 5;

Здесь / является DivPunctuator.

const r = /foo/;

Здесь первый / является началом RegularExpressionLiteral.

Шаблоны вводят схожую неоднозначность — интерпретация }` зависит от контекста, в котором она встречается:

const what1 = 'temp';
const what2 = 'late';
const t = `Я временный ${ what1 + what2 }`;

Здесь `Я временный ${ является TemplateHead, а }`TemplateTail.

if (0 == 1) {
}`не очень полезно`;

Здесь } является RightBracePunctuator, а ` — началом NoSubstitutionTemplate.

Хотя интерпретация / и }` зависит от их «контекста» — их положения в синтаксической структуре кода — грамматики, которые мы рассмотрим далее, все равно являются контекстно-свободными.

Лексическая грамматика использует несколько символов цели, чтобы различать контексты, где некоторые входные элементы разрешены, а некоторые — нет. Например, символ цели InputElementDiv используется в контекстах, где / — это деление, а /= — это присваивание с делением. Продукции InputElementDiv перечисляют возможные токены, которые могут быть созданы в этом контексте:

InputElementDiv ::
WhiteSpace
LineTerminator
Comment
CommonToken
DivPunctuator
RightBracePunctuator

В данном контексте встреча с / создаёт входной элемент DivPunctuator. Создание RegularExpressionLiteral здесь невозможно.

С другой стороны, InputElementRegExp — это символ цели для контекстов, где / является началом регулярного выражения:

InputElementRegExp ::
WhiteSpace
LineTerminator
Comment
CommonToken
RightBracePunctuator
RegularExpressionLiteral

Как мы видим из продукций, здесь возможно создание входного элемента RegularExpressionLiteral, но создание DivPunctuator невозможно.

Аналогично, существует другой символ цели, InputElementRegExpOrTemplateTail, для контекстов, где разрешены TemplateMiddle и TemplateTail, в дополнение к RegularExpressionLiteral. И, наконец, InputElementTemplateTail — это символ цели для контекстов, где разрешены только TemplateMiddle и TemplateTail, но RegularExpressionLiteral не разрешен.

В реализации синтаксический анализатор («парсер») может вызывать лексический анализатор («токенизатор» или «лексер»), передавая символ цели в качестве параметра и запрашивая следующий элемент ввода, подходящий для этого символа цели.

Синтаксическая грамматика

Мы рассмотрели лексическую грамматику, которая определяет, как мы создаем токены из кодовых точек Unicode. Синтаксическая грамматика строит на её основе: она определяет, как синтаксически корректные программы составляются из токенов.

Пример: Разрешение устаревших идентификаторов

Добавление нового ключевого слова в грамматику может привести к несовместимости — что если существующий код уже использует это ключевое слово как идентификатор?

Например, до того как await стал ключевым словом, кто-то мог написать следующий код:

function old() {
var await;
}

Грамматика ECMAScript осторожно добавила ключевое слово await таким образом, чтобы этот код продолжал работать. Внутри асинхронных функций await является ключевым словом, поэтому следующий код не работает:

async function modern() {
var await; // Синтаксическая ошибка
}

Аналогично, разрешение использования yield как идентификатора в негенераторах и запрет в генераторах работает так же.

Чтобы понять, как await разрешён как идентификатор, нужно разобраться в специфической для ECMAScript нотации синтаксической грамматики. Давайте разберёмся с этим прямо сейчас!

Продукции и сокращения

Давайте посмотрим, как определяются продукции для VariableStatement. На первый взгляд грамматика может выглядеть немного пугающе:

VariableStatement[Yield, Await] :
var VariableDeclarationList[+In, ?Yield, ?Await] ;

Что означают индексы ([Yield, Await]) и префиксы (+ в +In и ? в ?Async)?

Нотация объяснена в разделе Grammar Notation.

Индексы — это сокращение для выражения набора продукций для набора символов слева сразу. Левый символ имеет два параметра, которые расширяются в четыре "реальных" символа слева: VariableStatement, VariableStatement_Yield, VariableStatement_Await и VariableStatement_Yield_Await.

Заметьте, что здесь простой VariableStatement означает «VariableStatement без _Await и _Yield». Это не следует путать с VariableStatement[Yield, Await].

На правой стороне продукции мы видим сокращение +In, что означает "использовать версию с _In", и ?Await, что означает «использовать версию с _Await, если и только если символ слева имеет _Await» (аналогично с ?Yield).

Третье сокращение, ~Foo, означающее «использовать версию без _Foo», не используется в этой продукции.

С этой информацией мы можем расширить продукции вот так:

VariableStatement :
var VariableDeclarationList_In ;

VariableStatement_Yield :
var VariableDeclarationList_In_Yield ;

VariableStatement_Await :
var VariableDeclarationList_In_Await ;

VariableStatement_Yield_Await :
var VariableDeclarationList_In_Yield_Await ;

В конечном итоге нам нужно узнать две вещи:

  1. Где решается, используем ли мы случай с _Await или без _Await?
  2. Где это имеет значение — где продукции для Something_Await и Something (без _Await) расходятся?

_Await или без _Await?

Давайте сначала рассмотрим вопрос 1. Легко догадаться, что асинхронные и синхронные функции различаются тем, используем ли мы параметр _Await для тела функции или нет. Читая продукции для объявлений асинхронных функций, мы находим это:

AsyncFunctionBody :
FunctionBody[~Yield, +Await]

Заметьте, что у AsyncFunctionBody нет параметров — они добавляются к FunctionBody на правой стороне.

Если мы расширим эту продукцию, мы получим:

AsyncFunctionBody :
FunctionBody_Await

Другими словами, асинхронные функции имеют FunctionBody_Await, то есть тело функции, где await рассматривается как ключевое слово.

С другой стороны, если мы внутри обычной функции, релевантная продукция такова:

FunctionDeclaration[Yield, Await, Default] :
function BindingIdentifier[?Yield, ?Await] ( FormalParameters[~Yield, ~Await] ) { FunctionBody[~Yield, ~Await] }

FunctionDeclaration есть еще одна продукция, но она не относится к нашему примеру кода.)

Чтобы избежать комбинаторного расширения, давайте проигнорируем параметр Default, который не используется в данной продукции.

Расширенная форма продукции такова:

FunctionDeclaration :
function BindingIdentifier ( FormalParameters ) { FunctionBody }

FunctionDeclaration_Yield :
function BindingIdentifier_Yield ( FormalParameters ) { FunctionBody }

FunctionDeclaration_Await :
function BindingIdentifier_Await ( FormalParameters ) { FunctionBody }

FunctionDeclaration_Yield_Await :
function BindingIdentifier_Yield_Await ( FormalParameters ) { FunctionBody }

В этой конструкции мы всегда получаем FunctionBody и FormalParameters (без _Yield и _Await), так как они параметризованы [~Yield, ~Await] в неразвёрнутой конструкции.

Имя функции рассматривается по-разному: оно получает параметры _Await и _Yield, если символ с левой стороны их имеет.

Подытожим: асинхронные функции имеют FunctionBody_Await, а неасинхронные функции имеют FunctionBody (без _Await). Так как мы говорим о нефункциях-генераторах, как наш пример асинхронной функции, так и наш пример неасинхронной функции параметризованы без _Yield.

Может быть сложно запомнить, какая функция является FunctionBody, а какая FunctionBody_Await. FunctionBody_Await применяется для функции, где await это идентификатор, или где await это ключевое слово?

Вы можете думать о параметре _Await как о значении "await является ключевым словом". Этот подход также подходит для будущих изменений. Представьте новое ключевое слово blob, добавленное только внутри "blob" функций. Не-blob не-асинхронные не-генераторы всё равно будут иметь FunctionBody (без _Await, _Yield или _Blob), точно так же, как сейчас. Blob функции будут иметь FunctionBody_Blob, асинхронные blob функции будут иметь FunctionBody_Await_Blob и так далее. Нам всё равно придётся добавить Blob как нижний индекс в конструкции, но развернутые формы FunctionBody для уже существующих функций останутся такими же.

Запрет использования await как идентификатора

Теперь нам нужно выяснить, как await запрещено как идентификатор, если мы находимся внутри FunctionBody_Await.

Мы можем проследить дальнейшие конструкции, чтобы увидеть, что параметр _Await остаётся неизменным от FunctionBody до конструкции VariableStatement, которую мы рассматриваем.

Таким образом, внутри асинхронной функции у нас будет VariableStatement_Await, а внутри неасинхронной функции у нас будет VariableStatement.

Мы можем продолжать следить за конструкциями и отслеживать параметры. Мы уже видели конструкцию VariableStatement:

VariableStatement[Yield, Await] :
var VariableDeclarationList[+In, ?Yield, ?Await] ;

Все конструкции для VariableDeclarationList просто передают параметры как есть:

VariableDeclarationList[In, Yield, Await] :
VariableDeclaration[?In, ?Yield, ?Await]

(Здесь мы показываем только конструкцию, относящуюся к нашему примеру.)

VariableDeclaration[In, Yield, Await] :
BindingIdentifier[?Yield, ?Await] Initializer[?In, ?Yield, ?Await] opt

Сокращение opt означает, что символ справа является необязательным; в действительности существует две конструкции: одна с необязательным символом, и одна без.

В простом случае, относящемся к нашему примеру, VariableStatement состоит из ключевого слова var, за которым следует один BindingIdentifier без инициализатора и заканчивается точкой с запятой.

Чтобы запретить или разрешить await как BindingIdentifier, мы стремимся получить что-то вроде этого:

BindingIdentifier_Await :
Identifier
yield

BindingIdentifier :
Identifier
yield
await

Это запретит использование await как идентификатора внутри асинхронных функций и разрешит его использование внутри неасинхронных функций.

Но спецификация не определяет это таким образом, вместо этого мы находим эту конструкцию:

BindingIdentifier[Yield, Await] :
Identifier
yield
await

В развернутом виде конструкция выглядит следующим образом:

BindingIdentifier_Await :
Identifier
yield
await

BindingIdentifier :
Identifier
yield
await

(Мы опускаем конструкции для BindingIdentifier_Yield и BindingIdentifier_Yield_Await, которые не нужны для нашего примера.)

На первый взгляд кажется, что await и yield всегда разрешено использовать как идентификаторы. Как так? Пост бесполезен?

Статические семантики приходят на помощь

Оказывается, нужны статические семантики, чтобы запретить использование await как идентификатора внутри асинхронных функций.

Статические семантики описывают статические правила — то есть, правила, которые проверяются до выполнения программы.

В данном случае статические семантики для BindingIdentifier определяют следующее правило, направленное на синтаксис:

BindingIdentifier[Yield, Await] : await

Это синтаксическая ошибка, если эта конструкция имеет параметр [Await].

Фактически это запрещает конструкцию BindingIdentifier_Await : await.

Спецификация объясняет, что причиной наличия этого производства, но определения его как синтаксической ошибки статическими семантиками является вмешательство автоматической вставки точек с запятой (ASI).

Помните, что ASI включается, когда мы не можем разобрать строку кода согласно грамматическим правилам. ASI пытается добавить точки с запятой, чтобы удовлетворить требование, что заявления и объявления должны заканчиваться точкой с запятой. (Мы опишем ASI подробнее в следующем эпизоде.)

Рассмотрим следующий код (пример из спецификации):

async function too_few_semicolons() {
let
await 0;
}

Если грамматика запрещала await как идентификатор, ASI включился бы и преобразовал код в следующий грамматически правильный код, который также использует let как идентификатор:

async function too_few_semicolons() {
let;
await 0;
}

Этот вид вмешательства с ASI был признан слишком запутанным, поэтому статические семантики были использованы для запрета await как идентификатора.

Запрещенные StringValues идентификаторов

Есть еще одно связанное правило:

BindingIdentifier : Identifier

Это синтаксическая ошибка, если у этого производства есть параметр [Await], а StringValue идентификатора равен "await".

Это может показаться запутанным сначала. Identifier определен следующим образом:

Identifier :
IdentifierName but not ReservedWord

await — это ReservedWord, так как же Identifier может быть await?

Как выясняется, Identifier не может быть await, но он может быть чем-то другим, чтобы его StringValue был равен "await" — другим представлением последовательности символов await.

Статические семантики для имен идентификаторов определяют, как вычисляется StringValue имени идентификатора. Например, Unicode escape-последовательность для a — это \u0061, так что \u0061wait имеет StringValue "await". \u0061wait не будет распознан как ключевое слово лексической грамматикой, вместо этого он будет Identifier. Статические семантики запрещают использовать его как имя переменной внутри асинхронных функций.

Итак, это работает:

function old() {
var \u0061wait;
}

А это — нет:

async function modern() {
var \u0061wait; // Syntax error
}

Резюме

В этом эпизоде мы ознакомились с лексической грамматикой, синтаксической грамматикой и сокращениями, используемыми для определения синтаксической грамматики. В качестве примера мы изучили запрет использования await как идентификатора внутри асинхронных функций, но разрешили его использование внутри неасинхронных функций.

Другие интересные части синтаксической грамматики, такие как автоматическая вставка точек с запятой и покрывающие грамматики, будут рассмотрены в следующем эпизоде. Оставайтесь с нами!