Перейти к основному содержимому
Chapter content StyleChapter Difficulty18 min

«Language is a city to the building of which every human being brought a stone».

Ralph Waldo Emerson,

«Letters and Social Aims», 18751

Глава 3.2: Грамматика — Свобода vs Сложность

Грамматика языка спецификации за всё своё время существования претерпела много изменений. Язык ECMAScript развивался от версии к версии. С момента выпуска первой версии спецификации в 1997 году, которая умещалась чуть более, чем на 100 страницах, в данный момент это число перевалило за 800, то есть материала стало в 8 раз больше.

Первая версия спецификации многого не имела, если сравнивать с текущим прогрессом ECMAScript, но выбор в пользу описания языка через formal language был сделан уже тогда.

У спецификации того времени была своя "атмосфера". Если сравнивать с последней версией, там, в первой, всё было куда проще. Многое было вынесено в общие блоки с кратким описанием, а бич на бесконтрольные сокращения в угоду простоты написания редакторами, а не понимания обычными людьми, ещё не получил серьёзного развития.

Всё пришло к тому, что написанием спецификации занялись люди, далёкие от разработки. Уверен, они, скорее всего, являются хорошими лингвистами или учёными, но это не отменяет того факта, что описанием языка занимаются люди, которым как будто не приходилось применять его в серьёзных условиях, для себя. Такие документы должны писаться согласно модели "разработчик — разработчику"2.

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

Виды грамматик

Как было описано ранее, грамматика языка спецификации ECMAScript разделена на несколько подграмматик. Каждая из них устанавливает свои правила реализации граммматических обозначений, то есть реализует свои требования и ограничения на то, что она подразумевает под общими терминами context-free grammar. Например, terminal symbols в разных грамматиках определяются по-разному.

Всего на страницах спецификации по одному общему приципу реализовано 4 вида частных грамматик:

Давайте приступим к внимательному рассмотрению каждой из них. Заодно попутно попытаемся ответить на вопрос: что лучше — Свобода vs Сложность?

warning

Далее будет происходить переплетение языка ECMAScript с языком спецификации.

Lexical grammar

Эта грамматика является основополагающей во всём языке. Она помогает разбивать нашу программу по смысловым частям — по сути, придаёт смысл нашим написанным буквам и цифрам в коде. Именно она вступает в игру, когда необходимо определить, какую сущность языка мы тут написали, будь то идентификатор (Identifiers), цикл while (WhileStatement) или комментарий (Comments). И эта грамматика отвечает не за правильность их расположения относительно друг друга, а только за распознавание из всего нашего кода тех или иных конструкций языка, прописанных в нём, согласно спецификации.

Она призвана распознавать и различать сущности языка на, так называемые, неделимые лексические единицы (indivisible lexical units), особенно когда один и тот же символ в разных грамматических контекстах обозначает разные смысловые конструкции, например, знак "+" может быть унарным оператором (UnaryExpression) или знаком сложения двух частей (AdditiveExpression).

В основе этой грамматики лежит некоторый Unicode code point — кодовое значение символа Unicode. Именно из таких символов состоит написанный программистами код или, согласно Lexical grammar, исходный текст (source text). В рамках этой грамматики code point — это уже знакомый нам terminal symbol. Вот как выглядит code point в спецификации:

White Space Code Points table
White Space Code Points table

Итак, source text определяется как последовательность code points. Как и в обычном "предложении", code points собираются в "слова", названные в грамматике input elements или уже известные нам nonterminal symbols.

Так, уже в рамках другой грамматики — Syntactic grammar — наши input elements формируют token-ы. То есть, буквально, токен while (цикл while в JavaScript) побуквенно или, согласно Lexical grammar, по каждому code point был собран в input element while, который также является token-ом в рамках уже Syntactic grammar, отвечающей за синтаксис.

Идентифицировать грамматику можно по двойному двоеточию"::" (two colons). На картинке ниже изображены 2 productions в Lexical grammar:

Lexical Grammar productions
Lexical Grammar productions
Обращаю внимание

Может показаться, что одни и те же термины являются одинаковыми для разных грамматик. Это не так! Каждой грамматике самой дозволено определять, что она считает terminal symbol, а что — token.

Выше Вы увидели, что такое переплетение двух грамматик. while для Lexical grammar — это последовательность Unicode code points, то есть input element, который по совместительству является и nonterminal, а для Syntactic grammar этот же самый while — это token или terminal symbol.

Parsing

То есть если рассматривать на реальном примере парсинга кода программы, наш source text, который в алгоритмах определяется спецификацией в рамках Script или Module, согласно Lexical grammar, на этом этапе разбивается на последовательность input elements. Делается это во время многократного сканирования source text слева направо в поисках максимальной последовательности code points, чтобы определить её, как token, и занести в условный список распознанных input elements.

Немаловажными элементами в процессе сканирования кода являются разделители строк (Line Terminators), пробелы (White Space) и комментарии (Comments). Разделители строк не являются токенами, но попадают в input elements, в отличие от пробелов и однострочных комментариев. А вот многострочный комментарий (MultiLineComment) имеет свою отдельную проверку.

