Лексический анализ описывает, как буквы и другие символы образуют «слова» в языке программирования в целом. Эти «слова» технически называются лексемами, и остальная часть конвейера преобразований компилятора работает с лексемами, а не с отдельными символами. Конвейер преобразований обычно называют фазой трансляции.
Компилятор Nim использует следующие этапы трансляции:
В этой главе подробно описан первый этап — лексический анализ.
Языковые конструкции объясняются с помощью расширенной формы Бэкуса—Наура (РБНФ), в которой a* означает 0 или более элементов a, a+ означает 1 или более элементов a, а a? означает необязательный элемент a (либо ни одного элемента a, либо один элемент a). Для группировки элементов могут использоваться скобки.
& — это опережающий оператор; &a означает, что ожидается буква a, но она не используется.
Символы | и / используются для обозначения альтернатив и имеют самый низкий приоритет. / — это упорядоченный выбор, который требует, чтобы парсер пробовал альтернативные варианты в указанном порядке. / часто используется для устранения неоднозначности грамматики.
Нетерминалы начинаются со строчной буквы, абстрактные терминальные символы — с заглавной. Дословные терминальные символы (включая ключевые слова) заключаются в кавычки '. Пример:
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)*)?. Например:
arrayConstructor = '[' expr ^* ',' ']'
Программа на языке Nim состоит из одного или нескольких текстовых исходных файлов, содержащих код на языке Nim. Текст должен быть закодирован в UTF-8. Грамматика языка Nim не привязана напрямую к входному тексту в кодировке Unicode. Вместо этого она основана на списке отдельных непересекающихся лексем. Лексема может быть отнесена к одной из следующих категорий:
if или type.Стандартная грамматика языка Nim описывает язык с чувствительностью к отступам. Это означает, что все управляющие конструкции распознаются по отступам.
Отступы делаются только с помощью пробелов, табуляция не допускается. Правило «используй табуляцию для отступов, а пробелы — для выравнивания» никогда не работало достаточно хорошо в больших кодовых базах, а если и работало, то создавало дополнительные трудности для разработчиков. Без такого правила работать проще. Поскольку в языке Nim используется синтаксис на основе отступов и допускаются только пробелы, макет исходного кода совместим со всеми редакторами.
Обработка отступов реализована следующим образом: лексер аннотирует следующий токен количеством пробелов, предшествующих ему; отступ не является отдельным токеном. Этот прием позволяет анализировать Nim, просматривая только один токен.
Парсер использует стек уровней отступа: стек состоит из целых чисел, соответствующих количеству пробелов. Информация об отступе запрашивается в стратегически важных местах парсера, но в остальных случаях игнорируется: Псевдотерминал IND{>} обозначает отступ, состоящий из большего количества пробелов, чем запись в верхней части стека; IND{=} обозначает отступ, состоящий из того же количества пробелов. DED — еще один псевдотерминал, обозначающий действие извлечения значения из стека. Тогда IND{>} означает помещение значения в стек.
С помощью этих обозначений мы можем определить основу грамматики: блок операторов (упрощенный пример):
ifStmt = 'if' expr ':' stmt
(IND{=} 'elif' expr ':' stmt)*
(IND{=} 'else' ':' stmt)?
simpleStmt = ifStmt / ...
stmt = IND{>} stmt ^+ IND{=} DED # список выражений
/ simpleStmt # или простое выражение
Комментарии начинаются с символа решетки (#) в любом месте за пределами строкового или символьного литерала. Комментарии состоят из конкатенации фрагментов комментария. Фрагмент комментария начинается с # и продолжается до конца строки. Символы конца строки относятся к фрагменту. Если следующая строка состоит только из фрагмента комментария и между ней и предыдущей строкой нет других токенов, она не начинает новый комментарий:
i = 0 # Это единичный комментарий в конце строки".
# Сканер убирёт всё это.
# Другой комментарий и тоже будет удалён.
Комментарии в документации — это комментарии, которые начинаются с двух символов решетки (##). Комментарии в документации являются токенами и допускаются только в определенных местах входного файла, поскольку они относятся к синтаксическому дереву.
Многострочный комментарий начинается с #[ и заканчивается ]#:
#[Комментароий может быть
разнесён
на несколько строк.]#
Многострочные комментарии могут быть вложенными:
#[ #[ Многострочный комментарий
также может включать код. ]#
proc p[T](x: T) = discard
]#
Существуют многострочные комментарии к документации, которые также поддерживают вложенность:
proc foo =
##[Длинный документирующий комментарий
помещается здесь.
]##
Идентификаторы в языке Nim могут представлять собой любую последовательность букв, цифр и символов подчеркивания со следующими ограничениями:
_.Два последовательных подчеркивания __ недопустимы:
letter ::= 'A'..'Z' | 'a'..'z' | '\x80'..'\xff'
digit ::= '0'..'9'
IDENTIFIER ::= letter ( ['_'] (letter | digit) )*
Символы Юникода с порядковым номером выше 127 (не входящие в ASCII) могут относиться как к буквам, так и к операторам. Подробности этой классификации здесь не рассматриваются, поскольку они могут измениться в будущем.
Следующие ключевые слова являются зарезервированными и не могут использоваться в качестве идентификаторов:
Некоторые ключевые слова в настоящее время не используются; они зарезервированы для будущих версий языка.
Два идентификатора считаются равными, если следующий алгоритм возвращает true:
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), чтобы оценить преимущества. Разницу в написании не так просто уловить на слух; как только двум или более разработчикам нужно обсудить свою кодовую базу, преимущества четко различимых имен становятся очевидными.
Терминальный символ в грамматике: 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-битное значение, даже встроенные нули.
Терминальный символ в грамматике: TRIPLESTR_LIT.
Строковые литералы также могут быть заключены в две тройные кавычки """ ... """. Литералы в таком виде могут занимать несколько строк, могут содержать " и не интерпретируют никакие управляющие последовательности. Для удобства, если за открывающей """ следует символ новой строки (между открывающей """ и символом новой строки могут быть пробелы), символ новой строки (и предшествующие пробелы) не включается в строку. Конец строкового литерала определяется шаблоном """[^"]. Другими словами, вот так:
""""Длинная строка с кавычками. """"
Производит:
"Длинная строка с кавычками. "
Терминальный символ в грамматике: RSTR_LIT.
Существуют также необработанные строковые литералы, перед которыми ставится буква r (или R). Они заключаются в соответствующие двойные кавычки (как и обычные строковые литералы) и не интерпретируют управляющие последовательности. Это особенно удобно для регулярных выражений или путей в Windows.
# сырая строка, таким образом ``\t`` не табуляция
var f = openFile(r"C:\texts\text.txt")
Чтобы в необработанном строковом литерале появился один символ ", его нужно продублировать:
r"a""b"
Производит:
a"b
r"""" невозможно записать в таком виде, потому что три ведущие кавычки обозначают строковый литерал в тройных кавычках. r""" — это то же самое, что """, поскольку строковые литералы в тройных кавычках также не интерпретируют управляющие последовательности.
Терминальные символы в грамматике: GENERALIZED_STR_LIT, GENERALIZED_TRIPLESTR_LIT.
Идентификатор имя"строковый литерал" (без пробелов между именем и открывающей кавычкой) представляет собой обобщенный необработанный строковый литерал.
Это сокращение от идентификатора конструкции (r"строковый литерал"), обозначающее вызов подпрограммы с необработанным строковым литералом в качестве единственного аргумента. Обобщенные необработанные строковые литералы особенно удобны для встраивания мини-языков непосредственно в Nim (например, регулярных выражений).
Идентификатор имя"""строковый литерал""" тоже существует. Это сокращение от identifier("""строковый литерал""").
Символьные литералы заключаются в одинарные кавычки ' и могут содержать те же управляющие последовательности, что и строки, за одним исключением: зависящая от платформы новая строка (\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 «Пользовательские числовые литералы».
Числовые литералы имеют следующий вид:
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 — это один токен:
echo -1
echo(-1)
echo [-1]
echo 3,-1
"abc";-1
В следующих примерах -1 разбивается на два отдельных токена (как - и 1):
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, а не вызывает ошибку переполнения.
Если суффикс не является предопределенным, то предполагается, что он представляет собой вызов процедуры, шаблона, макроса или другого вызываемого идентификатора, которому передается строка, содержащая литерал. Вызываемый идентификатор должен быть объявлен со специальным префиксом ':
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. Преобразование естественным образом обрабатывает случай, когда вызываемому объекту передаются дополнительные параметры:
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. Пользовательский числовой литерал представляет собой один токен.
В Nim можно использовать пользовательские операторы. На самом деле в Nim нет разницы между пользовательскими и встроенными операторами. Когда мы пишем 1 + 2, это вызов оператора сложения +(1, 2), который подлежит разрешению перегрузки. Модуль system автоматически импортируется в каждую программу на Nim и предлагает func \(a, b: int): int, поэтому вызов будет разрешен как system.+(1, 2).
Оператор — это любая комбинация следующих символов:
= + - * / < >
@ $ ~ & % |
! ? ^ . : \
(В грамматике используется терминал 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).
Следующие строки обозначают другие токены:
` ( ) { } [ ] , ; [. .] {. .} (. .) [:
Оператор среза .. имеет приоритет над другими токенами, содержащими точку: {..} — это три токена: { и .. и }, а не два: {. и .}.
Эти операторы Юникода также распознаются как операторы:
∙ ∘ × ★ ⊗ ⊘ ⊙ ⊛ ⊠ ⊡ ∩ ∧ # такой же приоритет, как у * (умножение)
± ⊕ ⊖ ⊞ ⊟ ∪ ∨ # тот же приоритет, что и у + (сложение)
Операторы Юникода можно комбинировать с символами операторов, не входящими в Юникод. При этом применяются обычные расширения приоритета, например ×= — это оператор присваивания, как и *=.
Этап нормализации Unicode не выполняется.