# Глава 12. Лексический анализ _Лексический анализ_ описывает, как буквы и другие символы образуют «слова» в языке программирования в целом. Эти «слова» технически называются _лексемами_, и остальная часть конвейера преобразований компилятора работает с лексемами, а не с отдельными символами. Конвейер преобразований обычно называют _фазой трансляции_. Компилятор **Nim** использует следующие этапы трансляции: - 1 Лексический анализ: преобразование потока символов в поток лексем. - 2 Парсинг: преобразование потока лексем в абстрактное синтаксическое дерево (**AST**). - 3 Семантический анализ: преобразование **AST** в аннотированный **AST**. Аннотации в основном касаются типов, поскольку **Nim** — это язык со статической типизацией, и каждое выражение должно иметь тип. В этой главе подробно описан первый этап — лексический анализ. ## 12.1. Обозначения, используемые в этой главе Языковые конструкции объясняются с помощью _расширенной формы Бэкуса—Наура_ (**РБНФ**), в которой `a*` означает 0 или более элементов `a`, `a+` означает 1 или более элементов `a`, а `a?` означает необязательный элемент `a` (либо ни одного элемента `a`, либо один элемент `a`). Для группировки элементов могут использоваться скобки. `&` — это опережающий оператор; `&a` означает, что ожидается буква `a`, но она не используется. Символы `|` и `/` используются для обозначения альтернатив и имеют самый низкий приоритет. `/` — это упорядоченный выбор, который требует, чтобы парсер пробовал альтернативные варианты в указанном порядке. `/` часто используется для устранения неоднозначности грамматики. Нетерминалы начинаются со строчной буквы, абстрактные терминальные символы — с заглавной. Дословные терминальные символы (включая ключевые слова) заключаются в кавычки `'`. Пример: ```rbnf ifStmt = 'if' expr ':' stmts ('elif' expr ':' stmts)* ('else' stmts)? ``` Бинарный оператор `^*` используется как сокращение для обозначения 0 или более вхождений, разделенных вторым аргументом; аналогично `^''` означает 1 или более вхождений: `a^b` is short for `a (b a)*` and `a ^* b` это сокращение для `(a (b a)*)?`. Например: ```rbnf arrayConstructor = '[' expr ^* ',' ']' ``` Программа на языке **Nim** состоит из одного или нескольких текстовых исходных файлов, содержащих код на языке **Nim**. Текст должен быть закодирован в `UTF-8`. Грамматика языка **Nim** не привязана напрямую к входному тексту в кодировке `Unicode`. Вместо этого она основана на списке отдельных непересекающихся лексем. Лексема может быть отнесена к одной из следующих категорий: - Комментарий. - Идентификатор. - Ключевое слово, например `if` или `type`. - Литерал (символ, строка, целое число, число с плавающей запятой). - Оператор. - Разделитель, например (, `, ). (Сюда входят точка с запятой, запятая и различные типы скобок.) ## 12.2. Отступы Стандартная грамматика языка **Nim** описывает язык с чувствительностью к отступам. Это означает, что все управляющие конструкции распознаются по отступам. Отступы делаются _только с помощью пробелов_, **табуляция не допускается**. Правило «используй табуляцию для отступов, а пробелы — для выравнивания» никогда не работало достаточно хорошо в больших кодовых базах, а если и работало, то создавало дополнительные трудности для разработчиков. Без такого правила работать проще. Поскольку в языке **Nim** используется синтаксис на основе отступов и допускаются только пробелы, макет исходного кода совместим со всеми редакторами. Обработка отступов реализована следующим образом: лексер аннотирует следующий токен количеством пробелов, предшествующих ему; отступ не является отдельным токеном. Этот прием позволяет анализировать **Nim**, просматривая только один токен. Парсер использует стек уровней отступа: стек состоит из целых чисел, соответствующих количеству пробелов. Информация об отступе запрашивается в стратегически важных местах парсера, но в остальных случаях игнорируется: Псевдотерминал `IND{>}` обозначает отступ, состоящий из большего количества пробелов, чем запись в верхней части стека; `IND{=}` обозначает отступ, состоящий из того же количества пробелов. `DED` — еще один псевдотерминал, обозначающий действие _извлечения_ значения из стека. Тогда `IND{>}` означает _помещение_ значения в стек. С помощью этих обозначений мы можем определить основу грамматики: блок операторов (упрощенный пример): ```rbnf ifStmt = 'if' expr ':' stmt (IND{=} 'elif' expr ':' stmt)* (IND{=} 'else' ':' stmt)? simpleStmt = ifStmt / ... stmt = IND{>} stmt ^+ IND{=} DED # список выражений / simpleStmt # или простое выражение ``` ## 12.3. Комментарии Комментарии начинаются с символа решетки (`#`) в любом месте за пределами строкового или символьного литерала. Комментарии состоят из конкатенации фрагментов комментария. Фрагмент комментария начинается с `#` и продолжается до конца строки. Символы конца строки относятся к фрагменту. Если следующая строка состоит только из фрагмента комментария и между ней и предыдущей строкой нет других токенов, она не начинает новый комментарий: ```nim i = 0 # Это единичный комментарий в конце строки". # Сканер убирёт всё это. # Другой комментарий и тоже будет удалён. ``` Комментарии в документации — это комментарии, которые начинаются с двух символов решетки (`##`). Комментарии в документации являются токенами и допускаются только в определенных местах входного файла, поскольку они относятся к синтаксическому дереву. ## 12.4. Многострочные комментарии Многострочный комментарий начинается с `#[` и заканчивается `]#`: ```nim #[Комментароий может быть разнесён на несколькострок.]# ``` Многострочные комментарии могут быть вложенными: ```nim #[ #[ Многострочный комментарий также может включать код. ]# proc p[T](x: T) = discard ]# ``` Существуют многострочные комментарии к документации, которые также поддерживают вложенность: ```nim proc foo = ##[Длинный документирующий комментарий помещается здесь. ]## ``` ## 12.5. Идентификаторы и ключевые слова Идентификаторы в языке **Nim** могут представлять собой любую последовательность букв, цифр и символов подчеркивания со следующими ограничениями: - Оно должно начинаться с буквы. - Оно _не должно заканчиваться_ символом подчеркивания `_`. - Два последовательных подчеркивания `__` _недопустимы_: ```rbnf letter ::= 'A'..'Z' | 'a'..'z' | '\x80'..'\xff' digit ::= '0'..'9' IDENTIFIER ::= letter ( ['_'] (letter | digit) )* ``` Символы **Юникода** с порядковым номером выше 127 (не входящие в **ASCII**) могут относиться как к буквам, так и к операторам. Подробности этой классификации здесь не рассматриваются, поскольку они могут измениться в будущем. Следующие ключевые слова являются зарезервированными и не могут использоваться в качестве идентификаторов: - addr and as asm - bind block break - case cast concept const continue converter - defer discard distinct div do - elif else end enum except export - finally for from func - if import in include interface is isnot iterator - let - macro method mixin mod - nil not notin - object of or out - proc ptr - raise ref return - shl shr static - template try tuple type - using - var - when while - xor - yield Некоторые ключевые слова в настоящее время не используются; они зарезервированы для будущих версий языка. ## 12.6. Равенство идентификаторов Два идентификатора считаются равными, если следующий алгоритм возвращает `true`: ```nim proc sameIdentifier(a, b: string): bool = a[0] == b[0] and a.replace("_", "").toLowerAscii == b.replace("_", "").toLowerAscii ``` Это означает, что только первые буквы сравниваются с учетом регистра. Остальные буквы сравниваются без учета регистра в диапазоне **ASCII**, а символы подчеркивания игнорируются. Это правило также применимо к ключевым словам, то есть `notin` — это то же самое, что `notIn` и `not_in`. Эксцентричные правила, касающиеся равенства идентификаторов, призваны обеспечить более разумный подход к именованию, при котором разные объекты имеют разные названия, а не только разное написание. Сравните `const ROOT = root(Root)` и `const RootUser = rootof(RootDir)`, чтобы оценить преимущества. Разницу в написании не так просто уловить на слух; как только двум или более разработчикам нужно обсудить свою кодовую базу, преимущества четко различимых имен становятся очевидными. ## 12.7. Строковые литералы Терминальный символ в грамматике: **STR_LIT**. Строковые литералы могут быть заключены в двойные кавычки и содержать следующие управляющие последовательности: **Таблица 1**. Управляющие последовательности для строковых литералов | Код | значение кода | | ------ | ---------------- | | \p | Специфичный для платформы символ перевода строки: `CRLF` в **Windows**, `LF` в **Unix** | | \r, \c | возврат каретки | | \n, \l | перевод строки (часто называемый новой строкой)| | \f | подача формы| | \t | табулятор| | \v | вертикальный табулятор| | \\ | обратная косая черта| | \" | кавычки| | \' | апостроф| | \'0'..'9'+| символ с десятичным значением d; все последующие десятичные цифры используются для символа | | \a | тревога (звонок)| | \b | бэкспейс (шаг назад, стирание)| | \e | отмена [ESC]| | \xHH | символ с шестнадцатеричным значением HH; допускаются только две шестнадцатеричные цифры| | \uHHHH | кодовая точка Юникода с шестнадцатеричным значением HHHH; допускаются ровно четыре шестнадцатеричные цифры| | \u{H+} | кодовая точка Юникода; все шестнадцатеричные цифры, заключенные в {}, используются для обозначения кодовой точки| Строки в языке **Nim** могут содержать любое 8-битное значение, даже встроенные нули. ## 12.8. Строковые литералы в тройных кавычках Терминальный символ в грамматике: `TRIPLESTR_LIT`. Строковые литералы также могут быть заключены в две тройные кавычки `""" ... """`. Литералы в таком виде могут занимать несколько строк, могут содержать `"` и не интерпретируют никакие управляющие последовательности. Для удобства, если за открывающей `"""` следует символ новой строки (между открывающей `"""` и символом новой строки могут быть пробелы), символ новой строки (и предшествующие пробелы) не включается в строку. Конец строкового литерала определяется шаблоном `"""[^"]`. Другими словами, вот так: ```nim """"Длинная строка с кавычками. """" ``` Производит: ```nim "Длинная строка с кавычками. " ``` ## 12.9. Необработанные строковые литералы Терминальный символ в грамматике: **RSTR_LIT**. Существуют также необработанные строковые литералы, перед которыми ставится буква `r` (или `R`). Они заключаются в соответствующие двойные кавычки (как и обычные строковые литералы) и не интерпретируют управляющие последовательности. Это особенно удобно для регулярных выражений или путей в **Windows**. ```nim # сырая строка, таким образом ``\t`` не табуляция var f = openFile(r"C:\texts\text.txt") ``` Чтобы в необработанном строковом литерале появился один символ `"`, его нужно продублировать: ```nim r"a""b" ``` Производит: ```nim a"b ``` `r""""` невозможно записать в таком виде, потому что три ведущие кавычки обозначают строковый литерал в тройных кавычках. `r"""` — это то же самое, что `"""`, поскольку строковые литералы в тройных кавычках также не интерпретируют управляющие последовательности. ## 12.10. Обобщенные необработанные строковые литералы Терминальные символы в грамматике: `GENERALIZED_STR_LIT`, `GENERALIZED_TRIPLESTR_LIT`. Идентификатор `имя"строковый литерал"` (без пробелов между именем и открывающей кавычкой) представляет собой обобщенный необработанный строковый литерал. Это сокращение от идентификатора конструкции (`r"строковый литерал"`), обозначающее вызов подпрограммы с необработанным строковым литералом в качестве единственного аргумента. Обобщенные необработанные строковые литералы особенно удобны для встраивания мини-языков непосредственно в **Nim** (например, регулярных выражений). Идентификатор `имя"""строковый литерал"""` тоже существует. Это сокращение от `identifier("""строковый литерал""")`. ## 12.11. Символьные литералы Символьные литералы заключаются в одинарные кавычки `'` и могут содержать те же управляющие последовательности, что и строки, за одним исключением: зависящая от платформы новая строка (`\p`) не допускается, так как она может состоять более чем из одного символа (это может быть пара символов `CR/LF`). Вот допустимые управляющие последовательности для символьных литералов: **Таблица 2**. Управляющие последовательности для символьных литералов | Код | Значение кода | | ------ | ---------------- | |\r, \c | возврат каретки | |\n, \l | перевод строки | |\f | подача формы | |\t | табулятор | |\v | вертикальный табулятор | |\\ | обратная косая черта| |\" | кавычки | |\' | апостроф | |\'0'..'9'+| символ с десятичным значением d; все последующие десятичные цифры используются для символа | |\a | nhtdjuf (pdjyjr)| |\b | забой| |\e | отмена [ESC]| |\xHH | символ с шестнадцатеричным значением HH; допускаются только две шестнадцатеричные цифры | Символ — это не символ Юникода, а отдельный байт. Символьный литерал, не заканчивающийся на `'`, интерпретируется как `'`, если ему предшествует обратный апостроф. Между обратным апострофом и символьным литералом не должно быть пробелов. Этот особый случай гарантирует, что объявление вида `proc 'customLiteral'(s: string)` правильное, а `proc 'customLiteral'(s: string)` будет таким же, как `proc '\''customLiteral'(s: string)`. См. также раздел 12.12.1 «Пользовательские числовые литералы». ## 12.12. Числовые литералы Числовые литералы имеют следующий вид: ```rbnf hexdigit = digit | 'A'..'F' | 'a'..'f' octdigit = '0'..'7' bindigit = '0'..'1' unary_minus = '-' # See the section about unary minus HEX_LIT = unary_minus? '0' ('x' | 'X' ) hexdigit ( ['_'] hexdigit )* DEC_LIT = unary_minus? digit ( ['_'] digit )* OCT_LIT = unary_minus? '0' 'o' octdigit ( ['_'] octdigit )* BIN_LIT = unary_minus? '0' ('b' | 'B' ) bindigit ( ['_'] bindigit )* INT_LIT = HEX_LIT | DEC_LIT | OCT_LIT | BIN_LIT INT8_LIT = INT_LIT ['\''] ('i' | 'I') '8' INT16_LIT = INT_LIT ['\''] ('i' | 'I') '16' INT32_LIT = INT_LIT ['\''] ('i' | 'I') '32' INT64_LIT = INT_LIT ['\''] ('i' | 'I') '64' UINT_LIT = INT_LIT ['\''] ('u' | 'U') UINT8_LIT = INT_LIT ['\''] ('u' | 'U') '8' UINT16_LIT = INT_LIT ['\''] ('u' | 'U') '16' UINT32_LIT = INT_LIT ['\''] ('u' | 'U') '32' UINT64_LIT = INT_LIT ['\''] ('u' | 'U') '64' exponent = ('e' | 'E' ) ['+' | '-'] digit ( ['_'] digit )* FLOAT_LIT = unary_minus? digit (['_'] digit)* (('.' digit (['_'] digit)*[exponent]) |exponent) FLOAT32_SUFFIX = ('f' | 'F') ['32'] FLOAT32_LIT = HEX_LIT '\'' FLOAT32_SUFFIX | (FLOAT_LIT | DEC_LIT | OCT_LIT | BIN_LIT) ['\''] FLOAT32_SUFFIX FLOAT64_SUFFIX = ( ('f' | 'F') '64' ) | 'd' | 'D' FLOAT64_LIT = HEX_LIT '\'' FLOAT64_SUFFIX | (FLOAT_LIT | DEC_LIT | OCT_LIT | BIN_LIT) ['\''] FLOAT64_SUFFIX CUSTOM_NUMERIC_LIT = (FLOAT_LIT | INT_LIT) '\'' CUSTOM_NUMERIC_SUFFIX # CUSTOM_NUMERIC_SUFFIX is any Nim identifier that is not # a pre-defined type suffix. ``` Как видно из примеров, числовые литералы могут содержать символы подчеркивания для удобства чтения. Целочисленные литералы и литералы с плавающей запятой могут быть представлены в десятичной (без префикса), двоичной (префикс `0b`), восьмеричной (префикс `0o`) и шестнадцатеричной (префикс `0x`) системах счисления. Тот факт, что унарный минус `-` в числовом литерале, например `-1`, считается частью литерала, появился в языке довольно поздно. Это связано с тем, что выражение `-128'i8` должно быть допустимым, а без этого особого случая это было бы невозможно — `128` не является допустимым значением `int8`, только `-128`. Для правила `unary_minus` существуют дополнительные ограничения, которые не отражены в формальной грамматике. Чтобы - было частью числового литерала, непосредственно предшествующий ему символ должен входить в набор {' ', '\t', '\n', '\r', ',', ';', '(', '[', '{'}. В следующих примерах `-1` — это один токен: ```nim echo -1 echo(-1) echo [-1] echo 3,-1 "abc";-1 ``` В следующих примерах `-1` разбивается на два отдельных токена (как `-` и `1`): ```nim echo x-1 echo (int)-1 echo [a]-1 "abc"-1 ``` Суффикс, начинающийся с апострофа (`'`), называется _суффиксом типа_. Литералы без суффикса типа имеют целочисленный тип, за исключением случаев, когда литерал содержит точку или `E|e`, — тогда он имеет тип `float`. Этот целочисленный тип является типом `int`, если литерал находится в диапазоне `low(int32)..high(int32)`, в противном случае это тип `int64`. Для удобства записи апостроф в суффиксе типа необязателен, если он не приводит к неоднозначности (неоднозначными могут быть только шестнадцатеричные литералы с плавающей запятой и суффиксом типа). Предустановленные суффиксы типов: **Таблица 3**. Предустановленные суффиксы типов | Тип суффикса | Результирующий литерал| | ------------ | --------------------- | |'i8 | int8 | |'i16 | int16 | |'i32 | int32 | |'i64 | int64 | |'u | uint | |'u8 | uint8 | |'u16 | uint16 | |'u32 | uint32 | |'u64 | uint64 | |'f | float32 | |'d | float64 | |'f32 | float32 | |'f64 | float64 | Литералы должны соответствовать типу данных. Например, `333'i8` — недопустимый литерал. Литералы, не основанные на десятичной системе счисления, используются в основном для флагов и представления битовых шаблонов, поэтому проверка выполняется по разрядности, а не по диапазону значений. Следовательно: `0b10000000'u8 == 0x80'u8 == 128`, но: `0b10000000'i8 == 0x80'i8 == -1`, а не вызывает ошибку переполнения. ## 12.12.1. Пользовательские числовые литералы Если суффикс не является предопределенным, то предполагается, что он представляет собой вызов процедуры, шаблона, макроса или другого вызываемого идентификатора, которому передается строка, содержащая литерал. Вызываемый идентификатор должен быть объявлен со специальным префиксом `'`: ```nim import strutils type u4 = distinct uint8 # 4-битное беззнаковое целое "нибл" proc `'u4`(n: string): u4 = # Требуется лидирующая кавычка `'` в апострофах result = (parseInt(n) and 0x0F).u4 var x = 5'u4 ``` Если говорить более формально, пользовательский числовой литерал `123'custom` на этапе синтаксического анализа преобразуется в `r"123".'custom`. Этому преобразованию не соответствует ни один тип узла **AST**. Преобразование естественным образом обрабатывает случай, когда вызываемому объекту передаются дополнительные параметры: ```nim import strutils type u4 = distinct uint8 # 4-битное беззнаковое целое "нибл" proc `'u4`(n: string; moreData: int): u4 = result = (parseInt(n) and 0x0F).u4 var x = 5'u4(123) ``` Пользовательские числовые литералы описываются грамматическим правилом `CUSTOM_NUMERIC_LIT`. Пользовательский числовой литерал представляет собой один токен. ## 12.13. Операторы В **Nim** можно использовать пользовательские операторы. На самом деле в **Nim** нет разницы между пользовательскими и встроенными операторами. Когда мы пишем `1 + 2`, это вызов оператора сложения `+(1, 2)`, который подлежит разрешению перегрузки. Модуль `system` автоматически импортируется в каждую программу на **Nim** и предлагает `func \(a, b: int): int`, поэтому вызов будет разрешен как `system.+(1, 2)`. Оператор — это любая комбинация следующих символов: ```text = + - * / < > @ $ ~ & % | ! ? ^ . : \ ``` (В грамматике используется терминал `OPR` для обозначения символов операторов, как указано здесь.) Эти ключевые слова также являются операторами: and or not xor shl shr div mod in notin is isnot of as from. `.`, `=`, `:`, `::` недоступны в качестве общих операторов; они используются для других целей. `*:` в качестве особого случая рассматривается как два токена `*` и `:` (для поддержки `var v*: T`). Ключевое слово `not` всегда является унарным оператором, `a not b` анализируется как `a(not b)`, а не как `(a) not (b)`. ## 12.14. Другие токены Следующие строки обозначают другие токены: ```text ` ( ) { } [ ] , ; [. .] {. .} (. .) [: ``` Оператор среза `..` имеет приоритет над другими токенами, содержащими точку: `{..}` — это три токена: `{` и `..` и `}`, а не два: `{.` и `.}`. ## 12.15. Операторы Юникода Эти операторы **Юникода** также распознаются как операторы: ```nim ∙ ∘ × ★ ⊗ ⊘ ⊙ ⊛ ⊠ ⊡ ∩ ∧ # такой же приоритет, как у * (умножение) ± ⊕ ⊖ ⊞ ⊟ ∪ ∨ # тот же приоритет, что и у + (сложение) ``` Операторы **Юникода** можно комбинировать с символами операторов, не входящими в **Юникод**. При этом применяются обычные расширения приоритета, например `×=` — это оператор присваивания, как и `*=`. Этап нормализации Unicode не выполняется.