Особое место занимает операция автоматической расстановки оператора-пунктуатора (Punctuators) ";" или Automatic Semicolon Insertion.

Как итог, можно сказать, что Lexical grammar служит для всех остальных ориентиром. Без неё не будет работать ни одна другая грамматика, потому что именно она определяет, что Вы имели в виду, когда писали тот или иной символ.

RegExp grammar

Об этой грамматике стоит лишь сказать, что её отделили от Lexical grammar в целях разделения ответственности за предмет своего описания — регулярные выражения (regular expressions). Грамматика описывает, как последовательности code points преобразуются в паттерны регулярных выражений.

RegExp и Lexical grammars имеют много общего; даже на уровне спецификации они определяются вместе в общем блоке и имеют почти одинаковые обозначения грамматических терминов, типа terminal symbols и других. Идентифицировать первую можно также по двум двоеточиям "::" (two colons), что можно увидеть на картинке ниже:

RegExp grammar production
RegExp grammar production

Более детальный разбор грамматики в текущем формате документации будет опущен.

Numeric String grammar

Это ещё одна дополнительная грамматика, являющаяся попросту инкапсулированным вариантом Lexical grammar для определения частных форм операции приведения типов в языке ECMAScript — конвертация primitive value типа String в primitive value общего типа Numeric type: Number или BigInt.

Вот примеры определения Numeric String grammar, идентифицирующиеся с помощью трёх двоиточий ":::" (three colons):

Numeric String grammar production
Numeric String grammar production

Более детальный разбор грамматики в текущем формате документации будет опущен.

Syntactic grammar

Последняя по счёту, но не по значению, эта грамматика является равной по важности Lexical grammar, без которой тоже невозможно представить ни один язык. Как мы определили выше, если первая даёт нам возможность распознать токены, то эта грамматика уже определяет, как последовательности токенов формируют синтаксически правильные независимые компоненты программы.

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

В основе этой грамматики лежит token, здесь он же и terminal symbol. Выше мы уже рассмотрели один из таких токенов — while. Вот как он в рамках грамматики выглядит в спецификации, с разделением на две части с помощью одного двоеточия ":" (just one colon):

The While Statement syntax production
The While Statement syntax production

Остальные сущности на картинке нам уже знакомы из общей, базовой грамматики. Так что предлагаю рассмотреть особенности работы Syntactic grammar.

Parsing

Сначала проводится сканирование кода по типу, указанному в Lexical grammar, чтобы вычленить из source code некие input elements в отдельный stream, среди которых будут tokens и Line terminators. Затем, согласно спецификации, для этого stream однократно будет произведён парсинг в рамках уже рассматриваемой Syntactic grammar на наличие синтаксических ошибок. Они могут быть найдены только в том случае, если в момент соотнесения input elements из stream с nonterminal symbols грамматики будут замечены лишние незадействованные tokens.

После успешного парсинга, что логично, должен быть представлен какой-то результат. Согласно спецификации, создаётся некое parse tree — корневая древовидная структура, ответвления которого формируют Parse Node. Такое строение достаточно просто описывается тем, что каждая отдельная сущность (instance) предствляет собой nonterminal с соотнемённым куском source text, а в основании parse tree — весь представленный source text.

Согласно спецификации, важно помнить что:

Parse Nodes are specification artefacts, and implementations are not required to use an analogous data structure.

~ tc39

Кругом одни Yield / Await

Так уж вышло, что с выходом новых версий языка вводились и новые функции. Это привело к появлению новых ключевых зарезервированных слов (Keywords and Reserved Words), которые разработчики раньше могли использовать в своих программах в качестве идентификаторов.

В целях обратной совместимости, чтобы сохранить работоспособность всего написанного ранее кода, была придумана машинерия по аккуратному добавлению новых ключевых слов в язык. На страницах спецификации это реализовали самым неудобным способом, из-за чего теперь почти в каждом production представлены два интересных параметра [Yield, Await].

Вывод

Давайте подытожим всё вышесказанное:

  1. На страницах спецификации живут и властвуют 4 вида грамматик, выполняющие свои обособленные роли: Lexical, RegExp, Numeric String и Syntactic grammar.
  2. Lexical grammar: определяет, как Unicode code points преобразуются в input elements.
  3. RegExp grammar: определяет, как Unicode code points преобразуются в паттерны регулярных выражений
  4. Numeric String grammar: опредеяет, как строки преобразуются в числовые значения.
  5. Syntactic grammar: определяет, как последовательности токенов формируют синтаксически правильные независимые компоненты программы.
  6. Рассуждая над тем, каков же итог нашего раунда — Свобода vs Сложность — понятно одно: язык спецификации написан свободно, что удобно для редактирования, и сложно, что плохо для восприятия.

Footnotes

  1. Так Ральф Эмерсон написал в своей книге "Letters and Social Aims" в 1875 году. Он был человеком, который на собственном опыте осознал, что значит "сделать себя самому". Признан общественностью глубочайшим умом Америки.

  2. Авторский термин. Автор уверен, что разработчики, как никто другие, знают, как правильно и понятно описать что-либо так, чтобы это понял другой разработчик.