Spec-Zone.ru › Nim

Справочник по Nim

Исходный кодИзменить

"Сложность" похожа на "энергию": её можно передать от конечного пользователя одному или нескольким другим участникам, но общее количество, по-видимому, остаётся практически постоянным для данной задачи. -- Ран

Об этой документации

Примечание: Данный документ — черновик! Некоторым функциям Nim может потребоваться более точная формулировка. Этот справочник постоянно развивается в полноценную спецификацию.

Примечание: Экспериментальные возможности Nim описаны здесь.

Примечание: Присваивания, перемещения и уничтожение описаны в документе уничтожители.

В этом документе описываются лексика, синтаксис и семантика языка Nim.

Чтобы узнать, как компилировать программы Nim и генерировать документацию, см. Руководство пользователя компилятора и Руководство по инструменту DocGen.

Конструкции языка объяснены с помощью расширенного BNF, в котором (a)* означает 0 или более a, a+ означает 1 или более a, а (a)? означает необязательное a. Скобки могут использоваться для группировки элементов.

& — оператор предпросмотра; &a означает, что a ожидается, но не потребляется. Он будет потреблён в следующем правиле.

Символы |, / используются для обозначения альтернатив и имеют наименьший приоритет. / — упорядоченный выбор, который требует от парсера попробовать альтернативы в заданном порядке. / часто используется для обеспечения недвусмысленности грамматики.

Нетерминальные символы начинаются с маленькой буквы, абстрактные терминальные символы — заглавными. Буквальные терминальные символы (включая ключевые слова) заключены в кавычки '. Пример:

ifStmt = 'if' expr ':' stmts ('elif' expr ':' stmts)* ('else' stmts)?

Бинарный оператор ^* используется как сокращение для 0 или более вхождений, разделённых его вторым аргументом; аналогично ^+ означает 1 или более вхождений: a ^+ b является сокращением для a (b a)*, а a ^* b — сокращением для (a (b a)*)?. Пример:

arrayConstructor = '[' expr ^* ',' ']'

Другие части Nim, такие как правила области видимости или семантика выполнения, описаны неформально.

Определения

Код Nim задаёт вычисление, которое действует на памяти, состоящей из компонентов, называемых локациями. Переменная — это, по сути, имя для локации. Каждая переменная и локация имеют определённый тип. Тип переменной называется статическим типом, а тип локации — динамическим типом. Если статический тип не совпадает с динамическим типом, то он является супертипом или подтипом динамического типа.

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

Выражение задаёт вычисление, которое производит значение или локацию. Выражения, которые производят локации, называются l-значениями. L-значение может обозначать либо локацию, либо значение, содержащееся в локации, в зависимости от контекста.

Программа Nim программа состоит из одного или нескольких текстовых исходных файлов, содержащих код Nim. Она обрабатывается компилятором Nim компилятором в исполняемый файл. Характер этого исполняемого файла зависит от реализации компилятора; это может быть, например, нативный двоичный файл или исходный код JavaScript.

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

Компилятор парсит исходный код Nim в внутреннюю структуру данных, называемую абстрактным синтаксическим деревом (AST). Затем, прежде чем выполнять код или компилировать его в исполняемый файл, он преобразует AST через семантический анализ. Это добавляет семантическую информацию, такую как типы выражений, значения идентификаторов и, в некоторых случаях, значения выражений. Ошибка, обнаруженная во время семантического анализа, называется статической ошибкой. Ошибки, описанные в этом руководстве, являются статическими ошибками, если не указано иное.

Ошибка паника — это ошибка, которая обнаруживается и сообщается реализацией во время выполнения. Способ отчёта об этих ошибках — через выбрасывание исключений или прекращение выполнения с фатальной ошибкой. Тем не менее, реализация предоставляет возможность отключения этих проверок во время выполнения. См. раздел Директивы для получения подробной информации.

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

var a: array[0..1, char]
let i = 5
try:
  a[i] = 'N'
except IndexDefect:
  echo "invalid index"

Текущая реализация позволяет переключаться между различными поведением через --panics:on|off. Если паники включены, программа прекращает работу с ошибкой паники, если выключены — ошибки времени выполнения преобразуются в исключения. Преимущество --panics:on заключается в том, что он генерирует более компактный двоичный код, а компилятор получает больше свободы для оптимизации кода.

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

Выражение константное выражение — это выражение, значение которого может быть вычислено во время семантического анализа кода, в котором оно появляется. Оно никогда не является l-значением и никогда не имеет побочных эффектов. Константные выражения не ограничены возможностями семантического анализа, такими как свёртка констант; они могут использовать все возможности языка Nim, поддерживаемые для выполнения во время компиляции. Поскольку константные выражения могут использоваться в качестве входных данных для семантического анализа (например, для определения границ массивов), эта гибкость требует от компилятора чередовать семантический анализ и выполнение кода во время компиляции.

Семантический анализ, в основном, происходит сверху вниз и слева направо в исходном коде, а выполнение кода во время компиляции вставляется по мере необходимости для вычисления значений, необходимых для последующего семантического анализа. Мы увидим много позже в этом документе, что вызов макроса не только требует такого чередования, но и создаёт ситуацию, когда семантический анализ не полностью проходит сверху вниз и слева направо.

Лексический анализ

Кодировка

Все исходные файлы Nim используют кодировку UTF-8 (или её подмножество ASCII). Другие кодировки не поддерживаются. Любые стандартные последовательности завершения строки платформы могут быть использованы — форма Unix с использованием ASCII LF (перевод строки), форма Windows с использованием последовательности ASCII CR LF (возврат каретки, за которым следует перевод строки) или старая форма Macintosh с использованием символа ASCII CR (возврат каретки). Все эти формы могут быть использованы одинаково независимо от платформы.

Отступы

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

Обработка отступов реализована следующим образом: лексер добавляет предыдущее количество пробелов к следующему токену; отступ не является отдельным токеном. Этот приём позволяет парсить Nim с только 1 токеном предпросмотра.

Парсер использует стек уровней отступов: стек состоит из целых чисел, подсчитывающих пробелы. Информация об отступе запрашивается в стратегических местах парсера, но в противном случае игнорируется: псевдотерминал IND{>} обозначает отступ, состоящий из большего количества пробелов, чем запись вверху стека; IND{=} — отступ с тем же количеством пробелов. DED — ещё один псевдотерминал, который описывает действие извлечения значения из стека, IND{>} затем подразумевает добавление в стек.

С помощью этой нотации мы можем легко определить основу грамматики: блок инструкций (упрощённый пример):

ifStmt = 'if' expr ':' stmt
         (IND{=} 'elif' expr ':' stmt)*
         (IND{=} 'else' ':' stmt)?

simpleStmt = ifStmt / ...

stmt = IND{>} stmt ^+ IND{=} DED  # list of statements
     / simpleStmt                 # or a simple statement

Комментарии

Комментарии начинаются везде за пределами строковых или символьных литералов с символом решётки #. Комментарии состоят из конкатенации компонент комментариев. Компонент комментария начинается с # и продолжается до конца строки. Символы конца строки принадлежат компоненту. Если следующая строка состоит только из компонента комментария без других токенов между ним и предыдущим, он не начинает новый комментарий:

i = 0     # This is a single comment over multiple lines.
  # The lexer merges these two pieces.
  # The comment continues here.

Комментарии документации — это комментарии, которые начинаются с двух ##. Комментарии документации являются токенами; они допускаются только в определённых местах в исходном файле, так как они принадлежат синтаксическому дереву.

Многострочные комментарии

Начиная с версии 0.13.0 языка Nim поддерживает многострочные комментарии. Они выглядят следующим образом:

#[Comment here.
Multiple lines
are not a problem.]#

Многострочные комментарии поддерживают вложение:

#[  #[ Multiline comment in already
   commented out code. ]#
proc p[T](x: T) = discard
]#

Также существуют многострочные комментарии документации, которые также поддерживают вложение:

proc foo =
  ##[Long documentation comment
     here.
  ]##

Вы также можете использовать оператор discard вместе с тройными кавычками для создания многострочных комментариев:

discard """ You can have any Nim code text commented
out inside this with no indentation restrictions.
      yes("May I ask a pointless question?") """

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

Идентификаторы и ключевые слова

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

  • начинается с буквы
  • не заканчивается подчеркиванием _
  • два последовательных подчеркивания __ не допускаются:

    letter ::= 'A'..'Z' | 'a'..'z' | '\x80'..'\xff'
    digit ::= '0'..'9'
    IDENTIFIER ::= letter ( ['_'] (letter | digit) )*

В настоящее время любой символ Юникода с порядковым значением > 127 (не-ASCII) классифицируется как letter и может быть частью идентификатора, но в более поздних версиях языка некоторые символы Юникода могут быть отнесены к операторным символам.

Следующие ключевые слова зарезервированы и не могут использоваться в качестве идентификаторов:

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

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

Равенство идентификаторов

Два идентификатора считаются равными, если следующий алгоритм возвращает true:

proc sameIdentifier(a, b: string): bool =
  a[0] == b[0] and
    a.replace("_", "").toLowerAscii == b.replace("_", "").toLowerAscii

Это означает, что только первые буквы сравниваются в регистрозависимом режиме. Другие буквы сравниваются без учёта регистра в диапазоне ASCII, а подчеркивания игнорируются.

Этот довольно необычный способ сравнения идентификаторов называется частичной регистронезависимостью и имеет некоторые преимущества перед обычной регистрозависимостью:

Он позволяет программистам в основном использовать свой предпочтительный стиль написания, будь то humpStyle или snake_style, и библиотеки, написанные разными программистами, не могут использовать несовместимые соглашения. Редактор или IDE, поддерживающие Nim, могут отображать идентификаторы в соответствии с предпочтениями. Другое преимущество заключается в том, что программист освобождается от необходимости запоминать точное написание идентификатора. Исключение в отношении первой буквы позволяет разбор такого кода, как var foo: Foo, быть однозначным.

Обратите внимание, что это правило также применяется к ключевым словам, что означает, что notin эквивалентно notIn и not_in (всестрочная версия (notin, isnot) является предпочтительным способом написания ключевых слов).

Исторически Nim был полностью нечувствительным к стилю языком. Это означало, что он был регистронезависимым, подчеркивания игнорировались, и не было даже различия между foo и Foo.

Ключевые слова как идентификаторы

Если ключевое слово заключено в обратные кавычки, оно теряет свой статус ключевого слова и становится обычным идентификатором.

Примеры

var `var` = "Hello Stropping"
type Obj = object
  `type`: int

let `object` = Obj(`type`: 9)
assert `object` is Obj
assert `object`.`type` == 9

var `var` = 42
let `let` = 8
assert `var` + `let` == 50

const `assert` = true
assert `assert`

Строковые литералы

Терминальный символ в грамматике: STR_LIT.

Строковые литералы могут быть ограничены соответствующими двойными кавычками и могут содержать следующие последовательности escape:

Последовательность escape Значение
\p платформозависимая новая строка: CRLF в Windows, LF в Unix
\r, \c возврат каретки
\n, \l перевод строки (часто называется новой строкой)
\f формат страницы
\t табуляция
\v вертикальная табуляция
\\ обратная косая черта
\" двойная кавычка
\' одинарная кавычка
\ '0'..'9'+ символ с десятичным значением d; все десятичные цифры, непосредственно следующие за ним, используются для символа
\a сигнал
\b ввод назад
\e escape [ESC]
\x HH символ с шестнадцатеричным значением HH; допускается ровно две шестнадцатеричные цифры
\u HHHH код Юникода с шестнадцатеричным значением HHHH; допускается ровно четыре шестнадцатеричные цифры
\u {H+} код Юникода; все шестнадцатеричные цифры, заключённые в {}, используются для кода

Строки в Nim могут содержать любое 8-битное значение, включая вставленные нули. Однако некоторые операции могут интерпретировать первый двоичный ноль как терминатор.

Строковые литералы с тройными кавычками

Терминальный символ в грамматике: TRIPLESTR_LIT.

Строковые литералы также могут быть ограничены тремя двойными кавычками """ ... """. Литералы в этой форме могут занимать несколько строк, могут содержать " и не интерпретируют никакие escape-последовательности. Для удобства, если открывающая """ следует за новой строкой (между открывающей """ и новой строкой могут быть пробелы), новая строка (и предшествующие пробелы) не включаются в строку. Окончание строкового литерала определяется шаблоном """[^"], поэтому это:

""""long string within quotes""""

Производит:

"long string within quotes"

Сырые строковые литералы

Терминальный символ в грамматике: RSTR_LIT.

Также существуют сырые строковые литералы, которые предваряются буквой r (или R) и ограничены соответствующими двойными кавычками (как и обычные строковые литералы) и не интерпретируют escape-последовательности. Это особенно удобно для регулярных выражений или путей Windows:

var f = openFile(r"C:\texts\text.txt") # a raw string, so ``\t`` is no tab

Чтобы получить одиночную " внутри сырого строкового литерала, она должна быть удвоена:

r"a""b"

Производит:

a"b

r"""" невозможна с этой нотацией, потому что три ведущие кавычки вводят строковый литерал с тройными кавычками. r""" эквивалентно """, поскольку строковые литералы с тройными кавычками также не интерпретируют escape-последовательности.

Обобщённые сырые строковые литералы

Терминальные символы в грамматике: GENERALIZED_STR_LIT, GENERALIZED_TRIPLESTR_LIT.

Конструкции identifier"string literal" (без пробелов между идентификатором и открывающей кавычкой) — обобщённый сырой строковый литерал. Он является сокращением для конструкции identifier(r"string literal"), поэтому обозначает вызов процедуры со строковым литералом как единственным аргументом. Обобщённые сырые строковые литералы особенно удобны для встраивания мини-языков непосредственно в Nim (например, регулярных выражений).

Конструкция identifier"""string literal""" тоже существует. Она является сокращением для identifier("""string literal""").

Символьные литералы

Символьные литералы заключены в одинарные кавычки '' и могут содержать те же escape-последовательности, что и строки, — с одним исключением: платформа-зависимая новая строка (\p) не допускается, так как она может быть шире одного символа (может быть парой CR/LF). Вот допустимые escape-последовательности для символьных литералов:

Последовательность escape Значение
\r, \c возврат каретки
\n, \l перевод строки
\f формат страницы
\t табуляция
\v вертикальная табуляция
\\ обратная косая черта
\" двойная кавычка
\' одинарная кавычка
\ '0'..'9'+ символ с десятичным значением d; все десятичные цифры, непосредственно следующие за ним, используются для символа
\a сигнал
\b ввод назад
\e escape [ESC]
\x HH символ с шестнадцатеричным значением HH; допускается ровно две шестнадцатеричные цифры

Символ не является символом Юникода, а представляет собой один байт.

Обоснование: Это позволяет эффективно поддерживать array[char, int] или set[char].

Тип Rune может представлять любой символ Юникода. Rune объявлен в модуле unicode.

Символьный литерал, не заканчивающийся ', интерпретируется как ', если перед ним стоит токен обратной кавычки. Между предшествующим токеном обратной кавычки и символьным литералом не должно быть пробелов. Это специальный случай, гарантирующий, что объявление, подобное proc `'customLiteral`(s: string), является корректным. proc `'customLiteral`(s: string) эквивалентно proc `'\''customLiteral`(s: string).

См. также настраиваемые числовые литералы.

Числовые литералы

Числовые литералы имеют вид:

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. Для удобства обозначений апостроф суффикса типа является необязательным, если он не неоднозначен (только шестнадцатеричные литералы с плавающей точкой и суффиксом типа могут быть неоднозначными).

Предопределённые суффиксы типа:

Суффикс типа Результат типа литерала
'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

Литералы с плавающей точкой также могут быть в двоичной, восьмеричной или шестнадцатеричной записи: 0B0_10001110100_0000101001000111101011101111111011000101001101001001'f64 приблизительно равен 1.72826e35 в соответствии со стандартом IEEE с плавающей точкой.

Литералы должны соответствовать типу данных, например, 333'i8 — это недопустимый литерал. Недесятичные литералы используются в основном для флагов и представлений битовых шаблонов, поэтому проверка выполняется по ширине бита, а не по диапазону значений. Следовательно: 0b10000000'u8 == 0x80'u8 == 128, но 0b10000000'i8 == 0x80'i8 == -1 вместо того, чтобы вызывать ошибку переполнения.

Пользовательские числовые литералы

Если суффикс не предопределён, то суффикс предполагается вызовом процедуры, шаблона, макроса или другого вызываемого идентификатора, которому передаётся строка, содержащая литерал. Вызываемый идентификатор необходимо объявить с использованием специального ' префикса:

import std/strutils
type u4 = distinct uint8 # a 4-bit unsigned integer aka "nibble"
proc `'u4`(n: string): u4 =
  # The leading ' is required.
  result = (parseInt(n) and 0x0F).u4

var x = 5'u4

Более формально, пользовательский числовой литерал 123'custom преобразуется в r"123".'custom на этапе анализа. Не существует типа узла AST, соответствующего этому преобразованию. Преобразование естественным образом обрабатывает случай, когда вызываемому передаются дополнительные параметры:

import std/strutils
type u4 = distinct uint8 # a 4-bit unsigned integer aka "nibble"
proc `'u4`(n: string; moreData: int): u4 =
  result = (parseInt(n) and 0x0F).u4

var x = 5'u4(123)

Пользовательские числовые литералы охватываются правилом грамматики под названием CUSTOM_NUMERIC_LIT. Пользовательский числовой литерал — это единственный токен.

Операторы

Nim допускает определение операторов пользователем. Оператор — это любая комбинация следующих символов:

=     +     -     *     /     <     >
@     $     ~     &     %     |
!     ?     ^     .     :     \

(В грамматике используется терминал 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-операторы

Эти Unicode-операторы также анализируются как операторы:

∙ ∘ × ★ ⊗ ⊘ ⊙ ⊛ ⊠ ⊡ ∩ ∧ ⊓   # same priority as * (multiplication)
± ⊕ ⊖ ⊞ ⊟ ∪ ∨ ⊔             # same priority as + (addition)

Unicode-операторы могут быть объединены с символами операторов без Unicode. Тогда применяются обычные расширения приоритета, например, ⊠= — это оператор присваивания, подобно тому, как *=.

Никакая нормализация Unicode не выполняется.

Другие токены

Следующие строки обозначают другие токены:

`   (    )     {    }     [    ]    ,  ;   [.    .]  {.   .}  (.  .)  [:

Оператор срезов .. имеет приоритет над другими токенами, содержащими точку: {..} — это три токена {, .., }, а не два токена {., .}.

Синтаксис

В этом разделе перечислены стандартный синтаксис Nim. Способ обработки отступов анализатором уже описан в разделе Лексический анализ.

Nim позволяет определять операторы пользователем. Бинарные операторы имеют 11 различных уровней приоритета.

Ассоциативность

Бинарные операторы, первый символ которых является ^, являются правоассоциативными, все остальные бинарные операторы являются левоассоциативными.

proc `^/`(x, y: float): float =
  # a right-associative division operator
  result = x / y
echo 12 ^/ 4 ^/ 8 # 24.0 (4 / 8 = 0.5, then 12 / 0.5 = 24.0)
echo 12  / 4  / 8 # 0.375 (12 / 4 = 3.0, then 3 / 8 = 0.375)

Приоритет

Унарные операторы всегда связываются сильнее, чем любой бинарный оператор: $a + b это ($a) + b, а не $(a + b).

Если первый символ унарного оператора является @, то это оператор, подобный сигилу, который связывается сильнее, чем primarySuffix: @x.abc анализируется как (@x).abc, тогда как $x.abc анализируется как $(x.abc).

Для бинарных операторов, которые не являются ключевыми словами, приоритет определяется следующими правилами:

Операторы, оканчивающиеся на ->, ~> или =>, называются операторами, подобными стрелкам, и имеют самый низкий приоритет из всех операторов.

Если оператор заканчивается на =, и его первый символ не является ни <, ни >, ни !, ни =, ни ~, ни ?, это оператор присваивания со вторым по величине приоритетом.

В противном случае приоритет определяется первым символом.

Уровень приоритета Операторы Первый символ Терминальный символ
10 (наивысший) $ ^ OP10
9 * / div mod shl shr % * % \ / OP9
8 + - + - ~ | OP8
7 & & OP7
6 .. . OP6
5 == <= < >= > != in notin is isnot not of as from = < > ! OP5
4 and OP4
3 or xor OP3
2 @ : ? OP2
1 оператор присваивания (например, +=, *=) OP1
0 (наименьший) оператор, подобный стрелке (например, ->, =>) OP0

То, используется ли оператор как префиксный оператор, также зависит от предшествующего пробела (это изменение парсинга было введено с версией 0.13.0):

echo $foo
# is parsed as
echo($foo)

Пробелы также определяют, анализируется ли (a, b) как список аргументов вызова, или же как конструктор кортежа:

echo(1, 2) # pass 1 and 2 to echo
echo (1, 2) # pass the tuple (1, 2) to echo

Операторы, подобные точкам

Терминальный символ в грамматике: DOTLIKEOP.

Операторы, подобные точкам, — это операторы, начинающиеся с ., но не с .., например, .?; они имеют тот же приоритет, что и ., поэтому a.?b.c анализируется как (a.?b).c, а не как a.?(b.c).

Грамматика

Начальный символ грамматики — module.

# This file is generated by compiler/parser.nim.
module = complexOrSimpleStmt ^* (';' / IND{=})
comma = ',' COMMENT?
semicolon = ';' COMMENT?
colon = ':' COMMENT?
colcom = ':' COMMENT?
operator =  OP0 | OP1 | OP2 | OP3 | OP4 | OP5 | OP6 | OP7 | OP8 | OP9
         | 'or' | 'xor' | 'and'
         | 'is' | 'isnot' | 'in' | 'notin' | 'of' | 'as' | 'from'
         | 'div' | 'mod' | 'shl' | 'shr' | 'not' | '..'
prefixOperator = operator
optInd = COMMENT? IND?
optPar = (IND{>} | IND{=})?
simpleExpr = arrowExpr (OP0 optInd arrowExpr)* pragma?
arrowExpr = assignExpr (OP1 optInd assignExpr)*
assignExpr = orExpr (OP2 optInd orExpr)*
orExpr = andExpr (OP3 optInd andExpr)*
andExpr = cmpExpr (OP4 optInd cmpExpr)*
cmpExpr = sliceExpr (OP5 optInd sliceExpr)*
sliceExpr = ampExpr (OP6 optInd ampExpr)*
ampExpr = plusExpr (OP7 optInd plusExpr)*
plusExpr = mulExpr (OP8 optInd mulExpr)*
mulExpr = dollarExpr (OP9 optInd dollarExpr)*
dollarExpr = primary (OP10 optInd primary)*
operatorB = OP0 | OP1 | OP2 | OP3 | OP4 | OP5 | OP6 | OP7 | OP8 | OP9 |
            'div' | 'mod' | 'shl' | 'shr' | 'in' | 'notin' |
            'is' | 'isnot' | 'not' | 'of' | 'as' | 'from' | '..' | 'and' | 'or' | 'xor'
symbol = '`' (KEYW|IDENT|literal|(operator|'('|')'|'['|']'|'{'|'}'|'=')+)+ '`'
       | IDENT | 'addr' | 'type' | 'static'
symbolOrKeyword = symbol | KEYW
exprColonEqExpr = expr ((':'|'=') expr
                       / doBlock extraPostExprBlock*)?
exprEqExpr = expr ('=' expr
                  / doBlock extraPostExprBlock*)?
exprList = expr ^+ comma
optionalExprList = expr ^* comma
exprColonEqExprList = exprColonEqExpr (comma exprColonEqExpr)* (comma)?
qualifiedIdent = symbol ('.' optInd symbolOrKeyword)?
setOrTableConstr = '{' ((exprColonEqExpr comma)* | ':' ) '}'
castExpr = 'cast' ('[' optInd typeDesc optPar ']' '(' optInd expr optPar ')') /
parKeyw = 'discard' | 'include' | 'if' | 'while' | 'case' | 'try'
        | 'finally' | 'except' | 'for' | 'block' | 'const' | 'let'
        | 'when' | 'var' | 'mixin'
par = '(' optInd
          ( &parKeyw (ifExpr / complexOrSimpleStmt) ^+ ';'
          | ';' (ifExpr / complexOrSimpleStmt) ^+ ';'
          | pragmaStmt
          | simpleExpr ( (doBlock extraPostExprBlock*)
                       | ('=' expr (';' (ifExpr / complexOrSimpleStmt) ^+ ';' )? )
                       | (':' expr (',' exprColonEqExpr     ^+ ',' )? ) ) )
          optPar ')'
literal = | INT_LIT | INT8_LIT | INT16_LIT | INT32_LIT | INT64_LIT
          | UINT_LIT | UINT8_LIT | UINT16_LIT | UINT32_LIT | UINT64_LIT
          | FLOAT_LIT | FLOAT32_LIT | FLOAT64_LIT
          | STR_LIT | RSTR_LIT | TRIPLESTR_LIT
          | CHAR_LIT | CUSTOM_NUMERIC_LIT
          | NIL
generalizedLit = GENERALIZED_STR_LIT | GENERALIZED_TRIPLESTR_LIT
identOrLiteral = generalizedLit | symbol | literal
               | par | arrayConstr | setOrTableConstr | tupleConstr
               | castExpr
tupleConstr = '(' optInd (exprColonEqExpr comma?)* optPar ')'
arrayConstr = '[' optInd (exprColonEqExpr comma?)* optPar ']'
primarySuffix = '(' (exprColonEqExpr comma?)* ')'
      | '.' optInd symbolOrKeyword ('[:' exprList ']' ( '(' exprColonEqExpr ')' )?)? generalizedLit?
      | DOTLIKEOP optInd symbolOrKeyword generalizedLit?
      | '[' optInd exprColonEqExprList optPar ']'
      | '{' optInd exprColonEqExprList optPar '}'
pragma = '{.' optInd (exprColonEqExpr comma?)* optPar ('.}' | '}')
identVis = symbol OPR?  # postfix position
identVisDot = symbol '.' optInd symbolOrKeyword OPR?
identWithPragma = identVis pragma?
identWithPragmaDot = identVisDot pragma?
declColonEquals = identWithPragma (comma identWithPragma)* comma?
                  (':' optInd typeDescExpr)? ('=' optInd expr)?
identColonEquals = IDENT (comma IDENT)* comma?
     (':' optInd typeDescExpr)? ('=' optInd expr)?)
tupleTypeBracket = '[' optInd (identColonEquals (comma/semicolon)?)* optPar ']'
tupleType = 'tuple' tupleTypeBracket
tupleDecl = 'tuple' (tupleTypeBracket /
    COMMENT? (IND{>} identColonEquals (IND{=} identColonEquals)*)?)
paramList = '(' declColonEquals ^* (comma/semicolon) ')'
paramListArrow = paramList? ('->' optInd typeDesc)?
paramListColon = paramList? (':' optInd typeDesc)?
doBlock = 'do' paramListArrow pragma? colcom stmt
routineExpr = ('proc' | 'func' | 'iterator') paramListColon pragma? ('=' COMMENT? stmt)?
routineType = ('proc' | 'iterator') paramListColon pragma?
forStmt = 'for' ((varTuple / identWithPragma) ^+ comma) 'in' expr colcom stmt
forExpr = forStmt
expr = (blockExpr
      | ifExpr
      | whenExpr
      | caseStmt
      | forExpr
      | tryExpr)
      / simpleExpr
simplePrimary = SIGILLIKEOP? identOrLiteral primarySuffix*
commandStart = &('`'|IDENT|literal|'cast'|'addr'|'type'|'var'|'out'|
                 'static'|'enum'|'tuple'|'object'|'proc')
primary = simplePrimary (commandStart expr (doBlock extraPostExprBlock*)?)?
        / operatorB primary
        / routineExpr
        / rawTypeDesc
        / prefixOperator primary
rawTypeDesc = (tupleType | routineType | 'enum' | 'object' |
                ('var' | 'out' | 'ref' | 'ptr' | 'distinct') typeDesc?)
                ('not' primary)?
typeDescExpr = (routineType / simpleExpr) ('not' primary)?
typeDesc = rawTypeDesc / typeDescExpr
typeDefValue = ((tupleDecl | enumDecl | objectDecl | conceptDecl |
                 ('ref' | 'ptr' | 'distinct') (tupleDecl | objectDecl))
               / (simpleExpr (exprEqExpr ^+ comma postExprBlocks?)?))
               ('not' primary)?
extraPostExprBlock = ( IND{=} doBlock
                     | IND{=} 'of' exprList ':' stmt
                     | IND{=} 'elif' expr ':' stmt
                     | IND{=} 'except' optionalExprList ':' stmt
                     | IND{=} 'finally' ':' stmt
                     | IND{=} 'else' ':' stmt )
postExprBlocks = (doBlock / ':' (extraPostExprBlock / stmt)) extraPostExprBlock*
exprStmt = simpleExpr postExprBlocks?
         / simplePrimary (exprEqExpr ^+ comma) postExprBlocks?
         / simpleExpr '=' optInd (expr postExprBlocks?)
importStmt = 'import' optInd expr
              ((comma expr)*
              / 'except' optInd (expr ^+ comma))
exportStmt = 'export' optInd expr
              ((comma expr)*
              / 'except' optInd (expr ^+ comma))
includeStmt = 'include' optInd expr ^+ comma
fromStmt = 'from' expr 'import' optInd expr (comma expr)*
returnStmt = 'return' optInd expr?
raiseStmt = 'raise' optInd expr?
yieldStmt = 'yield' optInd expr?
discardStmt = 'discard' optInd expr?
breakStmt = 'break' optInd expr?
continueStmt = 'continue' optInd expr?
condStmt = expr colcom stmt COMMENT?
           (IND{=} 'elif' expr colcom stmt)*
           (IND{=} 'else' colcom stmt)?
ifStmt = 'if' condStmt
whenStmt = 'when' condStmt
condExpr = expr colcom stmt optInd
        ('elif' expr colcom stmt optInd)*
         'else' colcom stmt
ifExpr = 'if' condExpr
whenExpr = 'when' condExpr
whileStmt = 'while' expr colcom stmt
ofBranch = 'of' exprList colcom stmt
ofBranches = ofBranch (IND{=} ofBranch)*
                      (IND{=} 'elif' expr colcom stmt)*
                      (IND{=} 'else' colcom stmt)?
caseStmt = 'case' expr ':'? COMMENT?
            (IND{>} ofBranches DED
            | IND{=} ofBranches)
tryStmt = 'try' colcom stmt &(IND{=}? 'except'|'finally')
           (IND{=}? 'except' optionalExprList colcom stmt)*
           (IND{=}? 'finally' colcom stmt)?
tryExpr = 'try' colcom stmt &(optInd 'except'|'finally')
           (optInd 'except' optionalExprList colcom stmt)*
           (optInd 'finally' colcom stmt)?
blockStmt = 'block' symbol? colcom stmt
blockExpr = 'block' symbol? colcom stmt
staticStmt = 'static' colcom stmt
deferStmt = 'defer' colcom stmt
asmStmt = 'asm' pragma? (STR_LIT | RSTR_LIT | TRIPLESTR_LIT)
genericParam = symbol (comma symbol)* (colon expr)? ('=' optInd expr)?
genericParamList = '[' optInd
  genericParam ^* (comma/semicolon) optPar ']'
pattern = '{' stmt '}'
indAndComment = (IND{>} COMMENT)? | COMMENT?
routine = optInd identVis pattern? genericParamList?
  paramListColon pragma? ('=' COMMENT? stmt)? indAndComment
commentStmt = COMMENT
section(RULE) = COMMENT? RULE / (IND{>} (RULE / COMMENT)^+IND{=} DED)
enumDecl = 'enum' optInd (symbol pragma? optInd ('=' optInd expr COMMENT?)? comma?)+
objectWhen = 'when' expr colcom objectPart COMMENT?
            ('elif' expr colcom objectPart COMMENT?)*
            ('else' colcom objectPart COMMENT?)?
objectBranch = 'of' exprList colcom objectPart
objectBranches = objectBranch (IND{=} objectBranch)*
                      (IND{=} 'elif' expr colcom objectPart)*
                      (IND{=} 'else' colcom objectPart)?
objectCase = 'case' declColonEquals ':'? COMMENT?
            (IND{>} objectBranches DED
            | IND{=} objectBranches)
objectPart = IND{>} objectPart^+IND{=} DED
           / objectWhen / objectCase / 'nil' / 'discard' / declColonEquals
objectDecl = 'object' ('of' typeDesc)? COMMENT? objectPart
conceptParam = ('var' | 'out' | 'ptr' | 'ref' | 'static' | 'type')? symbol
conceptDecl = 'concept' conceptParam ^* ',' (pragma)? ('of' typeDesc ^* ',')?
              &IND{>} stmt
typeDef = identVisDot genericParamList? pragma '=' optInd typeDefValue
            indAndComment?
varTupleLhs = '(' optInd (identWithPragma / varTupleLhs) ^+ comma optPar ')' (':' optInd typeDescExpr)?
varTuple = varTupleLhs '=' optInd expr
colonBody = colcom stmt postExprBlocks?
variable = (varTuple / identColonEquals) colonBody? indAndComment
constant = (varTuple / identWithPragma) (colon typeDesc)? '=' optInd expr indAndComment
bindStmt = 'bind' optInd qualifiedIdent ^+ comma
mixinStmt = 'mixin' optInd qualifiedIdent ^+ comma
pragmaStmt = pragma (':' COMMENT? stmt)?
simpleStmt = ((returnStmt | raiseStmt | yieldStmt | discardStmt | breakStmt
           | continueStmt | pragmaStmt | importStmt | exportStmt | fromStmt
           | includeStmt | commentStmt) / exprStmt) COMMENT?
complexOrSimpleStmt = (ifStmt | whenStmt | whileStmt
                    | tryStmt | forStmt
                    | blockStmt | staticStmt | deferStmt | asmStmt
                    | 'proc' routine
                    | 'method' routine
                    | 'func' routine
                    | 'iterator' routine
                    | 'macro' routine
                    | 'template' routine
                    | 'converter' routine
                    | 'type' section(typeDef)
                    | 'const' section(constant)
                    | ('let' | 'var' | 'using') section(variable)
                    | bindStmt | mixinStmt)
                    / simpleStmt
stmt = (IND{>} complexOrSimpleStmt^+(IND{=} / ';') DED)
     / simpleStmt ^+ ';'

Порядок вычисления

Порядок вычисления строго слева направо, внутри наружу, как это типично для большинства других императивных языков программирования:

var s = ""

proc p(arg: int): int =
  s.add $arg
  result = arg

discard p(p(1) + p(2))

doAssert s == "123"

Операции присваивания не являются специальными, выражение в левой части вычисляется перед выражением в правой части:

var v = 0
proc getI(): int =
  result = v
  inc v

var a, b: array[0..2, int]

proc someCopy(a: var int; b: int) = a = b

a[getI()] = getI()

doAssert a == [1, 0, 0]

v = 0
someCopy(b[getI()], getI())

doAssert b == [1, 0, 0]

Обоснование: Согласованность с перегруженными операциями присваивания или операциями, похожими на присваивание, a = b может быть прочитано как performSomeCopy(a, b).

Однако концепция «порядка вычисления» применима только после нормализации кода: нормализация включает в себя расширения шаблонов и переупорядочения аргументов, которые были переданы именованным параметрам:

var s = ""

proc p(): int =
  s.add "p"
  result = 5

proc q(): int =
  s.add "q"
  result = 3

# Evaluation order is 'b' before 'a' due to template
# expansion's semantics.
template swapArgs(a, b): untyped =
  b + a

doAssert swapArgs(p() + q(), q() - p()) == 6
doAssert s == "qppq"

# Evaluation order is not influenced by named parameters:
proc construct(first, second: int) =
  discard

# 'p' is evaluated before 'q'!
construct(second = q(), first = p())

doAssert s == "qppqpq"

Обоснование: Это намного проще реализовать, чем гипотетические альтернативы.

Константы и константные выражения

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

  • литералы
  • встроенные операторы
  • ранее объявленные константы и переменные времени компиляции
  • ранее объявленные макросы и шаблоны
  • ранее объявленные процедуры, не имеющие побочных эффектов, кроме, возможно, изменения переменных времени компиляции

Константное выражение может содержать блоки кода, которые могут внутренне использовать все возможности Nim, поддерживаемые во время компиляции (как подробно описано в следующем разделе ниже). Внутри такого блока кода можно объявлять переменные, а затем читать и обновлять их или объявлять переменные и передавать их процедурам, которые их изменяют. Однако код в таком блоке по-прежнему должен соответствовать перечисленным выше ограничениям для обращения к значениям и операциям за пределами блока.

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

import std/strformat

var fibN {.compileTime.}: int
var fibPrev {.compileTime.}: int
var fibPrevPrev {.compileTime.}: int

proc nextFib(): int =
  result = if fibN < 2:
    fibN
  else:
    fibPrevPrev + fibPrev
  inc(fibN)
  fibPrevPrev = fibPrev
  fibPrev = result

const f0 = nextFib()
const f1 = nextFib()

const displayFib = block:
  const f2 = nextFib()
  var result = fmt"Fibonacci sequence: {f0}, {f1}, {f2}"
  for i in 3..12:
    add(result, fmt", {nextFib()}")
  result

static:
  echo displayFib

Ограничения на выполнение во время компиляции

Код Nim, который будет выполняться во время компиляции, не может использовать следующие возможности языка:

  • методы
  • итераторы замыканий
  • оператор cast
  • типы ссылок (указатели)
  • FFI

Запрещено использование обёртки, использующей FFI и/или cast. Обратите внимание, что это включает обёртки из стандартных библиотек.

Некоторые или все эти ограничения, вероятно, будут сняты со временем.

Типы

У всех выражений есть тип, известный во время семантического анализа. Nim — статически типизированный язык. Можно объявлять новые типы, что по сути является определением идентификатора, который может использоваться для обозначения этого пользовательского типа.

Вот основные классы типов:

  • Порядковые типы (состоят из целых, булевых, символьных, перечислений (и поддиапазонов этих типов))
  • Типы с плавающей точкой
  • Строковый тип
  • Структурные типы
  • Тип ссылки (указатель)
  • Процедурный тип
  • Обобщённый тип

Порядковые типы

Порядковые типы обладают следующими характеристиками:

  • Порядковые типы являются счётными и упорядоченными. Это свойство позволяет определить операции функций, таких как inc, ord и dec над порядковыми типами.
  • Порядковые типы имеют наименьшее возможное значение, доступное с помощью low(type). Попытка подсчёта ниже наименьшего значения приводит к панике или статической ошибке.
  • Порядковые типы имеют наибольшее возможное значение, доступное с помощью high(type). Попытка подсчёта выше наибольшего значения приводит к панике или статической ошибке.

Целые, булевы, символы и типы перечислений (и поддиапазоны этих типов) относятся к порядковым типам.

Дискретный тип является порядковым типом, если его базовый тип является порядковым типом.

Предопределённые целочисленные типы

Эти целочисленные типы предопределены:

int
обобщённый тип целых со знаком; его размер зависит от платформы и равен размеру указателя. Этот тип следует использовать в общем случае. Литерал целого числа без суффикса типа относится к этому типу, если он находится в диапазоне low(int32)..high(int32); в противном случае тип литерала — int64.
intXX
дополнительные типы целых со знаком из XX бит используют эту схему именования (например, int16 — целое число шириной 16 бит). Текущая реализация поддерживает int8, int16, int32, int64. Литералы этих типов имеют суффикс 'iXX.
uint
обобщённый тип беззнаковых целых; его размер зависит от платформы и равен размеру указателя. Литерал целого числа с суффиксом типа 'u относится к этому типу.
uintXX
дополнительные типы беззнаковых целых из XX бит используют эту схему именования (например, uint16 — беззнаковое целое число шириной 16 бит). Текущая реализация поддерживает uint8, uint16, uint32, uint64. Литералы этих типов имеют суффикс 'uXX. Беззнаковые операции всегда производят циклическое переполнение; они не могут привести к ошибкам переполнения или переполнения.

В дополнение к обычным арифметическим операторам для целых со знаком и без знака (+ - * и т. д.) также есть операторы, которые формально работают с целыми со знаком, но обрабатывают свои аргументы как беззнаковые: они в основном предоставлены для обратной совместимости со старыми версиями языка, в которых отсутствовали типы беззнаковых целых. Эти беззнаковые операции для целых со знаком используют суффикс % в качестве соглашения:

операция значение
a +% b сложение беззнаковых целых
a -% b вычитание беззнаковых целых
a *% b умножение беззнаковых целых
a /% b деление беззнаковых целых
a %% b операция взятия остатка беззнаковых целых
a <% b обработка a и b как беззнаковых и сравнение
a <=% b обработка a и b как беззнаковых и сравнение

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

Преобразование типа сужения преобразует тип большего размера в тип меньшего размера (например, int32 -> int16). Преобразование типа расширения преобразует тип меньшего размера в тип большего размера (например, int16 -> int32). В Nim только расширяющие преобразования типов являются неявными:

var myInt16 = 5i16
var myInt: int
myInt16 + 34     # of type `int16`
myInt16 + myInt  # of type `int`
myInt16 + 2i32   # of type `int32`

Однако, int литералы неявно преобразуются в целочисленный тип меньшего размера, если значение литерала подходит для этого типа меньшего размера, и такое преобразование менее затратно, чем другие неявные преобразования, поэтому myInt16 + 34 производит результат int16.

Для получения дополнительной информации см. Отношение преобразуемости.

Поддиапазонные типы

Поддиапазонный тип — это диапазон значений от порядкового или типа с плавающей точкой (базовый тип). Чтобы определить поддиапазонный тип, необходимо указать его предельные значения — наименьшее и наибольшее значение типа. Например:

type
  Subrange = range[0..5]
  PositiveFloat = range[0.0..Inf]
  Positive* = range[1..high(int)] # as defined in `system`

Subrange является поддиапазоном целого числа, которое может хранить только значения от 0 до 5. PositiveFloat определяет поддиапазон всех положительных значений с плавающей точкой. NaN не принадлежит ни одному поддиапазону типов с плавающей точкой. Присвоение любого другого значения переменной типа Subrange вызывает панику (или статическую ошибку, если это можно определить во время семантического анализа). Разрешены присвоения от базового типа к одному из его поддиапазонных типов (и наоборот).

Поддиапазонный тип имеет тот же размер, что и базовый тип (int в примере поддиапазона).

Предопределённые типы с плавающей точкой

Следующие типы с плавающей точкой предопределены:

float
обобщённый тип с плавающей точкой; его размер раньше зависел от платформы, но теперь он всегда отображается в float64. Этот тип следует использовать в общем случае.
floatXX
реализация может определить дополнительные типы с плавающей точкой из XX бит, используя эту схему именования (например, float64 — число с плавающей точкой шириной 64 бита). Текущая реализация поддерживает float32 и float64. Литералы этих типов имеют суффикс 'fXX.

Автоматическое преобразование типов в выражениях с разными типами с плавающей точкой выполняется: см. Отношение преобразуемости для получения дополнительной информации. Арифметика, выполняемая над типами с плавающей точкой, соответствует стандарту IEEE. Целочисленные типы не преобразуются в типы с плавающей точкой автоматически и наоборот.

Стандарт IEEE определяет пять типов исключений с плавающей точкой:

  • Недействительный: операции с математически недопустимыми операндами, например 0.0/0.0, sqrt(-1.0) и log(-37.8).
  • Деление на ноль: знаменатель равен нулю, а делимое — конечное ненулевое число, например 1.0/0.0.
  • Переполнение: операция производит результат, превышающий диапазон показателя, например MAXDOUBLE+0.0000000000001e308.
  • Подполнение: операция производит результат, слишком маленький для представления в виде нормального числа, например MINDOUBLE * MINDOUBLE.
  • Неточный: операция производит результат, который не может быть представлен с бесконечной точностью, например 2.0 / 3.0, log(1.1) и 0.1 в качестве входных данных.

Исключения IEEE либо игнорируются во время выполнения, либо отображаются в исключения Nim: FloatInvalidOpDefect, FloatDivByZeroDefect, FloatOverflowDefect, FloatUnderflowDefect и FloatInexactDefect. Эти исключения наследуются от базового класса FloatingPointDefect.

Nim предоставляет директивы nanChecks и infChecks для управления тем, игнорируются ли исключения IEEE или вызывается исключение Nim:

{.nanChecks: on, infChecks: on.}
var a = 1.0
var b = 0.0
echo b / b # raises FloatInvalidOpDefect
echo a / b # raises FloatOverflowDefect

В текущей реализации FloatDivByZeroDefect и FloatInexactDefect никогда не поднимаются. FloatOverflowDefect поднимается вместо FloatDivByZeroDefect. Также существует директива floatChecks, которая является сокращением для комбинации директив nanChecks и infChecks. floatChecks отключены по умолчанию.

Единственные операции, на которые влияет директива floatChecks, — это операторы +, -, *, / для типов с плавающей точкой.

Реализация должна всегда использовать максимальную точность, доступную для оценки значений с плавающей точкой во время семантического анализа; это означает, что выражения, подобные 0.09'f32 + 0.01'f32 == 0.09'f64 + 0.01'f64, которые вычисляются во время свёртки констант, верны.

Булевый тип

Булевый тип называется bool в Nim и может принимать одно из двух предопределённых значений true и false. Условия в while, if, elif, when-выражениях должны иметь тип bool.

Это условие выполняется:

ord(false) == 0 and ord(true) == 1

Операторы not, and, or, xor, <, <=, >, >=, !=, == определены для булевого типа. Операторы and и or выполняют вычисление по короткому замыканию. Пример:

while p != nil and p.name != "xyz":
  # p.name is not evaluated if p == nil
  p = p.next

Размер булевого типа составляет один байт.

Символьный тип

Символьный тип называется char в Nim. Его размер составляет один байт. Таким образом, он не может представлять символ UTF-8, а только часть его.

Тип Rune используется для символов Unicode, он может представлять любой символ Unicode. Rune объявлен в модуле unicode.

Типы перечислений

Типы перечислений определяют новый тип, значения которого состоят из указанных значений. Значения упорядочены. Пример:

type
  Direction = enum
    north, east, south, west

Теперь выполняется следующее:

ord(north) == 0
ord(east) == 1
ord(south) == 2
ord(west) == 3

# Also allowed:
ord(Direction.west) == 3

Подразумеваемый порядок: north < east < south < west. Операторы сравнения могут использоваться с типами перечислений. Вместо north и т. д., значение перечисления также можно квалифицировать с типом перечисления, в котором оно находится, Direction.north.

Для лучшего взаимодействия с другими языками программирования поля типов перечислений можно назначить явное порядковое значение. Однако порядковые значения должны быть в порядке возрастания. Поле, которому не присвоено явное порядковое значение, получает значение предыдущего поля + 1.

Явно упорядоченное перечисление может иметь пробелы:

type
  TokenType = enum
    a = 2, b = 4, c = 89 # holes are valid

Однако, после этого он больше не является порядковым, поэтому невозможно использовать эти перечисления в качестве типа индекса для массивов. Для них также недоступны процедуры inc, dec, succ и pred.

Компилятор поддерживает встроенный оператор строкового представления $ для перечислений. Результат строкового представления можно контролировать, явно задав используемые строковые значения:

type
  MyEnum = enum
    valueA = (0, "my value A"),
    valueB = "value B",
    valueC = 2,
    valueD = (3, "abc")

Как видно из примера, можно указать как порядковое значение поля, так и его строковое значение, используя кортеж. Также можно указать только одно из них.

Перечисление можно пометить предикатом pure, чтобы его поля добавлялись в специальную модульно-специфическую скрытую область видимости, которая запрашивается только как последняя попытка. В эту область видимости добавляются только недвусмысленные символы. Но к ним всегда можно получить доступ с помощью квалификации типа, записанной как MyEnum.value:

type
  MyEnum {.pure.} = enum
    valueA, valueB, valueC, valueD, amb
  
  OtherEnum {.pure.} = enum
    valueX, valueY, valueZ, amb


echo valueA # MyEnum.valueA
echo amb    # Error: Unclear whether it's MyEnum.amb or OtherEnum.amb
echo MyEnum.amb # OK.

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

type
  E1 = enum
    value1,
    value2
  E2 = enum
    value1,
    value2 = 4

const
  Lookuptable = [
    E1.value1: "1",
    # no need to qualify value2, known to be E1.value2
    value2: "2"
  ]

proc p(e: E1) =
  # disambiguation in 'case' statements:
  case e
  of value1: echo "A"
  of value2: echo "B"

p value2

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

# a.nim
type Foo* = enum abc

# b.nim
import a
type Bar = enum abc
echo abc is Bar # true

block:
  type Baz = enum abc
  echo abc is Baz # true

Для реализации битовых полей с помощью перечислений см. Битовые поля.

Тип строки

Все строковые литералы имеют тип string. Строка в Nim очень похожа на последовательность символов. Однако строки в Nim являются и нуль-терминированными, и имеют поле длины. Длину можно получить с помощью встроенной процедуры len; длина никогда не учитывает завершающий нуль.

К завершающему нулю нельзя получить доступ, пока строка не будет преобразована в тип cstring. Завершающий нуль гарантирует, что это преобразование можно выполнить за O(1) и без выделения памяти.

Оператор присваивания для строк всегда копирует строку. Оператор & конкатенирует строки.

Большинство собственных типов Nim поддерживают преобразование в строки с помощью специальной процедуры $. Например, при вызове процедуры echo вызывается встроенное строковое представление для параметра:

echo 3 # calls `$` for `int`

Всякий раз, когда пользователь создает специализированный объект, реализация этой процедуры обеспечивает представление string.

type
  Person = object
    name: string
    age: int

proc `$`(p: Person): string = # `$` always returns a string
  result = p.name & " is " &
          $p.age & # we *need* the `$` in front of p.age which
                   # is natively an integer to convert it to
                   # a string
          " years old."

Хотя $p.name также можно использовать, операция $ над строкой ничего не делает. Обратите внимание, что мы не можем полагаться на автоматическое преобразование из int в string, как мы можем для процедуры echo.

Строки сравниваются по лексикографическому порядку. Все операторы сравнения доступны. К строкам можно обращаться по индексу, как к массивам (нижняя граница — 0). В отличие от массивов, они могут использоваться в операторах выбора:

case paramStr(i)
of "-v": incl(options, optVerbose)
of "-h", "-?": incl(options, optHelp)
else: write(stdout, "invalid command line option!\n")

Согласно соглашению, все строки являются строками UTF-8, но это не проверяется. Например, при чтении строк из двоичных файлов они представляют собой просто последовательность байтов. Операция индексирования s[i] означает i-й символ строки s, а не i-й символ Юникода. Итератор runes из модуля unicode может использоваться для итерации по всем символам Юникода.

Тип cstring

Тип cstring, означающий compatible string, является родным представлением строки для компилятора. Для C-компилятора тип cstring представляет собой указатель на нуль-терминированный массив char, совместимый с типом char* в ANSI C. Его основное назначение — удобное взаимодействие с C. Операция индексирования s[i] означает i-й символ строки s; однако проверка границ для cstring не выполняется, что делает операцию индексирования небезопасной.

Nim-строка string неявно преобразуется в cstring для удобства. Если Nim-строка передается процедуре с переменным числом параметров по стилю C, она также неявно преобразуется в cstring:

proc printf(formatstr: cstring) {.importc: "printf", varargs,
                                  header: "<stdio.h>".}

printf("This works %s", "as expected")

Несмотря на то, что преобразование неявное, оно небезопасно: сборщик мусора не считает cstring корневой областью и может собрать базовую память. По этой причине неявное преобразование будет удалено в будущих выпусках компилятора Nim. Некоторые идиомы, такие как преобразование const строки в cstring, безопасны и останутся допустимыми.

Для cstring определена процедура $, которая возвращает строку. Таким образом, чтобы получить Nim-строку из cstring:

var str: string = "Hello!"
var cstr: cstring = str
var newstr: string = $cstr

Литералы cstring не должны изменяться.

var x = cstring"literals"
x[1] = 'A' # This is wrong!!!

Если cstring происходит из обычной памяти (а не из памяти только для чтения), она может быть изменена:

var x = "123456"
prepareMutation(x) # call `prepareMutation` before modifying the strings
var s: cstring = cstring(x)
s[0] = 'u' # This is ok

Значения cstring также могут использоваться в операторах выбора, как и строки.

Структурированные типы

Переменная структурированного типа может хранить несколько значений одновременно. Структурированные типы могут быть вложены до неограниченных уровней. К структурированным типам относятся массивы, последовательности, кортежи, объекты и множества.

Типы массивов и последовательностей

Массивы — это однородный тип, т. е. каждый элемент массива имеет одинаковый тип. Массивы всегда имеют фиксированную длину, указанную как выражение константы (кроме открытых массивов). К ним можно обращаться по индексу любого порядкового типа. Параметр A может быть открытым массивом, в этом случае он индексируется целыми числами от 0 до len(A)-1. Выражение массива может быть построено с помощью конструктора массива []. Тип элемента этого выражения массива определяется из типа первого элемента. Все остальные элементы должны быть неявно преобразуемы в этот тип.

Тип массива может быть определен с помощью синтаксиса array[size, T] или с помощью синтаксиса array[lo..hi, T] для массивов, начинающихся с индекса, отличного от нуля.

Последовательности похожи на массивы, но имеют динамическую длину, которая может изменяться во время выполнения (как строки). Последовательности реализованы как изменяемые массивы, выделяющие фрагменты памяти по мере добавления элементов. Последовательность S всегда индексируется целыми числами от 0 до len(S)-1, и ее границы проверяются. Последовательности можно создавать с помощью конструктора массива [] в сочетании с оператором массива в последовательность @. Другой способ выделить память для последовательности — вызвать встроенную процедуру newSeq.

Последовательность можно передать в параметр типа открытый массив.

Пример:

type
  IntArray = array[0..5, int] # an array that is indexed with 0..5
  IntSeq = seq[int] # a sequence of integers
var
  x: IntArray
  y: IntSeq
x = [1, 2, 3, 4, 5, 6]  # [] is the array constructor
y = @[1, 2, 3, 4, 5, 6] # the @ turns the array into a sequence

let z = [1.0, 2, 3, 4] # the type of z is array[0..3, float]

Нижнюю границу массива или последовательности можно получить с помощью встроенной процедуры low(), верхнюю границу — с помощью процедуры high(). Длину можно получить с помощью процедуры len(). low() для последовательности или открытого массива всегда возвращает 0, так как это первый допустимый индекс. Элементы можно добавлять в последовательность с помощью процедуры add() или оператора &, а последний элемент последовательности можно удалить (и получить) с помощью процедуры pop().

Запись x[i] можно использовать для доступа к i-му элементу x.

Для массивов всегда выполняется проверка границ (статически или во время выполнения). Эти проверки можно отключить с помощью директив или вызвав компилятор со флагом командной строки --boundChecks:off.

Конструктор массива может иметь явные индексы для повышения читаемости:

type
  Values = enum
    valA, valB, valC

const
  lookupTable = [
    valA: "A",
    valB: "B",
    valC: "C"
  ]

Если индекс опущен, используется значение индекса succ(lastIndex):

type
  Values = enum
    valA, valB, valC, valD, valE

const
  lookupTable = [
    valA: "A",
    "B",
    valC: "C",
    "D", "e"
  ]

Открытые массивы

Часто фиксированные массивы оказываются слишком негибкими; процедуры должны уметь обрабатывать массивы разной длины. Тип openarray позволяет это; его можно использовать только для параметров. Открытые массивы всегда индексируются с помощью int, начиная с позиции 0. Для открытых массивов также доступны операции len, low и high. Любой массив с совместимым базовым типом может быть передан в параметр открытого массива, тип индекса не имеет значения. Помимо массивов, к параметру открытого массива также могут передаваться последовательности.

Тип openarray не может быть вложен: многомерные открытые массивы не поддерживаются, так как это редко требуется и не может быть эффективно реализовано.

proc testOpenArray(x: openArray[int]) = echo repr(x)

testOpenArray([1,2,3])  # array[]
testOpenArray(@[1,2,3]) # seq[]

Параметры с переменным числом аргументов

Параметр varargs — это параметр открытого массива, который дополнительно позволяет передавать процедуре переменное число аргументов. Компилятор неявно преобразует список аргументов в массив:

proc myWriteln(f: File, a: varargs[string]) =
  for s in items(a):
    write(f, s)
  write(f, "\n")

myWriteln(stdout, "abc", "def", "xyz")
# is transformed to:
myWriteln(stdout, ["abc", "def", "xyz"])

Это преобразование выполняется только в том случае, если параметр varargs является последним параметром в заголовке процедуры. Также возможно выполнять преобразования типов в этом контексте:

proc myWriteln(f: File, a: varargs[string, `$`]) =
  for s in items(a):
    write(f, s)
  write(f, "\n")

myWriteln(stdout, 123, "abc", 4.0)
# is transformed to:
myWriteln(stdout, [$123, $"abc", $4.0])

В этом примере $ применяется ко всем аргументам, передаваемым в параметр a. (Обратите внимание, что $ для строк — это nop.)

Обратите внимание, что явный конструктор массива, переданный в параметр varargs, не оборачивается в дополнительное неявное построение массива:

proc takeV[T](a: varargs[T]) = discard

takeV([123, 2, 1]) # takeV's T is "int", not "array of int"

varargs[typed] обрабатывается особым образом: он соответствует списку аргументов переменной длины произвольного типа, но всегда создаёт неявный массив. Это необходимо для того, чтобы встроенная процедура echo работала так, как ожидается:

proc echo*(x: varargs[typed, `$`]) {...}

echo @[1, 2, 3]
# prints "@[1, 2, 3]" and not "123"

Непроверенные массивы

Тип UncheckedArray[T] — это особый вид array, где его границы не проверяются. Это часто полезно для реализации настраиваемых гибких массивов с изменяемым размером. Кроме того, непроверенный массив преобразуется в массив C неопределенного размера:

type
  MySeq = object
    len, cap: int
    data: UncheckedArray[int]

Производит примерно такой код C:

typedef struct {
  NI len;
  NI cap;
  NI data[];
} MySeq;

Базовый тип непроверенного массива не должен содержать никакой памяти, управляемой сборщиком мусора, но это в настоящее время не проверяется.

Будущие направления: в непроверенные массивы должна быть разрешена память, управляемая сборщиком мусора, и должно быть явное обозначение того, как сборщик мусора определяет размер массива во время выполнения.

Кортежи и типы объектов

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

type
  Person = tuple[name: string, age: int] # type representing a person:
                                         # it consists of a name and an age.
var person: Person
person = (name: "Peter", age: 30)
assert person.name == "Peter"
# the same, but less readable:
person = ("Peter", 30)
assert person[0] == "Peter"
assert Person is (string, int)
assert (string, int) is Person
assert Person isnot tuple[other: string, age: int] # `other` is a different identifier

Кортеж с одним безымянным полем можно создать с помощью скобок и запятой в конце:

proc echoUnaryTuple(a: (int,)) =
  echo a[0]

echoUnaryTuple (1,)

В действительности, запятая в конце допускается для любой конструкции кортежа.

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

Для согласованности с object объявлениями, кортежи в блоке type также можно определить с отступом вместо []:

type
  Person = tuple   # type representing a person
    name: string   # a person consists of a name
    age: Natural   # and an age

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

type
  Person = object of RootObj
    name*: string   # the * means that `name` is accessible from other modules
    age: int        # no * means that the field is hidden
  
  Student = ref object of Person # a student is a person
    id: int                      # with an id field

var
  student: Student
  person: Person
assert(student of Student) # is true
assert(student of Person) # also true

Поля объектов, которые должны быть видимыми извне определяющего модуля, должны быть помечены *. В отличие от кортежей, разные типы объектов никогда не являются эквивалентными, они являются именованными типами, в то время как кортежи являются структурными. Объекты, у которых нет предков, неявно final и, следовательно, не имеют скрытой информации о типе. Можно использовать препроцессор inheritable, чтобы ввести новые корни объектов помимо system.RootObj.

type
  Person = object # example of a final object
    name*: string
    age: int
  
  Student = ref object of Person # Error: inheritance only works with non-final objects
    id: int

Оператор присваивания для кортежей и объектов копирует каждый компонент. Методы для переопределения этого поведения копирования описаны здесь.

Создание объектов

Объекты также могут быть созданы с помощью выражения создания объекта, имеющего синтаксис T(fieldA: valueA, fieldB: valueB, ...), где T — тип object или тип ref object:

type
  Student = object
    name: string
    age: int
  PStudent = ref Student
var a1 = Student(name: "Anton", age: 5)
var a2 = PStudent(name: "Anton", age: 5)
# this also works directly:
var a3 = (ref Student)(name: "Anton", age: 5)
# not all fields need to be mentioned, and they can be mentioned out of order:
var a4 = Student(age: 5)

Обратите внимание, что в отличие от кортежей, для объектов требуются имена полей вместе со значениями. Для типа ref object тип system.new вызывается неявно.

Варианты объектов

Часто иерархия объектов является избыточной в ситуациях, когда нужны простые типы вариантов. Варианты объектов — это помеченные объединения, различаемые перечислением, используемым для гибкости типа во время выполнения, отражая концепции типов-сумм и алгебраических типов данных (ADT), как в других языках.

Пример:

# This is an example of how an abstract syntax tree could be modelled in Nim
type
  NodeKind = enum  # the different node types
    nkInt,          # a leaf with an integer value
    nkFloat,        # a leaf with a float value
    nkString,       # a leaf with a string value
    nkAdd,          # an addition
    nkSub,          # a subtraction
    nkIf            # an if statement
  Node = ref NodeObj
  NodeObj = object
    case kind: NodeKind  # the `kind` field is the discriminator
    of nkInt: intVal: int
    of nkFloat: floatVal: float
    of nkString: strVal: string
    of nkAdd, nkSub:
      leftOp, rightOp: Node
    of nkIf:
      condition, thenPart, elsePart: Node

# create a new case object:
var n = Node(kind: nkIf, condition: nil)
# accessing n.thenPart is valid because the `nkIf` branch is active:
n.thenPart = Node(kind: nkFloat, floatVal: 2.0)

# the following statement raises an `FieldDefect` exception, because
# n.kind's value does not fit and the `nkString` branch is not active:
n.strVal = ""

# invalid: would change the active object branch:
n.kind = nkInt

var x = Node(kind: nkAdd, leftOp: Node(kind: nkInt, intVal: 4),
                          rightOp: Node(kind: nkInt, intVal: 2))
# valid: does not change the active object branch:
x.kind = nkSub

Как видно из примера, преимущество иерархии объектов заключается в том, что не требуется приведение типов между разными типами объектов. Тем не менее, доступ к недопустимым полям объекта вызывает исключение.

Синтаксис case в объявлении объекта тесно следует за синтаксисом оператора case: ветви в блоке case также могут иметь отступы.

В примере поле kind называется дискриминатором: для безопасности его адрес нельзя получить, и присваивания ему ограничены: новое значение не должно приводить к изменению активной ветви объекта. Кроме того, когда поля конкретной ветви указываются при создании объекта, соответствующее значение дискриминатора должно быть указано как константное выражение.

Вместо изменения активной ветви объекта, замените старый объект в памяти полностью новым:

var x = Node(kind: nkAdd, leftOp: Node(kind: nkInt, intVal: 4),
                          rightOp: Node(kind: nkInt, intVal: 2))
# change the node's contents:
x[] = NodeObj(kind: nkString, strVal: "abc")

Начиная с версии 0.20, system.reset больше не может использоваться для поддержки изменений ветвей объекта, поскольку это никогда не было полностью безопасным для памяти.

Как специальное правило, вид дискриминатора также может быть ограничен с помощью оператора case. Если возможные значения переменной дискриминатора в ветви оператора case являются подмножеством значений дискриминатора для выбранной ветви объекта, инициализация считается допустимой. Этот анализ работает только для неизменяемых дискриминаторов порядкового типа и игнорирует elif ветви. Для значений дискриминатора с типом range компилятор проверяет, является ли весь диапазон возможных значений значения дискриминатора допустимым для выбранной ветви объекта.

Небольшой пример:

let unknownKind = nkSub

# invalid: unsafe initialization because the kind field is not statically known:
var y = Node(kind: unknownKind, strVal: "y")

var z = Node()
case unknownKind
of nkAdd, nkSub:
  # valid: possible values of this branch are a subset of nkAdd/nkSub object branch:
  z = Node(kind: unknownKind, leftOp: Node(), rightOp: Node())
else:
  echo "ignoring: ", unknownKind

# also valid, since unknownKindBounded can only contain the values nkAdd or nkSub
let unknownKindBounded = range[nkAdd..nkSub](unknownKind)
z = Node(kind: unknownKindBounded, leftOp: Node(), rightOp: Node())

cast uncheckedAssign

Некоторые ограничения для объектов case могут быть отключены с помощью блока {.cast(uncheckedAssign).}:

type
  TokenKind* = enum
    strLit, intLit
  Token = object
    case kind*: TokenKind
    of strLit:
      s*: string
    of intLit:
      i*: int64

proc passToVar(x: var TokenKind) = discard

var t = Token(kind: strLit, s: "abc")

{.cast(uncheckedAssign).}:
  # inside the 'cast' section it is allowed to pass 't.kind' to a 'var T' parameter:
  passToVar(t.kind)
  
  # inside the 'cast' section it is allowed to set field 's' even though the
  # constructed 'kind' field has an unknown value:
  t = Token(kind: t.kind, s: "abc")
  
  # inside the 'cast' section it is allowed to assign to the 't.kind' field directly:
  t.kind = intLit

Значения по умолчанию для полей объектов

Поля объектов могут иметь константное значение по умолчанию. Тип поля можно опустить, если задано значение по умолчанию.

type
  Foo = object
    a: int = 2
    b: float = 3.14
    c = "I can have a default value"
  
  Bar = ref object
    a: int = 2
    b: float = 3.14
    c = "I can have a default value"

Явная инициализация использует эти значения по умолчанию, что включает в себя object, созданный с помощью выражения создания объекта или процедуры default; ref object, созданный с помощью выражения создания объекта или процедуры new; массив или кортеж с подтипом, который имеет значение по умолчанию, созданное процедурой default.

type
  Foo = object
    a: int = 2
    b = 3.0
  Bar = ref object
    a: int = 2
    b = 3.0

block: # created with an object construction expression
  let x = Foo()
  assert x.a == 2 and x.b == 3.0
  
  let y = Bar()
  assert y.a == 2 and y.b == 3.0

block: # created with an object construction expression
  let x = default(Foo)
  assert x.a == 2 and x.b == 3.0
  
  let y = default(array[1, Foo])
  assert y[0].a == 2 and y[0].b == 3.0
  
  let z = default(tuple[x: Foo])
  assert z.x.a == 2 and z.x.b == 3.0

block: # created with the procedure `new`
  let y = new Bar
  assert y.a == 2 and y.b == 3.0

Тип множества

Тип множества моделирует математическое понятие множества. Базовый тип множества может быть только порядковым типом определенного размера, а именно:
  • int8-int16
  • uint8/byte-uint16
  • char
  • enum
  • Поддиапазоны порядковых типов, т.е. range[-10..10]

или эквивалентный. При построении множества с целыми литералами со знаком, базовый тип множества определяется в диапазоне 0 .. DefaultSetElements-1, где DefaultSetElements в настоящее время всегда равно 2^8. Максимальная длина диапазона для базового типа множества составляет MaxSetElements, которая в настоящее время всегда равна 2^16. Типы с большей длиной диапазона приводятся к диапазону 0 .. MaxSetElements-1.

Причина в том, что множества реализуются в виде высокопроизводительных битовых векторов. Попытка объявить множество с большим типом приведет к ошибке:

var s: set[int64] # Error: set is too large; use `std/sets` for ordinal types
                    # with more than 2^16 elements

Примечание: Nim также предлагает хэш-множества (которые вам нужно импортировать с помощью import std/sets), у которых нет таких ограничений.

Множества можно создавать с помощью конструктора множеств: {} — пустое множество. Пустое множество совместимо по типу с любым конкретным типом множества. Конструктор также можно использовать для включения элементов (и диапазонов элементов):

type
  CharSet = set[char]
var
  x: CharSet
x = {'a'..'z', '0'..'9'} # This constructs a set that contains the
                         # letters from 'a' to 'z' and the digits
                         # from '0' to '9'

Модуль `std/setutils` предоставляет способ инициализации множества из итерируемого объекта:

import std/setutils

let uniqueChars = myString.toSet

Эти операции поддерживаются множествами:

операция значение
A + B объединение двух множеств
A * B пересечение двух множеств
A - B разность двух множеств (A без элементов B)
A == B равенство множеств
A <= B отношение подмножества (A является подмножеством B или равно ему)
A < B строгое отношение подмножества (A является собственным подмножеством B)
e in A принадлежность к множеству (A содержит элемент e)
e notin A A не содержит элемент e
contains(A, e) A содержит элемент e
card(A) мощность A (количество элементов в A)
incl(A, elem) то же, что и A = A + {elem}
excl(A, elem) то же, что и A = A - {elem}

Битовые поля

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

Перечисление, множества и приведение типов могут использоваться вместе, как в:

type
  MyFlag* {.size: sizeof(cint).} = enum
    A
    B
    C
    D
  MyFlags = set[MyFlag]

proc toNum(f: MyFlags): int = cast[cint](f)
proc toFlags(v: int): MyFlags = cast[MyFlags](v)

assert toNum({}) == 0
assert toNum({A}) == 1
assert toNum({D}) == 8
assert toNum({A, C}) == 5
assert toFlags(0) == {}
assert toFlags(7) == {A, B, C}

Обратите внимание, как множество превращает значения перечисления в степени 2.

При использовании перечислений и множеств с C используйте distinct cint.

Для взаимодействия с C см. также препроцессор bitsize.

Ссылки и указатели

Ссылки (аналогичные указателям в других языках программирования) — это способ введения многозначных отношений. Это означает, что разные ссылки могут указывать на одно и то же место в памяти и изменять его (также называется алиасингом).

Nim различает отслеживаемые и неотслеживаемые ссылки. Неотслеживаемые ссылки также называются указателями. Отслеживаемые ссылки указывают на объекты из управляемой кучи, неотслеживаемые ссылки указывают на объекты, выделенные вручную, или объекты где-то еще в памяти. Таким образом, неотслеживаемые ссылки являются небезопасными. Однако для некоторых операций низкого уровня (доступ к аппаратному обеспечению) неотслеживаемые ссылки неизбежны.

Отслеживаемые ссылки объявляются с ключевым словом ref, неотслеживаемые ссылки — с ключевым словом ptr. В общем случае ptr T неявно преобразуется в тип pointer.

Пустой индекс [] может использоваться для де-референцирования ссылки, процедура addr возвращает адрес элемента. Адрес всегда является неотслеживаемой ссылкой. Таким образом, использование addr — это небезопасная функция.

Операторы . (доступ к полю кортежа/объекта) и [] (оператор индекса массива/строки/последовательности) выполняют неявное де-референцирование для типов ссылок:

type
  Node = ref NodeObj
  NodeObj = object
    le, ri: Node
    data: int

var
  n: Node
new(n)
n.data = 9
# no need to write n[].data; in fact n[].data is highly discouraged!

Для упрощения проверки структурных типов рекурсивные кортежи не являются допустимыми:

# invalid recursion
type MyTuple = tuple[a: ref MyTuple]

Аналогично, T = ref T — это недопустимый тип.

В качестве синтаксического расширения, типы object могут быть анонимными, если объявлены в разделе типа с помощью обозначений ref object или ptr object. Эта функция полезна, если объект должен получить только семантику ссылки:

type
  Node = ref object
    le, ri: Node
    data: int

Для выделения нового отслеживаемого объекта должна быть использована встроенная процедура new. Для работы с неотслеживаемой памятью можно использовать процедуры alloc, dealloc и realloc. Дополнительная информация содержится в документации модуля system.

Nil

Если ссылка указывает на ничто, у неё есть значение nil. nil — это значение по умолчанию для всех типов ref и ptr. Значение nil также может использоваться как любое другое литеральное значение. Например, оно может использоваться в присваивании, как в myRef = nil.

Обращение к nil — это невосстановимая фатальная ошибка во время выполнения (а не паника).

Успешная операция разыменования p[] подразумевает, что p не равно nil. Это можно использовать в реализации для оптимизации кода, такого как:

p[].field = 3
if p != nil:
  # if p were nil, `p[]` would have caused a crash already,
  # so we know `p` is always not nil here.
  action()

В:

p[].field = 3
action()

Примечание: Это не сравнимо с «неопределённым поведением» в C при разыменовании указателей NULL.

Смешивание памяти с сборкой мусора с ptr

Необходимо проявлять особую осторожность, если неотслеживаемый объект содержит отслеживаемые объекты, такие как отслеживаемые ссылки, строки или последовательности: для правильного освобождения всего необходимо вызвать встроенную процедуру reset перед ручным освобождением неотслеживаемой памяти:

type
  Data = tuple[x, y: int, s: string]

# allocate memory for Data on the heap:
var d = cast[ptr Data](alloc0(sizeof(Data)))

# create a new string on the garbage collected heap:
d.s = "abc"

# tell the GC that the string is not needed anymore:
reset(d.s)

# free the memory:
dealloc(d)

Без вызова reset память, выделенная для строки d.s, никогда не будет освобождена. Пример также демонстрирует две важные особенности для программирования на низком уровне: процедура sizeof возвращает размер типа или значения в байтах. Оператор cast может обойти систему типов: компилятор вынужден рассматривать результат вызова alloc0 (который возвращает нетипизированный указатель) так, как будто у него есть тип ptr Data. Приведение типов следует использовать только в случае крайней необходимости: оно нарушает безопасность типов, и ошибки могут привести к загадочным сбоям.

Примечание: Пример работает только потому, что память инициализируется нулём (alloc0 вместо alloc делает это): таким образом, d.s инициализируется двоичным нулём, что строковое присваивание может обработать. При смешивании данных, управляемых сборкой мусора, с неуправляемой памятью, необходимо знать такие детали низкого уровня.

Процедурный тип

Процедурный тип — это внутренне указатель на процедуру. nil — допустимое значение для переменной процедурного типа.

Примеры:

proc printItem(x: int) = ...

proc forEach(c: proc (x: int) {.cdecl.}) =
  ...

forEach(printItem)  # this will NOT compile because calling conventions differ
type
  OnMouseMove = proc (x, y: int) {.closure.}

proc onMouseMove(mouseX, mouseY: int) =
  # has default calling convention
  echo "x: ", mouseX, " y: ", mouseY

proc setOnMouseMove(mouseMoveEvent: OnMouseMove) = discard

# ok, 'onMouseMove' has the default calling convention, which is compatible
# to 'closure':
setOnMouseMove(onMouseMove)

Незначительная проблема с процедурными типами заключается в том, что соглашение о вызове процедуры влияет на совместимость типов: процедурные типы совместимы только в случае одинакового соглашения о вызове. В качестве специального расширения процедура соглашения о вызове nimcall может быть передана в параметр, ожидающий proc с соглашением о вызове closure.

Nim поддерживает следующие соглашения о вызове:

nimcall
является соглашением по умолчанию, используемым для Nim proc. Оно совпадает с fastcall, но только для компиляторов C, поддерживающих fastcall.
closure
является соглашением о вызове по умолчанию для процедурного типа, не имеющего аннотаций pragmas. Оно указывает, что процедура имеет скрытый неявный параметр (среду). Переменные proc, имеющие соглашение о вызове closure, занимают два машинных слова: одно для указателя на процедуру и другое для указателя на неявно переданную среду.
stdcall
Это соглашение о вызове stdcall, как указано в Microsoft. Сгенерированная процедура C объявляется с ключевым словом __stdcall.
cdecl
Соглашение cdecl означает, что процедура должна использовать то же соглашение, что и компилятор C. В Windows сгенерированная процедура C объявляется с ключевым словом __cdecl.
safecall
Это соглашение о вызове safecall, как указано в Microsoft. Сгенерированная процедура C объявляется с ключевым словом __safecall. Слово безопасный относится к тому, что все регистры процессора должны быть помещены в стек процессора.
inline
Соглашение inline означает, что вызывающая сторона не должна вызывать процедуру, а встроить её код напрямую. Обратите внимание, что Nim не встраивает, а оставляет это компилятору C; он генерирует процедуры __inline. Это всего лишь подсказка для компилятора: он может её полностью игнорировать и может встраивать процедуры, которые не помечены как inline.
fastcall
Fastcall имеет различное значение для разных компиляторов C. Получаете то, что означает C __fastcall.
thiscall
Это соглашение о вызове thiscall, как указано в Microsoft, используемое для функций-членов класса C++ на архитектуре x86.
syscall
Соглашение syscall совпадает с __syscall в C. Оно используется для прерываний.
noconv
Сгенерированный код C не будет иметь явного соглашения о вызове и, следовательно, будет использовать соглашение о вызове по умолчанию компилятора C. Это необходимо, потому что соглашение о вызове Nim по умолчанию для процедур — fastcall для повышения скорости.

Большинство соглашений о вызове существуют только для 32-разрядной платформы Windows.

Соглашение о вызове по умолчанию — nimcall, если это не внутренняя процедура (процедура внутри процедуры). Для внутренней процедуры выполняется анализ того, имеет ли она доступ к своей среде. Если это так, то у неё соглашение о вызове closure, в противном случае — nimcall.

Дискретный тип

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

Дискретный тип является порядковым типом, если его базовый тип является порядковым типом.

Моделирование валют

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

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

type
  Dollar = distinct int
  Euro = distinct int

var
  d: Dollar
  e: Euro

echo d + 12
# Error: cannot add a number with no unit and a `Dollar`

К сожалению, d + 12.Dollar также не разрешено, поскольку + определено для int (и др.), а не для Dollar. Таким образом, требуется определить + для долларов:

proc `+` (x, y: Dollar): Dollar =
  result = Dollar(int(x) + int(y))

Не имеет смысла умножать доллар на доллар, но на число без единицы; то же самое справедливо и для деления:

proc `*` (x: Dollar, y: int): Dollar =
  result = Dollar(int(x) * y)

proc `*` (x: int, y: Dollar): Dollar =
  result = Dollar(x * int(y))

proc `div` ...

Это быстро становится утомительным. Реализации тривиальны, и компилятор не должен генерировать весь этот код только для его последующей оптимизации — ведь + для долларов должен генерировать тот же двоичный код, что и + для целых чисел. Пragma borrow разработано для решения этой проблемы; в принципе, оно генерирует вышеупомянутые тривиальные реализации:

proc `*` (x: Dollar, y: int): Dollar {.borrow.}
proc `*` (x: int, y: Dollar): Dollar {.borrow.}
proc `div` (x: Dollar, y: int): Dollar {.borrow.}

Пragma borrow заставляет компилятор использовать ту же реализацию, что и процедура, работающая с базовым типом дискретного типа, поэтому код не генерируется.

Но кажется, что весь этот шаблонный код нужно повторять для валюты Euro. Это можно решить с помощью шаблонов.

template additive(typ: typedesc) =
  proc `+` *(x, y: typ): typ {.borrow.}
  proc `-` *(x, y: typ): typ {.borrow.}
  
  # unary operators:
  proc `+` *(x: typ): typ {.borrow.}
  proc `-` *(x: typ): typ {.borrow.}

template multiplicative(typ, base: typedesc) =
  proc `*` *(x: typ, y: base): typ {.borrow.}
  proc `*` *(x: base, y: typ): typ {.borrow.}
  proc `div` *(x: typ, y: base): typ {.borrow.}
  proc `mod` *(x: typ, y: base): typ {.borrow.}

template comparable(typ: typedesc) =
  proc `<` * (x, y: typ): bool {.borrow.}
  proc `<=` * (x, y: typ): bool {.borrow.}
  proc `==` * (x, y: typ): bool {.borrow.}

template defineCurrency(typ, base: untyped) =
  type
    typ* = distinct base
  additive(typ)
  multiplicative(typ, base)
  comparable(typ)

defineCurrency(Dollar, int)
defineCurrency(Euro, int)

Пragma borrow также может использоваться для аннотации дискретного типа, чтобы разрешить поднятие определённых встроенных операций:

type
  Foo = object
    a, b: int
    s: string
  
  Bar {.borrow: `.`.} = distinct Foo

var bb: ref Bar
new bb
# field access now valid
bb.a = 90
bb.s = "abc"

В настоящее время только доступ к члену через точку может быть заимствован таким образом.

Предотвращение атак SQL-инъекции

SQL-запрос, передаваемый от Nim в базу данных SQL, может быть смоделирован как строка. Однако использование шаблонов строк и заполнение значений уязвимо к известной атаке SQL-инъекции:

import std/strutils

proc query(db: DbHandle, statement: string) = ...

var
  username: string

db.query("SELECT FROM users WHERE name = '$1'" % username)
# Horrible security hole, but the compiler does not mind!

Этому можно избежать, различая строки, содержащие SQL, и строки, которые его не содержат. Дискретные типы предоставляют способ введения нового типа строк SQL, несовместимого с string:

type
  SQL = distinct string

proc query(db: DbHandle, statement: SQL) = ...

var
  username: string

db.query("SELECT FROM users WHERE name = '$1'" % username)
# Static error: `query` expects an SQL string!

Это существенное свойство абстрактных типов, состоящее в том, что они не подразумевают отношение подтипа между абстрактным типом и его базовым типом. Разрешены явные преобразования типов из string в SQL:

import std/[strutils, sequtils]

proc properQuote(s: string): SQL =
  # quotes a string properly for an SQL statement
  return SQL(s)

proc `%` (frmt: SQL, values: openarray[string]): SQL =
  # quote each argument:
  let v = values.mapIt(properQuote(it))
  # we need a temporary type for the type conversion :-(
  type StrSeq = seq[string]
  # call strutils.`%`:
  result = SQL(string(frmt) % StrSeq(v))

db.query("SELECT FROM users WHERE name = '$1'".SQL % [username])

Теперь у нас есть проверка на этапе компиляции против атак SQL-инъекции. Так как "".SQL преобразуется в SQL(""), для красивых SQL строковых литералов не требуется новый синтаксис. Гипотетический тип SQL фактически существует в библиотеке в виде типа SqlQuery таких модулей, как db_sqlite.

Автоматический тип

Тип auto может использоваться только для типов возвращаемых значений и параметров. Для типов возвращаемых значений он заставляет компилятор выводить тип из тела процедуры:

proc returnsInt(): auto = 1984

Для параметров он в настоящее время создаёт неявно обобщённые процедуры:

proc foo(a, b: auto) = discard

Это то же самое, что:

proc foo[T1, T2](a: T1, b: T2) = discard

Однако в более поздних версиях языка это может измениться, чтобы означать «вывести типы параметров из тела». Тогда вышеприведённое foo было бы отклонено, так как типы параметров нельзя вывести из пустого discard оператора.

Отношения типов

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

Равенство типов

Nim использует структурное равенство типов для большинства типов. Только для объектов, перечислений, дискретных типов и для обобщённых типов используется равенство имён.

Отношение подтипа

Если объект a наследуется от b, то a является подтипом b.

Это отношение подтипа распространяется на типы var, ref, ptr. Если A является подтипом B и A и B являются object типами, то:

  • var A является подтипом var B
  • ref A является подтипом ref B
  • ptr A является подтипом ptr B.

Примечание: Одно из вышеперечисленных косвенных указаний на указатель требуется для присваивания от подтипа к родительскому типу для предотвращения «вырезания объекта».

Отношение преобразуемости

Тип a неявным образом преобразуется к типу b, если следующий алгоритм возвращает значение true:

proc isImplicitlyConvertible(a, b: PType): bool =
  if isSubtype(a, b):
    return true
  if isIntLiteral(a):
    return b in {int8, int16, int32, int64, int, uint, uint8, uint16,
                 uint32, uint64, float32, float64}
  case a.kind
  of int:     result = b in {int32, int64}
  of int8:    result = b in {int16, int32, int64, int}
  of int16:   result = b in {int32, int64, int}
  of int32:   result = b in {int64, int}
  of uint:    result = b in {uint32, uint64}
  of uint8:   result = b in {uint16, uint32, uint64}
  of uint16:  result = b in {uint32, uint64}
  of uint32:  result = b in {uint64}
  of float32: result = b in {float64}
  of float64: result = b in {float32}
  of seq:
    result = b == openArray and typeEquals(a.baseType, b.baseType)
  of array:
    result = b == openArray and typeEquals(a.baseType, b.baseType)
    if a.baseType == char and a.indexType.rangeA == 0:
      result = b == cstring
  of cstring, ptr:
    result = b == pointer
  of string:
    result = b == cstring
  of proc:
    result = typeEquals(a, b) or compatibleParametersAndEffects(a, b)

Мы использовали предикат typeEquals(a, b) для свойства "равенство типов" и предикат isSubtype(a, b) для свойства "отношение подтипов". compatibleParametersAndEffects(a, b) в настоящее время не определён.

Неявные преобразования также выполняются для конструктора типов range в Nim.

Пусть a0, b0 имеют тип T.

Пусть A = range[a0..b0] — тип аргумента, а F — тип формального параметра. Тогда неявное преобразование из A в F существует, если a0 >= low(F) and b0 <= high(F) и оба T и F являются целыми числами со знаком или оба являются целыми числами без знака.

Тип a явным образом преобразуется к типу b, если следующий алгоритм возвращает значение true:

proc isIntegralType(t: PType): bool =
  result = isOrdinal(t) or t.kind in {float, float32, float64}

proc isExplicitlyConvertible(a, b: PType): bool =
  result = false
  if isImplicitlyConvertible(a, b): return true
  if typeEquals(a, b): return true
  if a == distinct and typeEquals(a.baseType, b): return true
  if b == distinct and typeEquals(b.baseType, a): return true
  if isIntegralType(a) and isIntegralType(b): return true
  if isSubtype(a, b) or isSubtype(b, a): return true

Отношение преобразуемости может быть ослаблено пользователем с помощью определяемого пользователем типа converter.

converter toInt(x: char): int = result = ord(x)

var
  x: int
  chr: char = 'a'

# implicit conversion magic happens here
x = chr
echo x # => 97
# one can use the explicit form too
x = chr.toInt
echo x # => 97

Преобразование типа T(a) является l-значением, если a является l-значением и выполняется typeEqualsOrDistinct(T, typeof(a)).

Совместимость при присваивании

Выражение b может быть присвоено выражению a, если a является l-value и выполняется isImplicitlyConvertible(b.typ, a.typ).

Разрешение перегрузки

В вызове p(args), где p может ссылаться на более чем один кандидат, говорят о выборе символа. Разрешение перегрузки попытается найти наилучшего кандидата, таким образом преобразовав выбор символа в разрешённый символ. Выбирается наиболее подходящая процедура p в соответствии с рядом испытаний, объяснённых ниже. В порядке: соответствие категориям, сравнение иерархического порядка и, наконец, анализ сложности.

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

Первое испытание: соответствие категориям

Каждый аргумент в args должен соответствовать, и существует несколько разных категорий соответствий. Пусть f — тип формального параметра, а a — тип аргумента.

  1. Точное совпадение: a и f имеют один и тот же тип.
  2. Соответствие литералам: a — целое литеральное значение v, а f — тип целого со знаком или без знака, и v находится в диапазоне f. Или: a — литеральное значение с плавающей точкой v, а f — тип с плавающей точкой, и v находится в диапазоне f.
  3. Соответствие обобщениям: f — обобщённый тип, и a соответствует, например, a — это int, а f — обобщённый (ограниченный) тип параметра (как в [T] или [T: int|char]).
  4. Соответствие поддиапазонам или подтипам: a — это range[T], а T соответствует f точно. Или: a является подтипом f.
  5. Соответствие целого преобразования: a преобразуется в f, а f и a — это целые или типы с плавающей точкой.
  6. Соответствие преобразованию: a преобразуется в f, возможно, с помощью определяемого пользователем converter.

Каждый операнд может принадлежать к одной из вышеуказанных категорий; категория операнда с наивысшим приоритетом. Список выше представлен в порядке приоритета. Если у кандидата больше совпадений с более высоким приоритетом, чем у всех других кандидатов, он выбирается как разрешённый символ.

Например, если кандидат с одним точным соответствием сравнивается с кандидатом с несколькими обобщёнными соответствиями и нулевым точным соответствием, то кандидат с точным соответствием побеждает.

Ниже приведён псевдокод для интерпретации соответствия категориям, count(p, m) подсчитывает количество соответствий категории соответствия m для процедуры p.

Процедура p соответствует лучше, чем процедура q, если следующий алгоритм возвращает значение true:

for each matching category m in ["exact match", "literal match",
                                "generic match", "subtype match",
                                "integral match", "conversion match"]:
  if count(p, m) > count(q, m): return true
  elif count(p, m) == count(q, m):
    discard "continue with next category m"
  else:
    return false
return "ambiguous"

Второе испытание: сравнение иерархического порядка

Иерархический порядок типа аналогичен его относительной специфичности. Рассмотрим определённый тип:

type A[T] = object

Соответствующие формальные параметры для этого типа включают T, object, A, A[...] и A[C], где C — конкретный тип, A[...] — композиция обобщённого типа класса, а T — свободная переменная обобщённого типа. Этот список представлен в порядке специфичности по отношению к A, поскольку каждая последующая категория сужает множество типов, которые являются членами их набора соответствий.

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

Третье испытание: анализ сложности

Небольшое уточнение: в то время как соответствие категориям обрабатывает все формальные параметры кандидата одновременно (порядок не имеет значения), сравнение специфичности и анализ сложности работают с каждым формальным параметром по одному. Следующее является последним испытанием для устранения неоднозначности выбора символа, когда у пары формальных параметров одинаковый иерархический порядок.

Сложность типа в основном определяется количеством модификаторов и глубиной структуры. Определение с наибольшей сложностью побеждает. Рассмотрим следующие типы:

type
  A[T] = object
  B[T, H] = object

Примечание: нижеприведённые примеры не являются исчерпывающими.

Мы скажем, что:

  1. A[T] имеет более высокую сложность, чем A
  2. var A[T] имеет более высокую сложность, чем A[T]
  3. A[A[T]] имеет более высокую сложность, чем A[T]
  4. B[T, H] имеет более высокую сложность, чем A[T] (A и B здесь несовместимы, но существуют сложные варианты этого)
  5. B[ptr T, H] имеет более высокую сложность, чем B[T, H]

Некоторые примеры

proc takesInt(x: int) = echo "int"
proc takesInt[T](x: T) = echo "T"
proc takesInt(x: int16) = echo "int16"

takesInt(4) # "int"
var x: int32
takesInt(x) # "T"
var y: int16
takesInt(y) # "int16"
var z: range[0..4] = 0
takesInt(z) # "T"

Если аргумент a соответствует как типу параметра f от p, так и типу параметра g от q через отношение подтипизации, то учитывается глубина наследования:

type
  A = object of RootObj
  B = object of A
  C = object of B

proc p(obj: A) =
  echo "A"

proc p(obj: B) =
  echo "B"

var c = C()
# not ambiguous, calls 'B', not 'A' since B is a subtype of A
# but not vice versa:
p(c)

proc pp(obj: A, obj2: B) = echo "A B"
proc pp(obj: B, obj2: A) = echo "B A"

# but this is ambiguous:
pp(c, c)

Аналогично, для обобщённых соответствий предпочтительнее наиболее специализированный обобщённый тип (который всё ещё соответствует):

proc gen[T](x: ref ref T) = echo "ref ref T"
proc gen[T](x: ref T) = echo "ref T"
proc gen[T](x: T) = echo "T"

var ri: ref int
gen(ri) # "ref T"

Переменные типов соответствуют

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

Например:

type A
proc p[T: A](param: T)
proc p[T: object](param: T)

Эти подписи не являются неоднозначными для конкретного типа A, даже если формальные параметры совпадают ("T" == "T"). Вместо этого T рассматривается как переменная в этом (T ?= T), в зависимости от связанного типа T во время разрешения перегрузки.

Перегрузка, основанная на 'var T'

Если формальный параметр f имеет тип var T, в дополнение к обычной проверке типа, аргумент проверяется как l-значение. var T соответствует лучше, чем только T.

proc sayHi(x: int): string =
  # matches a non-var int
  result = $x
proc sayHi(x: var int): string =
  # matches a var int
  result = $(x + 10)

proc sayHello(x: int) =
  var m = x # a mutable version of x
  echo sayHi(x) # matches the non-var version of sayHi
  echo sayHi(m) # matches the var version of sayHi

sayHello(3) # 3
            # 13

Ленивое разрешение типа для неопределённых

Примечание: нерасшифрованное выражение — это выражение, для которого не было выполнено ни поиск символов, ни проверка типа.

Поскольку шаблоны и макросы, которые не объявлены как immediate, участвуют в разрешении перегрузки, необходимо иметь возможность передавать неразрешённые выражения шаблону или макросу. Это достигается метатипом untyped:

template rem(x: untyped) = discard

rem unresolvedExpression(undeclaredIdentifier)

Параметр типа untyped всегда соответствует любому аргументу (при условии, что ему передаётся какой-либо аргумент).

Но нужно следить, потому что другие перегрузки могут инициировать разрешение аргумента:

template rem(x: untyped) = discard
proc rem[T](x: T) = discard

# undeclared identifier: 'unresolvedExpression'
rem unresolvedExpression(undeclaredIdentifier)

untyped и varargs[untyped] — единственные метатипы, которые ленивы в этом смысле, другие метатипы typed и typedesc не ленивы.

Соответствие varargs

См. Varargs.

iterable

Вызываемая iterator, возвращающая тип T, может быть передана шаблону или макросу через параметр, типизированный как untyped (для неразрешённых выражений) или тип класса iterable или iterable[T] (после проверки типа и разрешения перегрузки).

iterator iota(n: int): int =
  for i in 0..<n: yield i

template toSeq2[T](a: iterable[T]): seq[T] =
  var ret: seq[T]
  assert a.typeof is T
  for ai in a: ret.add ai
  ret

assert iota(3).toSeq2 == @[0, 1, 2]
assert toSeq2(5..7) == @[5, 6, 7]
assert not compiles(toSeq2(@[1,2])) # seq[int] is not an iterable
assert toSeq2(items(@[1,2])) == @[1, 2] # but items(@[1,2]) is

Устранение неоднозначности перегрузки

Для вызовов процедур выполняется "разрешение перегрузки". Существует более слабая форма разрешения перегрузки, называемая устранением неоднозначности перегрузки, которая выполняется, когда перегруженный символ используется в контексте, где доступна дополнительная информация о типе. Пусть p — перегруженный символ. Эти контексты:

  • В вызове функции q(..., p, ...), когда соответствующий формальный параметр q имеет тип proc. Если сам q перегружен, необходимо рассмотреть декартово произведение всех интерпретаций q и p.
  • В конструкторе объекта Obj(..., field: p, ...), когда field — тип proc. Аналогичные правила существуют для конструкторов массивов/множеств/кортежей.
  • В объявлении, таком как x: T = p, когда T — тип proc.

Как обычно, неоднозначные совпадения приводят к ошибке компиляции.

Перегрузка с именованными аргументами

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

proc foo(x: int) =
  echo "Using x: ", x
proc foo(y: int) =
  echo "Using y: ", y

foo(x = 2) # Using x: 2
foo(y = 2) # Using y: 2

Если имя параметра не указано в таких случаях, возникает ошибка неоднозначности.

Утверждения и выражения

Nim использует обычную парадигму «утверждение/выражение»: Утверждения не производят значения в отличие от выражений. Однако некоторые выражения являются утверждениями.

Утверждения делятся на простые утверждения и сложные утверждения. Простые утверждения — это утверждения, которые не могут содержать другие утверждения, такие как присваивания, вызовы или утверждение return; сложные утверждения могут содержать другие утверждения. Чтобы избежать проблемы висящего else, сложные утверждения всегда должны быть отступом. Подробности можно найти в грамматике.

Выражение списка утверждений

Утверждения также могут встречаться в контексте выражения, похожего на (stmt1; stmt2; ...; ex). Это называется выражением списка утверждений или (;). Тип (stmt1; stmt2; ...; ex) — это тип ex. Все остальные утверждения должны быть типа void. (Можно использовать discard для получения типа void.) (;) не вводит новую область видимости.

Утверждение discard

Пример:

proc p(x, y: int): int =
  result = x + y

discard p(3, 4) # discard the return value of `p`

Утверждение discard оценивает своё выражение для побочных эффектов и отбрасывает возвращаемое значение выражения. Его следует использовать только тогда, когда известно, что игнорирование этого значения не вызовет проблем.

Игнорирование возвращаемого значения процедуры без использования утверждения discard — это статическая ошибка.

Возвращаемое значение можно неявно игнорировать, если вызываемый proc/итератор был объявлен с помощью директивы discardable:

proc p(x, y: int): int {.discardable.} =
  result = x + y

p(3, 4) # now valid

Однако директива discardable не работает для шаблонов, так как шаблоны подставляют AST на месте. Например:

{.push discardable .}
template example(): string = "https://nim-lang.org"
{.pop.}

example()

Этот шаблон будет разрешен в "https://nim-lang.org", который является строковой литералом, и поскольку {.discardable.} не применяется к литералам, компилятор выдаст ошибку.

Пустое discard утверждение часто используется как пустое утверждение:

proc classify(s: string) =
  case s[0]
  of SymChars, '_': echo "an identifier"
  of '0'..'9': echo "a number"
  else: discard

Контекст void

В списке утверждений каждое выражение, кроме последнего, должно иметь тип void. Кроме этого правила, присвоение встроенному символу result также запускает обязательный контекст void для последующих выражений:

proc invalid*(): string =
  result = "foo"
  "invalid"  # Error: value of type 'string' has to be discarded
proc valid*(): string =
  let x = 317
  "valid"

Утверждение var

Утверждения var объявляют новые локальные и глобальные переменные и инициализируют их. Можно использовать список переменных, разделенных запятыми, для задания переменных одного типа:

var
  a: int = 0
  x, y, z: int

Если задан инициализатор, тип можно опустить: переменная будет того же типа, что и выражение инициализации. Переменные всегда инициализируются значением по умолчанию, если нет выражения инициализации. Значение по умолчанию зависит от типа и всегда равно нулю в двоичном формате.

Тип Значение по умолчанию
любой целочисленный тип 0
любое число с плавающей точкой 0.0
char '\0'
bool false
тип ref или указатель nil
процедурный тип nil
последовательность @[]
строка ""
tuple[x: A, y: B, ...] (zeroDefault(A), zeroDefault(B), ...) (аналогично для объектов)
array[0..., T] [zeroDefault(T), ...]
range[T] default(T); это может быть вне допустимого диапазона
T = enum cast[T](0); это может быть недопустимое значение

Неявную инициализацию можно избежать по соображениям оптимизации с помощью директивы noinit:

var
  a {.noinit.}: array[0..1023, char]

Если процедура помечена директивой noinit, это относится к её неявной result переменной:

proc returnUndefinedValue: int {.noinit.} = discard

Неявную инициализацию также можно предотвратить с помощью директивы типа requiresInit. Компилятор требует явной инициализации объекта и всех его полей. Однако он выполняет анализ потока управления, чтобы доказать, что переменная была инициализирована, и не полагается на синтаксические свойства:

type
  MyObject {.requiresInit.} = object

proc p() =
  # the following is valid:
  var x: MyObject
  if someCondition():
    x = a()
  else:
    x = a()
  # use x

Директива requiresInit также может применяться к типам distinct.

Учитывая следующие отдельные определения типов:

type
  Foo = object
    x: string
  
  DistinctFoo {.requiresInit, borrow: `.`.} = distinct Foo
  DistinctString {.requiresInit.} = distinct string

Следующие блоки кода не будут компилироваться:

var foo: DistinctFoo
foo.x = "test"
doAssert foo.x == "test"
var s: DistinctString
s = "test"
doAssert string(s) == "test"

Но эти будут компилироваться успешно:

let foo = DistinctFoo(Foo(x: "test"))
doAssert foo.x == "test"
let s = DistinctString("test")
doAssert string(s) == "test"

Утверждение let

Утверждение let объявляет новые локальные и глобальные переменные с одним присваиванием и связывает значение с ними. Синтаксис такой же, как и у утверждения var, за исключением того, что ключевое слово var заменяется ключевым словом let. Переменные let не являются l-значениями, поэтому их нельзя передавать в параметры var, а также нельзя получить их адрес. Они не могут получать новые значения.

Для переменных let доступны те же директивы, что и для обычных переменных.

Поскольку утверждения let являются неизменяемыми после создания, им необходимо определить значение при объявлении. Единственное исключение из этого — если применяется директива {.importc.} (или любая другая директива importX), в этом случае значение ожидается из кода нативного уровня, обычно C/C++ const.

Специальный идентификатор _ (подчёркивание)

Идентификатор _ имеет специальное значение в объявлениях. Любое определение с именем _ не будет добавлено в область видимости, что означает, что определение вычисляется, но не может быть использовано. В результате имя _ можно неопределенно переопределять.

let _ = 123
echo _ # error
let _ = 456 # compiles

Распаковка кортежей

В утверждениях var, let или const может выполняться распаковка кортежей. Специальный идентификатор _ можно использовать для игнорирования некоторых частей кортежа:

proc returnsTuple(): (int, int, int) = (4, 2, 3)

let (x, _, z) = returnsTuple()

Это рассматривается как синтаксический сахар для примерно следующего:

let
  tmpTuple = returnsTuple()
  x = tmpTuple[0]
  z = tmpTuple[2]

Для утверждений var или let, если выражение значения является литералом кортежа, каждое выражение непосредственно расширяется в присвоение без использования временной переменной.

let (x, y, z) = (1, 2, 3)
# becomes
let
  x = 1
  y = 2
  z = 3

Распаковка кортежей также может быть вложенной:

proc returnsNestedTuple(): (int, (int, int), int, int) = (4, (5, 7), 2, 3)

let (x, (_, y), _, z) = returnsNestedTuple()

Раздел const

Раздел const объявляет константы, значения которых являются константными выражениями:

import std/[strutils]
const
  roundPi = 3.1415
  constEval = contains("abc", 'b') # computed at compile time!

После объявления символ константы может использоваться как константное выражение.

Часть значения объявления константы открывает новую область видимости для каждой константы, поэтому символы, объявленные в значении константы, недоступны за её пределами.

const foo = (var a = 1; a)
const bar = a # error
let baz = a # error

Подробности см. в разделе Константы и константные выражения.

Статическое утверждение/выражение

Статическое утверждение/выражение явно требует выполнения во время компиляции. Даже некоторый код, имеющий побочные эффекты, разрешен в статическом блоке:

static:
  echo "echo at compile time"

static также может использоваться как процедура.

proc getNum(a: int): int = a

# Below calls "echo getNum(123)" at compile time.
static:
  echo getNum(123)

# Below call evaluates the "getNum(123)" at compile time, but its
# result gets used at run time.
echo static(getNum(123))

Существуют ограничения на то, какой код Nim может быть выполнен во время компиляции; см. Ограничения на выполнение во время компиляции для получения подробностей. Если компилятор не может выполнить блок во время компиляции, это является статической ошибкой.

Утверждение if

Пример:

var name = readLine(stdin)

if name == "Andreas":
  echo "What a nice name!"
elif name == "":
  echo "Don't you have a name?"
else:
  echo "Boring name..."

Утверждение if — это простой способ создания ветвления в потоке управления: выражение после ключевого слова if оценивается; если оно истинно, выполняются соответствующие утверждения после :. В противном случае, выражение после elif оценивается (если есть ветвь elif), если оно истинно, выполняются соответствующие утверждения после :. Это продолжается до последнего elif. Если все условия ложны, выполняется часть else. Если нет части else, выполнение продолжается со следующего утверждения.

В утверждениях if новые области видимости начинаются сразу после ключевых слов if/elif/else и заканчиваются после соответствующего блока then. Для наглядности области видимости заключены в {| |} в следующем примере:

if {| (let m = input =~ re"(\w+)=\w+"; m.isMatch):
  echo "key ", m[0], " value ", m[1]  |}
elif {| (let m = input =~ re""; m.isMatch):
  echo "new m in this scope"  |}
else: {|
  echo "m not declared here"  |}

Утверждение case

Пример:

let line = readline(stdin)
case line
of "delete-everything", "restart-computer":
  echo "permission denied"
of "go-for-a-walk":     echo "please yourself"
elif line.len == 0:     echo "empty" # optional, must come after `of` branches
else:                   echo "unknown command" # ditto

# indentation of the branches is also allowed; and so is an optional colon
# after the selecting expression:
case readline(stdin):
  of "delete-everything", "restart-computer":
    echo "permission denied"
  of "go-for-a-walk":     echo "please yourself"
  else:                   echo "unknown command"

Утверждение case аналогично утверждению if, но оно представляет собой многоветвящую выборку. Выражение после ключевого слова case оценивается, и если его значение находится в списке фрагментов, выполняются соответствующие утверждения (после ключевого слова of). Если значение не находится ни в каком заданном списке фрагментов, выполняются заключительные части elif и else, используя ту же семантику, что и для утверждения if, и elif обрабатывается так же, как и else: if. Если нет частей else или elif, и не все возможные значения, которые может содержать expr, не встречаются в списке фрагментов, возникает статическая ошибка. Это относится только к выражениям порядковых типов. «Все возможные значения» expr определяются типом expr. Для подавления статической ошибки следует использовать else: discard.

В утверждениях case допускаются только порядковые типы, числа с плавающей точкой, строки и cстроки в качестве значений.

Для не-порядковых типов невозможно перечислить все возможные значения, поэтому они всегда требуют части else. Исключением из этого правила является тип string, для которого в настоящее время не требуется заключительная ветвь else или elif; не определено, будет ли это работать в будущих версиях.

Поскольку операторы case проверяются на исчерпываемость во время семантического анализа, значение в каждом of ветвлении должно быть константным выражением. Это ограничение также позволяет компилятору генерировать более производительный код.

В качестве специального семантического расширения, выражение в of ветвлении оператора case может вычисляться как конструктор множества или массива; множество или массив затем расширяются в список его элементов:

const
  SymChars: set[char] = {'a'..'z', 'A'..'Z', '\x80'..'\xFF'}

proc classify(s: string) =
  case s[0]
  of SymChars, '_': echo "an identifier"
  of '0'..'9': echo "a number"
  else: echo "other"

# is equivalent to:
proc classify(s: string) =
  case s[0]
  of 'a'..'z', 'A'..'Z', '\x80'..'\xFF', '_': echo "an identifier"
  of '0'..'9': echo "a number"
  else: echo "other"

Оператор case не генерирует l-значение, поэтому следующий пример не сработает:

type
  Foo = ref object
    x: seq[string]

proc get_x(x: Foo): var seq[string] =
  # doesn't work
  case true
  of true:
    x.x
  else:
    x.x

var foo = Foo(x: @[])
foo.get_x().add("asd")

Это можно исправить, явно используя result или return:

proc get_x(x: Foo): var seq[string] =
  case true
  of true:
    result = x.x
  else:
    result = x.x

Оператор when

Пример:

when sizeof(int) == 2:
  echo "running on a 16 bit system!"
elif sizeof(int) == 4:
  echo "running on a 32 bit system!"
elif sizeof(int) == 8:
  echo "running on a 64 bit system!"
else:
  echo "cannot happen!"

Оператор when почти идентичен оператору if с некоторыми исключениями:

  • Каждое условие (expr) должно быть константным выражением (типа bool).
  • Условные операторы не открывают новую область видимости.
  • Операторы, относящиеся к выражению, вычисленному как истинному, переводятся компилятором; другие операторы не проверяются на семантику! Однако каждое условие проверяется на семантику.

Оператор when позволяет применять техники условной компиляции. В качестве специального синтаксического расширения, конструкция when также доступна внутри определений object.

Оператор when nimvm

nimvm — специальный символ, который может использоваться как выражение оператора when nimvm, чтобы различать пути выполнения во время компиляции и в исполняемом файле.

Пример:

proc someProcThatMayRunInCompileTime(): bool =
  when nimvm:
    # This branch is taken at compile time.
    result = true
  else:
    # This branch is taken in the executable.
    result = false
const ctValue = someProcThatMayRunInCompileTime()
let rtValue = someProcThatMayRunInCompileTime()
assert(ctValue == true)
assert(rtValue == false)

Оператор when nimvm должен удовлетворять следующим требованиям:

  • Его выражение должно всегда быть nimvm. Более сложные выражения не допускаются.
  • Он не должен содержать elif ветвей.
  • Он должен содержать else ветвь.
  • Код во ветвях не должен влиять на семантику кода, следующего за оператором when nimvm. Например, он не должен определять символы, используемые в последующем коде.

Оператор return

Пример:

return 40 + 2

Оператор return завершает выполнение текущей процедуры. Он разрешен только в процедурах. Если есть expr, это синтаксический сахар для:

result = expr
return result

return без выражения — короткая запись для return result, если у процедуры есть тип возвращаемого значения. Переменная result всегда является возвращаемым значением процедуры. Она автоматически объявляется компилятором. Как и все переменные, result инициализируется нулём (в двоичном формате):

proc returnZero(): int =
  # implicitly returns 0

Оператор yield

Пример:

yield (1, 2, 3)

Оператор yield используется вместо оператора return в итераторах. Он допустим только в итераторах. Выполнение возвращается в тело цикла for, который вызвал итератор. Yield не завершает процесс итерации, но выполнение передается обратно в итератор, если начинается следующая итерация. Смотрите раздел об итераторах (Итераторы и оператор for) для получения дополнительной информации.

Оператор блока

Пример:

var found = false
block myblock:
  for i in 0..3:
    for j in 0..3:
      if a[j][i] == 7:
        found = true
        break myblock # leave the block, in this case both for-loops
echo found

Оператор блока — средство группировки операторов в (именованный) block. Внутри блока разрешен оператор break, чтобы немедленно покинуть блок. Оператор break может содержать имя окружающего блока, чтобы указать, какой блок нужно покинуть.

Оператор break

Пример:

break

Оператор break используется для немедленного выхода из блока. Если указано symbol, это имя окружающего блока, из которого нужно выйти. Если оно отсутствует, покидается самый внутренний блок.

Оператор while

Пример:

echo "Please tell me your password:"
var pw = readLine(stdin)
while pw != "12345":
  echo "Wrong password! Next try:"
  pw = readLine(stdin)

Оператор while выполняется до тех пор, пока выражение expr не станет ложным. Бесконечные циклы не являются ошибкой. Операторы while открывают implicit block, так что их можно покинуть оператором break.

Оператор continue

Оператор continue приводит к немедленной следующей итерации окружающего цикла. Он разрешён только внутри цикла. Оператор continue — синтаксический сахар для вложенного блока:

while expr1:
  stmt1
  continue
  stmt2

Эквивалентно:

while expr1:
  block myBlockName:
    stmt1
    break myBlockName
    stmt2

Оператор ассемблера

Прямое встраивание кода ассемблера в код Nim поддерживается небезопасным оператором asm. Идентификаторы в коде ассемблера, которые ссылаются на идентификаторы Nim, должны быть заключены в специальный символ, который можно указать в преамбуле оператора. По умолчанию специальный символ — '`':

{.push stackTrace:off.}
proc addInt(a, b: int): int =
  # a in eax, and b in edx
  asm """
      mov eax, `a`
      add eax, `b`
      jno theEnd
      call `raiseOverflow`
    theEnd:
  """
{.pop.}

Если используется ассемблер GNU, кавычки и новые строки вставляются автоматически:

proc addInt(a, b: int): int =
  asm """
    addl %%ecx, %%eax
    jno 1
    call `raiseOverflow`
    1:
    :"=a"(`result`)
    :"a"(`a`), "c"(`b`)
  """

Вместо:

proc addInt(a, b: int): int =
  asm """
    "addl %%ecx, %%eax\n"
    "jno 1\n"
    "call `raiseOverflow`\n"
    "1: \n"
    :"=a"(`result`)
    :"a"(`a`), "c"(`b`)
  """

Оператор using

Оператор using обеспечивает синтаксическую удобство в модулях, где одинаковые имена параметров и типы используются многократно. Вместо:

proc foo(c: Context; n: Node) = ...
proc bar(c: Context; n: Node, counter: int) = ...
proc baz(c: Context; n: Node) = ...

Можно сообщить компилятору о соглашении, что параметр с именем c по умолчанию должен иметь тип Context, n должен иметь тип Node и т.д.:

using
  c: Context
  n: Node
  counter: int

proc foo(c, n) = ...
proc bar(c, n, counter) = ...
proc baz(c, n) = ...

proc mixedMode(c, n; x, y: int) =
  # 'c' is inferred to be of the type 'Context'
  # 'n' is inferred to be of the type 'Node'
  # But 'x' and 'y' are of type 'int'.

Раздел using использует тот же синтаксис группировки, основанный на отступах, что и в разделе var или let.

Обратите внимание, что using не применяется к template, так как параметры шаблонов без типа по умолчанию имеют тип system.untyped.

Возможна смесь параметров, которые должны использовать объявление using, с параметрами, явно типизированными; в этом случае между ними требуется точка с запятой.

Выражение if

Выражение if почти идентично оператору if, но является выражением. Эта функция аналогична тройным операторам в других языках. Пример:

var y = if x > 8: 9 else: 10

Выражение if всегда возвращает значение, поэтому часть else обязательна. Разрешены также части elif.

Выражение when

Аналогично выражению if, но соответствует оператору when.

Выражение case

Выражение case очень похоже на оператор case:

var favoriteFood = case animal
  of "dog": "bones"
  of "cat": "mice"
  elif animal.endsWith"whale": "plankton"
  else:
    echo "I'm not sure what to serve, but everybody loves ice cream"
    "ice cream"

Как видно из примера выше, выражение case также может вызывать побочные эффекты. Если для ветви указано несколько операторов, Nim использует последнее выражение в качестве результата.

Выражение блока

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

let a = block:
  var fib = @[0, 1]
  for i in 0..10:
    fib.add fib[^1] + fib[^2]
  fib

Конструктор таблицы

Конструктор таблицы — это синтаксический сахар для конструктора массива:

{"key1": "value1", "key2", "key3": "value2"}

# is the same as:
[("key1", "value1"), ("key2", "value2"), ("key3", "value2")]

Пустая таблица может быть записана как {:} (в отличие от пустого множества, которое является {}), что является ещё одним способом записи конструктора пустого массива []. Этот несколько необычный способ поддержки таблиц имеет много преимуществ:

  • Порядок пар (ключ, значение) сохраняется, что упрощает поддержку упорядоченных словарей, например, с помощью {key: val}.newOrderedTable.
  • Литтерал таблицы может быть помещен в раздел const, и компилятор легко поместит его в раздел данных исполняемого файла, как он это делает для массивов, и сгенерированный раздел данных потребует минимального объема памяти.
  • Все реализации таблиц обрабатываются одинаково с синтаксической точки зрения.
  • Помимо минимального синтаксического сахара, ядру языка не нужно знать о таблицах.

Преобразования типов

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

Обычные процедуры часто предпочтительнее преобразований типов в Nim: например, $ по умолчанию является оператором toString, а toFloat и toInt могут использоваться для преобразования из чисел с плавающей точкой в целые числа и наоборот.

Преобразование типа также может использоваться для разрешения перегруженных процедур:

proc p(x: int) = echo "int"
proc p(x: string) = echo "string"

let procVar = (proc(x: string))(p)
procVar("a")

Так как операции над беззнаковыми числами циклически повторяются и не проверяются, то те же свойства имеют и преобразования к беззнаковым целым числам и между беззнаковыми целыми числами. Основная причина этого — лучшая совместимость с языком программирования C при переносе алгоритмов из C в Nim.

Примечание: Исторически операции были неконтролируемыми, а преобразования иногда проверялись, но начиная с версии 1.0.4 этого документа и реализации языка преобразования также всегда неконтролируемые.

Приведение типов

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

cast[int](x)

Целевой тип приведения должен быть конкретным типом, например, целевой тип, который является типом класса (который не является конкретным), будет недопустимым:

type Foo = int or float
var x = cast[Foo](1) # Error: cannot cast to a non concrete type: 'Foo'

Приведение типов не следует путать с преобразованием типов, как упоминалось в предыдущем разделе. В отличие от преобразований типов, приведение типа не может изменить основополагающий битовый шаблон данных, преобразуемых (за исключением того, что размер целевого типа может отличаться от размера исходного типа). Приведение напоминает типовое подражание в других языках или функции reinterpret_cast и bit_cast в C++.

Если размер целевого типа больше размера исходного типа, оставшаяся память обнуляется.

Оператор addr

Оператор addr возвращает адрес l-значения. Если тип расположения равен T, то результат оператора addr имеет тип ptr T. Адрес всегда представляет собой неотслеживаемую ссылку. Получение адреса объекта, расположенного в стеке, является небезопасным, так как указатель может существовать дольше, чем объект в стеке, и, следовательно, может ссылаться на несуществующий объект. Можно получить адрес переменных. Для лучшей совместимости с другими компилируемыми языками, такими как C, также можно получить адрес переменной let, параметра или переменной цикла for:

let t1 = "Hello"
var
  t2 = t1
  t3 : pointer = addr(t2)
echo repr(addr(t2))
# --> ref 0x7fff6b71b670 --> 0x10bb81050"Hello"
echo cast[ptr string](t3)[]
# --> Hello
# The following line also works
echo repr(addr(t1))

Оператор unsafeAddr

Оператор unsafeAddr является устаревшим алиасом для оператора addr:

let myArray = [1, 2, 3]
foreignProcThatTakesAnAddr(unsafeAddr myArray)

Процедуры

То, что большинство языков программирования называют методами или функциями, в Nim называется процедурами. Объявление процедуры состоит из идентификатора, нуля или более формальных параметров, типа возвращаемого значения и блока кода. Формальные параметры объявляются как список идентификаторов, разделенных запятыми или точкой с запятой. Параметру присваивается тип с помощью : typename. Тип применяется ко всем параметрам непосредственно перед ним, пока не будет достигнут либо начало списка параметров, разделитель точкой с запятой или уже типизированный параметр. Точка с запятой может использоваться для более четкого разделения типов и последующих идентификаторов.

# Using only commas
proc foo(a, b: int, c, d: bool): int

# Using semicolon for visual distinction
proc foo(a, b: int; c, d: bool): int

# Will fail: a is untyped since ';' stops type propagation.
proc foo(a; b: int; c, d: bool): int

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

# b is optional with 47 as its default value.
proc foo(a: int, b: int = 47): int

Параметры могут быть объявлены мутабельными, позволяя процедуре изменять эти аргументы, с помощью модификатора типа var.

# "returning" a value to the caller through the 2nd argument
# Notice that the function uses no actual return value at all (ie void)
proc foo(inp: int, outp: var int) =
  outp = inp + 47

Если объявление процедуры не имеет тела, это объявление вперед. Если процедура возвращает значение, тело процедуры может получить доступ к неявно объявленной переменной с именем result, представляющей возвращаемое значение. Процедуры могут быть перегружены. Алгоритм разрешения перегрузки определяет, какая процедура лучше всего подходит для аргументов. Пример:

proc toLower(c: char): char = # toLower for characters
  if c in {'A'..'Z'}:
    result = chr(ord(c) + (ord('a') - ord('A')))
  else:
    result = c

proc toLower(s: string): string = # toLower for strings
  result = newString(len(s))
  for i in 0..len(s) - 1:
    result[i] = toLower(s[i]) # calls toLower for characters; no recursion!

Процедуру можно вызвать многими способами:

proc callme(x, y: int, s: string = "", c: char, b: bool = false) = ...

# call with positional arguments      # parameter bindings:
callme(0, 1, "abc", '\t', true)       # (x=0, y=1, s="abc", c='\t', b=true)
# call with named and positional arguments:
callme(y=1, x=0, "abd", '\t')         # (x=0, y=1, s="abd", c='\t', b=false)
# call with named arguments (order is not relevant):
callme(c='\t', y=1, x=0)              # (x=0, y=1, s="", c='\t', b=false)
# call as a command statement: no () needed:
callme 0, 1, "abc", '\t'              # (x=0, y=1, s="abc", c='\t', b=false)

Процедура может вызывать себя рекурсивно.

Операторы — это процедуры со специальным операторным символом в качестве идентификатора:

proc `$` (x: int): string =
  # converts an integer to a string; this is a prefix operator.
  result = intToStr(x)

Операторы с одним параметром являются префиксными операторами, операторы с двумя параметрами — инфиксными операторами. (Однако, синтаксический анализатор различает их по положению оператора в выражении.) Нет способа объявить постфиксные операторы: все постфиксные операторы являются встроенными и обрабатываются грамматикой явно.

Любой оператор можно вызвать как обычную процедуру с помощью обозначения `opr`. (Таким образом, оператор может иметь более двух параметров):

proc `*+` (a, b, c: int): int =
  # Multiply and add
  result = a * b + c

assert `*+`(3, 4, 6) == `+`(`*`(a, b), c)

Маркер экспорта

Если объявленный символ помечен звёздочкой, он экспортируется из текущего модуля:

proc exportedEcho*(s: string) = echo s
proc `*`*(a: string; b: int): string =
  result = newStringOfCap(a.len * b)
  for i in 1..b: result.add a

var exportedVar*: int
const exportedConst* = 78
type
  ExportedType* = object
    exportedField*: int

Синтаксис вызова метода

Для объектно-ориентированного программирования можно использовать синтаксис obj.methodName(args) вместо methodName(obj, args). Скобки можно опустить, если дополнительных аргументов нет: obj.len (вместо len(obj)).

Этот синтаксис вызова метода не ограничен объектами, его можно использовать для предоставления любого типа первого аргумента для процедур:

echo "abc".len # is the same as echo len "abc"
echo "abc".toUpper()
echo {'a', 'b', 'c'}.card
stdout.writeLine("Hallo") # the same as writeLine(stdout, "Hallo")

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

Синтаксис вызова метода конфликтует с явными обобщенными инициализациями: p[T](x) нельзя записать как x.p[T], потому что x.p[T] всегда анализируется как (x.p)[T].

См. также: Ограничения синтаксиса вызова метода.

Обозначение [: ] разработано для уменьшения этой проблемы: x.p[:T] переписывается синтаксическим анализатором в p[T](x), x.p[:T](y) переписывается в p[T](x, y). Обратите внимание, что [: ] не имеет представления AST, переписывание выполняется непосредственно на этапе синтаксического анализа.

Свойства

Nim не нуждается в свойствах-get: Обычные процедуры-get, вызываемые с помощью синтаксиса вызова метода, достигают того же результата. Но установка значения отличается; для этого нужен специальный синтаксис установки:

# Module asocket
type
  Socket* = ref object of RootObj
    host: int # cannot be accessed from the outside of the module

proc `host=`*(s: var Socket, value: int) {.inline.} =
  ## setter of hostAddr.
  ## This accesses the 'host' field and is not a recursive call to
  ## `host=` because the builtin dot access is preferred if it is
  ## available:
  s.host = value

proc host*(s: Socket): int {.inline.} =
  ## getter of hostAddr
  ## This accesses the 'host' field and is not a recursive call to
  ## `host` because the builtin dot access is preferred if it is
  ## available:
  s.host
# module B
import asocket
var s: Socket
new s
s.host = 34  # same as `host=`(s, 34)

Процедура, определенная как f= (с заключительным =), называется сеттером. Сеттер можно вызвать явно с помощью обычного обозначения обратных кавычек:

proc `f=`(x: MyObject; value: string) =
  discard

`f=`(myObject, "value")

f= можно вызвать неявно в шаблоне x.f = value только в том случае, если тип x не имеет поля с именем f или если f не виден в текущем модуле. Эти правила гарантируют, что поля объектов и обработчики доступа могут иметь одинаковые имена. Внутри модуля x.f всегда интерпретируется как доступ к полю, а вне модуля — как вызов процедуры обработчика доступа.

Синтаксис вызова команды

Процедуры можно вызывать без (), если вызов является синтаксически оператором. Этот синтаксис вызова команды также работает для выражений, но за ним может следовать только один аргумент. Это ограничение означает, что echo f 1, f 2 анализируется как echo(f(1), f(2)), а не как echo(f(1, f(2))). В этом случае для предоставления ещё одного аргумента можно использовать синтаксис вызова метода:

proc optarg(x: int, y: int = 0): int = x + y
proc singlearg(x: int): int = 20*x

echo optarg 1, " ", singlearg 2  # prints "1 40"

let fail = optarg 1, optarg 8   # Wrong. Too many arguments for a command call
let x = optarg(1, optarg 8)  # traditional procedure call with 2 arguments
let y = 1.optarg optarg 8    # same thing as above, w/o the parenthesis
assert x == y

Синтаксис вызова команды также не может иметь сложных выражений в качестве аргументов. Например: анонимные процедуры, if, case или try. Вызовы функций без аргументов по-прежнему нуждаются в () для различения вызова и функции как первого класса значения.

Замыкания

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

Создание замыканий в циклах

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

Анонимные процедуры

Безымянные процедуры могут использоваться как лямбда-выражения для передачи в другие процедуры:

var cities = @["Frankfurt", "Tokyo", "New York", "Kyiv"]

cities.sort(proc (x, y: string): int =
  cmp(x.len, y.len))

Процедуры в качестве выражений могут появляться как вложенные процедуры, так и внутри исполняемого кода верхнего уровня. Модуль sugar содержит макрос =>, который позволяет использовать более лаконичный синтаксис для анонимных процедур, напоминающий лямбда-выражения в таких языках, как JavaScript, C#, и т. д.

Обозначение do

В качестве специального удобного обозначения, сохраняющего большинство элементов обычного выражения процедуры, можно использовать ключевое слово do для передачи анонимных процедур в процедуры:

var cities = @["Frankfurt", "Tokyo", "New York", "Kyiv"]

sort(cities) do (x, y: string) -> int:
  cmp(x.len, y.len)

# Less parentheses using the method plus command syntax:
cities = cities.map do (x: string) -> string:
  "City of " & x

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

do со списком параметров или списком пragma соответствует анонимной proc, однако do без параметров или пragma обрабатывается как обычный список операторов. Это позволяет макросам получать как отступы списков операторов в качестве аргумента во встраиваемых вызовах, так и непосредственное отображение синтаксиса процедур Nim.

# Passing a statement list to an inline macro:
macroResults.add quote do:
  if not `ex`:
    echo `info`, ": Check failed: ", `expString`

# Processing a routine definition in a macro:
rpc(router, "add") do (a, b: int) -> int:
  result = a + b

Func

Ключевое слово func вводит сокращение для процедуры noSideEffect.

func binarySearch[T](a: openArray[T]; elem: T): int

Сокращенно от:

proc binarySearch[T](a: openArray[T]; elem: T): int {.noSideEffect.}

Процедуры

Процедура — это символ типа: proc, func, method, iterator, macro, template, converter.

Операторы, связанные с типом

Оператор, связанный с типом, — это proc или func, имя которого начинается с =, но не является оператором (т. е. содержит только символы, такие как ==). Они не связаны с сеттерами (см. Свойства), которые вместо этого заканчиваются на =. Оператор, связанный с типом, объявленный для типа, применяется к типу независимо от того, находится ли оператор в области видимости (включая случай, когда он является закрытым).

# foo.nim:
var witness* = 0
type Foo[T] = object
proc initFoo*(T: typedesc): Foo[T] = discard
proc `=destroy`[T](x: var Foo[T]) = witness.inc # type bound operator

# main.nim:
import foo
block:
  var a = initFoo(int)
  doAssert witness == 0
doAssert witness == 1
block:
  var a = initFoo(int)
  doAssert witness == 1
  `=destroy`(a) # can be called explicitly, even without being in scope
  doAssert witness == 2
# will still be called upon exiting scope
doAssert witness == 3

Операторы, связанные с типом: =destroy, =copy, =sink, =trace, =deepcopy, =wasMoved, =dup.

Эти операции могут быть переопределены вместо перегружены. Это означает, что реализация автоматически подключается к структурам типов. Например, если тип T имеет переопределённый оператор присваивания =, этот оператор также используется для присваиваний типа seq[T].

Поскольку эти операции привязаны к типу, они должны быть привязаны к номинальному типу по соображениям простоты реализации; это означает, что переопределённый deepCopy для ref T фактически привязан к типу T, а не к типу ref T. Это также означает, что нельзя переопределить deepCopy как для ptr T, так и для ref T одновременно, вместо этого должен использоваться отдельный или вспомогательный тип объекта для одного типа указателей.

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

Неперегружаемые встроенные функции

Следующие встроенные процедуры не могут быть перегружены по соображениям простоты реализации (они требуют специализированной семантической проверки):

declared, defined, definedInScope, compiles, sizeof,
is, shallowCopy, getAst, astToStr, spawn, procCall

Таким образом, они ведут себя больше как ключевые слова, чем как обычные идентификаторы; в отличие от ключевого слова, переопределение может затмить определение в модуле system. Из этого списка следующие не должны записываться с помощью обозначения точкой x.f, так как x не может быть проверена на тип до передачи в f:

declared, defined, definedInScope, compiles, getAst, astToStr

Параметры var

Тип параметра может быть префиксным ключевым словом var:

proc divmod(a, b: int; res, remainder: var int) =
  res = a div b
  remainder = a mod b

var
  x, y: int

divmod(8, 5, x, y) # modifies x and y
assert x == 1
assert y == 3

В примере, res и remainder являются var parameters. Параметры var могут быть изменены процедурой, и изменения видны вызывающей стороне. Аргумент, переданный параметру var, должен быть l-значением. Параметры var реализуются как скрытые указатели. Приведенный выше пример эквивалентен:

proc divmod(a, b: int; res, remainder: ptr int) =
  res[] = a div b
  remainder[] = a mod b

var
  x, y: int
divmod(8, 5, addr(x), addr(y))
assert x == 1
assert y == 3

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

proc divmod(a, b: int): tuple[res, remainder: int] =
  (a div b, a mod b)

var t = divmod(8, 5)

assert t.res == 1
assert t.remainder == 3

Можно использовать распаковку кортежей для доступа к полям кортежа:

var (x, y) = divmod(8, 5) # tuple unpacking
assert x == 1
assert y == 3

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

Возвращаемый тип var

Процедура, конвертер или итератор могут возвращать тип var, что означает, что возвращаемое значение является l-значением и может быть изменено вызывающей стороной:

var g = 0

proc writeAccessToG(): var int =
  result = g

writeAccessToG() = 6
assert g == 6

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

proc writeAccessToG(): var int =
  var g = 0
  result = g # Error!

Для итераторов компонент кортежа возвращаемого типа также может иметь тип var:

iterator mpairs(a: var seq[string]): tuple[key: int, val: var string] =
  for i in 0..a.high:
    yield (i, a[i])

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

Безопасность памяти при возвращении по var T обеспечивается простым правилом оперирования: Если result не ссылается на место, указывающее на кучу (то есть в result = X случае, если X включает доступ ptr или ref), то он должен быть получен из первого параметра процедуры:

proc forward[T](x: var T): var T =
  result = x # ok, derived from the first parameter.

proc p(param: var int): var int =
  var x: int
  # we know 'forward' provides a view into the location derived from
  # its first argument 'x'.
  result = forward(x) # Error: location is derived from `x`
                      # which is not p's first parameter and lives
                      # on the stack.

Другими словами, время жизни того, на что указывает result, привязано к времени жизни первого параметра, и этого знания достаточно, чтобы проверить безопасность памяти в месте вызова.

Будущие направления

В более поздних версиях Nim можно более точно определить правило оперирования с синтаксисом, подобным:

proc foo(other: Y; container: var X): var T from container

Здесь var T from container явно показывает, что местоположение получено из второго параметра (в данном случае, называемого «контейнер»). Синтаксис var T from p определяет тип varTy[T, 2], несовместимый с varTy[T, 1].

NRVO

Примечание: Этот раздел описывает текущую реализацию. Эта часть спецификации языка будет изменена. Дополнительную информацию см. на странице https://github.com/nim-lang/RFCs/issues/230.

Возвращаемое значение представлено внутри тела процедуры как специальная переменная result. Это позволяет механизм, похожий на оптимизацию «именованного возвращаемого значения» C++ (NRVO). NRVO означает, что записи в result внутри p напрямую влияют на место назначения dest в let/var dest = p(args) (определение dest) и также в dest = p(args) (присваивание dest). Это достигается путем переписывания dest = p(args) в p'(args, dest), где p' является вариантом p, который возвращает void и получает скрытый изменяемый параметр, представляющий result.

Неформально:

proc p(): BigT = ...

var x = p()
x = p()

# is roughly turned into:

proc p(result: var BigT) = ...

var x; p(x)
p(x)

Пусть T - это тип возвращаемого значения p. NRVO применяется к T, если sizeof(T) >= N (где N зависит от реализации), другими словами, он применяется к «большим» структурам.

Если p может вызвать исключение, NRVO применяется независимо. Это может привести к наблюдаемым различиям в поведении:

type
  BigT = array[16, int]

proc p(raiseAt: int): BigT =
  for i in 0..high(result):
    if i == raiseAt: raise newException(ValueError, "interception")
    result[i] = i

proc main =
  var x: BigT
  try:
    x = p(8)
  except ValueError:
    doAssert x == [0, 1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0]

main()

Компилятор может выдать предупреждение в этих случаях, однако это поведение отключено по умолчанию. Его можно включить для определенного участка кода с помощью директив warning[ObservableStores] и push/pop. Возьмите вышеприведенный код в качестве примера:

{.push warning[ObservableStores]: on.}
main()
{.pop.}

Перегрузка оператора индексации

Оператор индексации [] для массивов/openarray/последовательностей может быть перегружен для любого типа (с некоторыми исключениями) путем определения процедуры с именем [].

type Foo = object
  data: seq[int]

proc `[]`(foo: Foo, i: int): int =
  result = foo.data[i]

let foo = Foo(data: @[1, 2, 3])
echo foo[1] # 2

Присваивание индексам также может быть перегружено путем именования процедуры []=, которая имеет приоритет над присваиванием результату [].

type Foo = object
  data: seq[int]

proc `[]`(foo: Foo, i: int): int =
  result = foo.data[i]
proc `[]=`(foo: var Foo, i: int, val: int) =
  foo.data[i] = val

var foo = Foo(data: @[1, 2, 3])
echo foo[1] # 2
foo[1] = 5
echo foo.data # @[1, 5, 3]
echo foo[1] # 5

Перегрузки оператора индексации не могут быть применены к самим символам процедур или типов, так как это противоречит синтаксису для инициализации параметров обобщений, т.е. foo[int](1, 2, 3) или Foo[int].

Методы

Процедуры всегда используют статический диспетчер. Методы используют динамический диспетчер. Для работы динамического диспетчера с объектом он должен быть типом ссылок.

type
  Expression = ref object of RootObj ## abstract base class for an expression
  Literal = ref object of Expression
    x: int
  PlusExpr = ref object of Expression
    a, b: Expression

method eval(e: Expression): int {.base.} =
  # override this base method
  raise newException(CatchableError, "Method without implementation override")

method eval(e: Literal): int = return e.x

method eval(e: PlusExpr): int =
  # watch out: relies on dynamic binding
  result = eval(e.a) + eval(e.b)

proc newLit(x: int): Literal =
  new(result)
  result.x = x

proc newPlus(a, b: Expression): PlusExpr =
  new(result)
  result.a = a
  result.b = b

echo eval(newPlus(newPlus(newLit(1), newLit(2)), newLit(4)))

В примере конструкторы newLit и newPlus являются процедурами, поскольку они должны использовать статическое связывание, но eval является методом, так как он требует динамического связывания.

Как видно из примера, базовые методы должны быть помечены директивой base. Директива base также служит напоминанием программисту о том, что базовый метод m используется в качестве основы для определения всех последствий, которые может вызвать вызов m.

Примечание: Выполнение во время компиляции для методов (ещё) не поддерживается.

Примечание: Начиная с Nim 0.20, обобщенные методы устарели.

Многометоды

Примечание: Начиная с Nim 0.20, для использования многометодов необходимо явно указать --multimethods:on при компиляции.

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

type
  Thing = ref object of RootObj
  Unit = ref object of Thing
    x: int

method collide(a, b: Thing) {.base, inline.} =
  quit "to override!"

method collide(a: Thing, b: Unit) {.inline.} =
  echo "1"

method collide(a: Unit, b: Thing) {.inline.} =
  echo "2"

var a, b: Unit
new a
new b
collide(a, b) # output: 2

Запрет динамического разрешения методов с помощью procCall

Динамическое разрешение методов может быть запрещено с помощью встроенной функции system.procCall. Это в какой-то степени сопоставимо с ключевым словом super, предлагаемым традиционными языками ООП.

type
  Thing = ref object of RootObj
  Unit = ref object of Thing
    x: int

method m(a: Thing) {.base.} =
  echo "base"

method m(a: Unit) =
  # Call the base method:
  procCall m(Thing(a))
  echo "1"

Итераторы и оператор for

Оператор for — это абстрактный механизм для итерации по элементам контейнера. Он полагается на итератор для этого. Как и while операторы, for операторы открывают явный блок, чтобы они могли быть завершены оператором break.

Цикл for объявляет переменные итерации — их область действия простирается до конца тела цикла. Типы переменных итерации выводятся по типу возвращаемого значения итератора.

Итератор похож на процедуру, за исключением того, что его можно вызвать в контексте цикла for. Итераторы предоставляют способ указать итерацию по абстрактному типу. Оператор yield в вызываемом итераторе играет ключевую роль в выполнении цикла for. Всякий раз, когда достигается оператор yield, данные связываются с переменными цикла for, и управление передается в тело цикла for. Локальные переменные и состояние выполнения итератора автоматически сохраняются между вызовами. Пример:

# this definition exists in the system module
iterator items*(a: string): char {.inline.} =
  var i = 0
  while i < len(a):
    yield a[i]
    inc(i)

for ch in items("hello world"): # `ch` is an iteration variable
  echo ch

Компилятор генерирует код так, как будто программист написал это:

var i = 0
while i < len(a):
  var ch = a[i]
  echo ch
  inc(i)

Если итератор возвращает кортеж, может быть столько же переменных итерации, сколько компонентов в кортеже. Тип i-й переменной итерации — тип i-го компонента. Другими словами, неявная распаковка кортежей в контексте цикла for поддерживается.

Неявные вызовы items/pairs

Если выражение цикла for e не обозначает итератор, и цикл for имеет ровно 1 переменную, выражение цикла for переписывается в items(e); т.е. вызывается неявный итератор items:

for x in [1,2,3]: echo x

Если цикл for имеет ровно 2 переменные, вызывается неявный итератор pairs.

Поиск символов идентификаторов items/pairs выполняется после шага переписывания, чтобы учитывать все перегрузки items/pairs.

Итераторы первого класса

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

Предупреждение: тело цикла for над встроенным итератором встраивается в каждый оператор yield, встречающийся в коде итератора, поэтому в идеале код должен быть переработан, чтобы содержать единственный yield, чтобы избежать разрастания кода.

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

В отличие от этого, итератор замыканий можно передавать более свободно:

iterator count0(): int {.closure.} =
  yield 0

iterator count2(): int {.closure.} =
  var x = 1
  yield x
  inc x
  yield x

proc invoke(iter: iterator(): int {.closure.}) =
  for x in iter(): echo x

invoke(count0)
invoke(count2)

У итераторов замыканий и встроенных итераторов есть некоторые ограничения:

  1. Пока что итератор замыканий не может быть выполнен во время компиляции.
  2. return разрешено в итераторе замыканий, но не в встроенном итераторе (но редко полезно) и завершает итерацию.
  3. Встроенные итераторы не могут быть рекурсивными.
  4. Ни встроенные, ни итераторы замыканий не имеют специальной переменной result.

Итераторы, которые не помечены явно как {.closure.} или {.inline.}, по умолчанию являются инлайновыми, но это может измениться в будущих версиях реализации.

Тип iterator всегда имеет соглашение о вызове closure неявно; следующий пример показывает, как использовать итераторы для реализации системы совместного выполнения задач:

# simple tasking:
type
  Task = iterator (ticker: int)

iterator a1(ticker: int) {.closure.} =
  echo "a1: A"
  yield
  echo "a1: B"
  yield
  echo "a1: C"
  yield
  echo "a1: D"

iterator a2(ticker: int) {.closure.} =
  echo "a2: A"
  yield
  echo "a2: B"
  yield
  echo "a2: C"

proc runTasks(t: varargs[Task]) =
  var ticker = 0
  while true:
    let x = t[ticker mod t.len]
    if finished(x): break
    x(ticker)
    inc ticker

runTasks(a1, a2)

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

Обратите внимание, что system.finished небезопасен в использовании, поскольку он возвращает true только после того, как итератор завершил свою работу:

iterator mycount(a, b: int): int {.closure.} =
  var x = a
  while x <= b:
    yield x
    inc x

var c = mycount # instantiate the iterator
while not finished(c):
  echo c(1, 3)

# Produces
1
2
3
0

Вместо этого необходимо использовать следующий код:

var c = mycount # instantiate the iterator
while true:
  let value = c(1, 3)
  if finished(c): break # and discard 'value'!
  echo value

Полезно представлять, что итератор на самом деле возвращает пару (value, done), и finished используется для доступа к скрытому полю done.

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

proc mycount(a, b: int): iterator (): int =
  result = iterator (): int =
    var x = a
    while x <= b:
      yield x
      inc x

let foo = mycount(1, 4)

for f in foo():
  echo f

Вызов можно сделать более похожим на инлайновый итератор с помощью макроса цикла for:

import std/macros
macro toItr(x: ForLoopStmt): untyped =
  let expr = x[0]
  let call = x[1][1] # Get foo out of toItr(foo)
  let body = x[2]
  result = quote do:
    block:
      let itr = `call`
      for `expr` in itr():
          `body`

for f in toItr(mycount(1, 4)): # using early `proc mycount`
  echo f

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

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

proc recCountDown(n: int): iterator(): int =
  result = iterator(): int =
    if n > 0:
      yield n
      for e in toItr(recCountDown(n - 1)):
        yield e

for i in toItr(recCountDown(6)): # Emits: 6 5 4 3 2 1
  echo i

См. также iterable для передачи итераторов шаблонам и макросам.

Преобразователи

Преобразователь похож на обычный процесс, за исключением того, что он расширяет отношение "неявного преобразования" типов (см. Отношение преобразования):

# bad style ahead: Nim is not C.
converter toBool(x: int): bool = x != 0

if 4:
  echo "compiles"

Преобразователь также может быть вызван явно для улучшения читабельности. Обратите внимание, что цепочки неявных преобразователей не поддерживаются: если существует преобразователь от типа A к типу B и от типа B к типу C, неявное преобразование от A к C не предоставляется.

Разделы типов

Пример:

type # example demonstrating mutually recursive types
  Node = ref object  # an object managed by the garbage collector (ref)
    le, ri: Node     # left and right subtrees
    sym: ref Sym     # leaves contain a reference to a Sym
  
  Sym = object       # a symbol
    name: string     # the symbol's name
    line: int        # the line the symbol was declared in
    code: Node       # the symbol's abstract syntax tree

Раздел типа начинается с ключевого слова type. Он содержит несколько определений типов. Определение типа связывает тип с именем. Определения типов могут быть рекурсивными или даже взаимно рекурсивными. Взаимно рекурсивные типы возможны только в одном разделе type. Номинальные типы, такие как objects или enums, могут быть определены только в разделе type.

Обработка исключений

Оператор try

Пример:

# read the first two lines of a text file that should contain numbers
# and tries to add them
var
  f: File
if open(f, "numbers.txt"):
  try:
    var a = readLine(f)
    var b = readLine(f)
    echo "sum: " & $(parseInt(a) + parseInt(b))
  except OverflowDefect:
    echo "overflow!"
  except ValueError, IOError:
    echo "catch multiple exceptions!"
  except CatchableError:
    echo "Catchable exception!"
  finally:
    close(f)

Операторы после try выполняются в последовательном порядке, если не возникает исключение e. Если тип исключения e соответствует какому-либо из перечисленных в операторе except, выполняются соответствующие операторы. Операторы, следующие за операторами except, называются обработчиками исключений.

Если есть оператор finally, он всегда выполняется после обработчиков исключений.

Исключение обрабатывается в обработчике исключений. Однако обработчик исключений может сгенерировать другое исключение. Если исключение не обработано, оно передается по стеку вызовов. Это означает, что часто остальная часть процедуры — то есть, не находящаяся внутри оператора finally — не выполняется (если возникает исключение).

Выражение try

Оператор try также может быть использован как выражение; тип ветви try должен соответствовать типам веток except, но тип ветви finally всегда должен быть void:

from std/strutils import parseInt

let x = try: parseInt("133a")
        except ValueError: -1
        finally: echo "hi"

Для предотвращения путаницы в коде существует ограничение синтаксического анализа; если try следует за (, оно должно быть записано как однострочное выражение:

from std/strutils import parseInt
let x = (try: parseInt("133a") except ValueError: -1)

Операторы except

Внутри оператора except можно получить доступ к текущему исключению с помощью следующего синтаксиса:

try:
  # ...
except IOError as e:
  # Now use "e"
  echo "I/O error: " & e.msg

В качестве альтернативы, можно использовать getCurrentException для извлечения исключения, которое было сгенерировано:

try:
  # ...
except IOError:
  let e = getCurrentException()
  # Now use "e"

Обратите внимание, что getCurrentException всегда возвращает тип ref Exception. Если необходима переменная нужного типа (в примере выше, IOError), необходимо выполнить явное преобразование:

try:
  # ...
except IOError:
  let e = (ref IOError)(getCurrentException())
  # "e" is now of the proper type

Однако это редко необходимо. В большинстве случаев требуется извлечь сообщение об ошибке из e, и для таких ситуаций достаточно использовать getCurrentExceptionMsg:

try:
  # ...
except CatchableError:
  echo getCurrentExceptionMsg()

Пользовательские исключения

Можно создавать пользовательские исключения. Пользовательское исключение — это пользовательский тип:

type
  LoadError* = object of Exception

Рекомендуется заканчивать имя пользовательского исключения Error.

Пользовательские исключения могут быть сгенерированы так же, как и любые другие исключения, например:

raise newException(LoadError, "Failed to load data")

Оператор defer

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

Любые операторы, следующие за defer, будут считаться находящимися в неявном блоке try в текущем блоке:

proc main =
  var f = open("numbers.txt", fmWrite)
  defer: close(f)
  f.write "abc"
  f.write "def"

Переписывается как:

proc main =
  var f = open("numbers.txt")
  try:
    f.write "abc"
    f.write "def"
  finally:
    close(f)

Когда defer находится в самой внешней области видимости шаблона/макроса, его область видимости простирается до блока, из которого вызван шаблон/макрос:

template safeOpenDefer(f, path) =
  var f = open(path, fmWrite)
  defer: close(f)

template safeOpenFinally(f, path, body) =
  var f = open(path, fmWrite)
  try: body # without `defer`, `body` must be specified as parameter
  finally: close(f)

block:
  safeOpenDefer(f, "/tmp/z01.txt")
  f.write "abc"
block:
  safeOpenFinally(f, "/tmp/z01.txt"):
    f.write "abc" # adds a lexical scope
block:
  var f = open("/tmp/z01.txt", fmWrite)
  try:
    f.write "abc" # adds a lexical scope
  finally: close(f)

Не поддерживаются операторы defer на верхнем уровне, так как неясно, к чему такой оператор должен относиться.

Оператор raise

Пример:

raise newException(IOError, "IO failed")

Помимо встроенных операций, таких как индексирование массивов, выделение памяти и т. д., оператор raise является единственным способом генерировать исключение.

Если имя исключения не указано, текущее исключение перевыводится. Исключение ReraiseDefect генерируется, если нет исключения для перевывода. Следовательно, оператор raise всегда генерирует исключение.

Иерархия исключений

Дерево исключений определяется в модуле system. Каждое исключение наследуется от system.Exception. Исключения, указывающие на ошибки программирования, наследуются от system.Defect (который является подтипом Exception) и строго говоря не могут быть перехвачены, так как они также могут быть отображены на операцию, которая завершает весь процесс. Если сбои преобразуются в исключения, эти исключения наследуются от Defect.

Исключения, указывающие на любую другую ошибку времени выполнения, которая может быть перехвачена, наследуются от system.CatchableError (который является подтипом Exception).

Exception
|-- CatchableError
|   |-- IOError
|   |   `-- EOFError
|   |-- OSError
|   |-- ResourceExhaustedError
|   `-- ValueError
|       `-- KeyError
`-- Defect
    |-- AccessViolationDefect
    |-- ArithmeticDefect
    |   |-- DivByZeroDefect
    |   `-- OverflowDefect
    |-- AssertionDefect
    |-- DeadThreadDefect
    |-- FieldDefect
    |-- FloatingPointDefect
    |   |-- FloatDivByZeroDefect
    |   |-- FloatInvalidOpDefect
    |   |-- FloatOverflowDefect
    |   |-- FloatUnderflowDefect
    |   `-- InexactDefect
    |-- IndexDefect
    |-- NilAccessDefect
    |-- ObjectAssignmentDefect
    |-- ObjectConversionDefect
    |-- OutOfMemoryDefect
    |-- RangeDefect
    |-- ReraiseDefect
    `-- StackOverflowDefect

Импортированные исключения

Можно генерировать/перехватывать импортированные исключения C++. Типы, импортированные с помощью importcpp, могут быть сгенерированы или перехвачены. Исключения генерируются по значению и перехватываются по ссылке. Пример:

type
  CStdException {.importcpp: "std::exception", header: "<exception>", inheritable.} = object
    ## does not inherit from `RootObj`, so we use `inheritable` instead
  CRuntimeError {.requiresInit, importcpp: "std::runtime_error", header: "<stdexcept>".} = object of CStdException
    ## `CRuntimeError` has no default constructor => `requiresInit`
proc what(s: CStdException): cstring {.importcpp: "((char *)#.what())".}
proc initRuntimeError(a: cstring): CRuntimeError {.importcpp: "std::runtime_error(@)", constructor.}
proc initStdException(): CStdException {.importcpp: "std::exception()", constructor.}

proc fn() =
  let a = initRuntimeError("foo")
  doAssert $a.what == "foo"
  var b: cstring
  try: raise initRuntimeError("foo2")
  except CStdException as e:
    doAssert e is CStdException
    b = e.what()
  doAssert $b == "foo2"
  
  try: raise initStdException()
  except CStdException: discard
  
  try: raise initRuntimeError("foo3")
  except CRuntimeError as e:
    b = e.what()
  except CStdException:
    doAssert false
  doAssert $b == "foo3"

fn()

Примечание: getCurrentException() и getCurrentExceptionMsg() недоступны для импортированных исключений из C++. Необходимо использовать синтаксис except ImportedException as x: и полагаться на функциональность объекта x для получения деталей исключения.

Система эффектов

Примечание: Правила отслеживания эффектов изменились с выпуском версии 1.6 компилятора Nim.

Отслеживание исключений

Nim поддерживает отслеживание исключений. Предикат raises можно использовать для явного определения, какие исключения может генерировать процедура/итератор/метод/преобразователь. Компилятор проверяет это:

proc p(what: bool) {.raises: [IOError, OSError].} =
  if what: raise newException(IOError, "IO")
  else: raise newException(OSError, "OS")

Пустой список raises (raises: []) означает, что исключение генерироваться не может:

proc p(): bool {.raises: [].} =
  try:
    unsafeCall()
    result = true
  except CatchableError:
    result = false

Список raises также может быть присоединен к типу процедуры. Это влияет на совместимость типов:

type
  Callback = proc (s: string) {.raises: [IOError].}
var
  c: Callback

proc p(x: string) =
  raise newException(OSError, "OS")

c = p # type error

Для процедуры p компилятор использует правила вывода, чтобы определить множество потенциально генерируемых исключений; алгоритм работает с графом вызовов p:

  1. Каждый косвенный вызов через некоторый тип процедуры T предполагается, что генерирует system.Exception (основной тип иерархии исключений), а значит, и любое исключение, если у T нет явного списка raises. Однако, если вызов имеет вид f(...), где f — параметр текущей анализируемой процедуры, помеченный как .effectsOf: f, он игнорируется. Вызов оптимистично предполагается без эффекта. Правило 2 компенсирует этот случай.
  2. Каждое выражение e некоторого типа процедуры внутри вызова, переданного параметру, помеченному как .effectsOf процедуры p, предполагается, что вызывается косвенно, и поэтому его список raises добавляется в список raises процедуры p.
  3. Каждый вызов процедуры q, имеющей неизвестное тело (из-за объявления вперед), предполагается, что генерирует system.Exception, если у q нет явного списка raises. Процедуры, помеченные как importc, предполагается, что генерируют .raises: [], если не объявлено иначе.
  4. Каждый вызов метода m предполагается, что генерирует system.Exception, если у m нет явного списка raises.
  5. Для всех остальных вызовов анализ может определить точный список raises.
  6. Для определения списка raises учитываются операторы raise и try процедуры p.

Исключения, наследуемые от system.Defect, не отслеживаются механизмом отслеживания исключений .raises: []. Это более согласуется со встроенными операциями. Следующий код корректен:

proc mydiv(a, b): int {.raises: [].} =
  a div b # can raise an DivByZeroDefect

И также корректен:

proc mydiv(a, b): int {.raises: [].} =
  if b == 0: raise newException(DivByZeroDefect, "division by zero")
  else: result = a div b

Причина в том, что DivByZeroDefect наследуется от Defect, и с --panics:on Ошибки становятся невосстанавливаемыми ошибками. (С версии 1.4 языка.)

Аннотация EffectsOf

Правила 1-2 правил вывода отслеживания исключений (см. предыдущий раздел) гарантируют, что следующее работает:

proc weDontRaiseButMaybeTheCallback(callback: proc()) {.raises: [], effectsOf: callback.} =
  callback()

proc doRaise() {.raises: [IOError].} =
  raise newException(IOError, "IO")

proc use() {.raises: [].} =
  # doesn't compile! Can raise IOError!
  weDontRaiseButMaybeTheCallback(doRaise)

Как видно из примера, параметр типа proc (...) может быть аннотирован как .effectsOf. Такой параметр позволяет использовать полиморфизм эффектов: процедура weDontRaiseButMaybeTheCallback вызывает исключения, которые вызывает callback.

Поэтому во многих случаях обратный вызов не заставляет компилятор быть чрезмерно консервативным в анализе его эффектов:

{.push warningAsError[Effect]: on.}

import std/algorithm

type
  MyInt = distinct int

var toSort = @[MyInt 1, MyInt 2, MyInt 3]

proc cmpN(a, b: MyInt): int =
  cmp(a.int, b.int)

proc harmless {.raises: [].} =
  toSort.sort cmpN

proc cmpE(a, b: MyInt): int {.raises: [Exception].} =
  cmp(a.int, b.int)

proc harmful {.raises: [].} =
  # does not compile, `sort` can now raise Exception
  toSort.sort cmpE

Отслеживание тегов

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

type IO = object ## input/output effect
proc readLine(): string {.tags: [IO].} = discard

proc no_effects_please() {.tags: [].} =
  # the compiler prevents this:
  let x = readLine()

Тег должен быть именем типа. Список tags, подобно списку raises, также может быть прикреплен к типу процедуры. Это влияет на совместимость типов.

Вывод для отслеживания тегов аналогичен выводу для отслеживания исключений.

Также есть способ запретить определенные эффекты:

type IO = object ## input/output effect
proc readLine(): string {.tags: [IO].} = discard
proc echoLine(): void = discard

proc no_IO_please() {.forbids: [IO].} =
  # this is OK because it didn't define any tag:
  echoLine()
  # the compiler prevents this:
  let y = readLine()

Предикат forbids определяет список недопустимых эффектов — если какое-либо утверждение вызывает любой из этих эффектов, компиляция завершится ошибкой. Типы процедур с любыми запрещенными эффектами являются подтипами равных типов процедур без таких списков:

type MyEffect = object
type ProcType1 = proc (i: int): void {.forbids: [MyEffect].}
type ProcType2 = proc (i: int): void

proc caller1(p: ProcType1): void = p(1)
proc caller2(p: ProcType2): void = p(1)

proc effectful(i: int): void {.tags: [MyEffect].} = echo $i
proc effectless(i: int): void {.forbids: [MyEffect].} = echo $i

proc toBeCalled1(i: int): void = effectful(i)
proc toBeCalled2(i: int): void = effectless(i)

## this will fail because toBeCalled1 uses MyEffect which was forbidden by ProcType1:
caller1(toBeCalled1)
## this is OK because both toBeCalled2 and ProcType1 have the same requirements:
caller1(toBeCalled2)
## these are OK because ProcType2 doesn't have any effect requirement:
caller2(toBeCalled1)
caller2(toBeCalled2)

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

Побочные эффекты

Предикат noSideEffect используется для маркировки процедуры/итератора, который может иметь только побочные эффекты через параметры. Это означает, что процедура/итератор изменяет только доступные по параметрам позиции, а возвращаемое значение зависит только от параметров. Если ни один из его параметров не имеет тип var, ref, ptr, cstring или proc, то никакие позиции не изменяются.

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

Это статическая ошибка — пометить процедуру/итератор как не имеющую побочных эффектов, если компилятор не может это проверить.

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

func — это синтаксический сахар для процедуры без побочных эффектов:

func `+` (x, y: int): int

Для переопределения анализа побочных эффектов компилятором можно использовать блок предиката {.noSideEffect.} cast:

func f() =
  {.cast(noSideEffect).}:
    echo "test"

Побочные эффекты обычно выводятся. Вывод для побочных эффектов аналогичен выводу для отслеживания исключений.

Когда компилятор не может вывести побочные эффекты, как это происходит для импортированных функций, их можно аннотировать с помощью предиката sideEffect.

Эффект безопасности GC

Мы называем процедуру p безопасной для GC, когда она не обращается к никаким глобальным переменным, содержащим память, управляемую GC (string, seq, ref или замыкание), ни напрямую, ни косвенно через вызов процедуры, небезопасной для GC.

Свойство безопасности GC обычно выводится. Вывод для безопасности GC аналогичен выводу для отслеживания исключений.

Аннотация gcsafe может быть использована для обозначения процедуры как gcsafe, в противном случае это свойство выводится компилятором. Обратите внимание, что noSideEffect подразумевает gcsafe.

Процедуры, импортированные из C, всегда предполагаются gcsafe.

Для переопределения анализа gcsafety компилятором можно использовать блок предиката {.cast(gcsafe).}:

var
  someGlobal: string = "some string here"
  perThread {.threadvar.}: string

proc setPerThread() =
  {.cast(gcsafe).}:
    deepCopy(perThread, someGlobal)

См. также:

  • Управление памятью общего куска.

Предикат эффектов

Предикат effects разработан для помощи программисту в анализе эффектов. Это утверждение, которое заставляет компилятор выводить все выведенные эффекты до позиции effects:

proc p(what: bool) =
  if what:
    raise newException(IOError, "IO")
    {.effects.}
  else:
    raise newException(OSError, "OS")

Компилятор выдает сообщение-подсказку, что может быть вызвано исключение IOError. OSError не указано, так как оно не может быть вызвано в ветке, в которой находится предикат effects.

Обобщения

Обобщения — это способ параметризации процедур, итераторов или типов с параметрами типов в Nim. В зависимости от контекста, скобки используются для введения параметров типов или для создания экземпляра обобщенной процедуры, итератора или типа.

Следующий пример показывает, как можно смоделировать обобщенное двоичное дерево:

type
  BinaryTree*[T] = ref object # BinaryTree is a generic type with
                              # generic parameter `T`
    le, ri: BinaryTree[T]     # left and right subtrees; may be nil
    data: T                   # the data stored in a node

proc newNode*[T](data: T): BinaryTree[T] =
  # constructor for a node
  result = BinaryTree[T](le: nil, ri: nil, data: data)

proc add*[T](root: var BinaryTree[T], n: BinaryTree[T]) =
  # insert a node into the tree
  if root == nil:
    root = n
  else:
    var it = root
    while it != nil:
      # compare the data items; uses the generic `cmp` proc
      # that works for any type that has a `==` and `<` operator
      var c = cmp(it.data, n.data)
      if c < 0:
        if it.le == nil:
          it.le = n
          return
        it = it.le
      else:
        if it.ri == nil:
          it.ri = n
          return
        it = it.ri

proc add*[T](root: var BinaryTree[T], data: T) =
  # convenience proc:
  add(root, newNode(data))

iterator preorder*[T](root: BinaryTree[T]): T =
  # Preorder traversal of a binary tree.
  # This uses an explicit stack (which is more efficient than
  # a recursive iterator factory).
  var stack: seq[BinaryTree[T]] = @[root]
  while stack.len > 0:
    var n = stack.pop()
    while n != nil:
      yield n.data
      add(stack, n.ri)  # push right subtree onto the stack
      n = n.le          # and follow the left pointer

var
  root: BinaryTree[string] # instantiate a BinaryTree with `string`
add(root, newNode("hello")) # instantiates `newNode` and `add`
add(root, "world")          # instantiates the second `add` proc
for str in preorder(root):
  stdout.writeLine(str)

T называется обобщенным параметром типа или переменной типа.

Обобщенные процедуры

Давайте рассмотрим анатомию обобщенной proc, чтобы договориться о терминологии.

p[T: t](arg1: f): y
  • p: Символ вызываемой стороны
  • [...]: Обобщенные параметры
  • T: t: Обобщенное ограничение
  • T: Переменная типа
  • [T: t](arg1: f): y: Формальная сигнатура
  • arg1: f: Формальный параметр
  • f: Тип формального параметра
  • y: Тип формального возвращаемого значения

Здесь использование слова «формальный» означает символы, как они определены программистом, а не как они могут быть в контексте компиляции. Поскольку обобщения могут быть созданы экземплярами и привязаны к типам, у нас есть несколько сущностей, о которых нужно думать, когда речь идет об обобщениях.

Использование обобщения приведет к преобразованию формально определенного выражения в экземпляр этого выражения, связанный только с конкретными типами. Этот процесс называется «созданием экземпляра».

В скобках в месте формального определения обобщения указываются «ограничения», как в:

type Foo[T] = object
proc p[H;T: Foo[H]](param: T): H

Определение ограничений может иметь более одного символа, разделяя каждое определение ;. Обратите внимание, как T состоит из H, и тип возвращаемого значения p определен как H. Когда этот обобщенный процесс создается экземпляром, H будет привязан к конкретному типу, делая T конкретным, и тип возвращаемого значения p будет привязан к тому же конкретному типу, используемому для определения H.

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

Оператор is

Оператор is вычисляется во время семантического анализа для проверки эквивалентности типов. Поэтому он очень полезен для специализации типов в обобщенном коде:

type
  Table[Key, Value] = object
    keys: seq[Key]
    values: seq[Value]
    when not (Key is string): # empty value for strings used for optimization
      deletedKeys: seq[bool]

Классы типов

Класс типа — это специальный псевдотип, который можно использовать для сопоставления с типами в контексте разрешения перегрузки или оператора is. Nim поддерживает следующие встроенные классы типов:

Класс типа Соответствия
object любой тип объекта
tuple любой кортежный тип
enum любая перечисление
proc любой тип процедуры
iterator любой тип итератора
ref любой тип ref
ptr любой тип ptr
var любой тип var
distinct любой отдельный тип
array любой массивный тип
set любой множественный тип
seq любой тип последовательности
auto любой тип

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

Классы типов можно комбинировать с помощью стандартных логических операторов, чтобы создать более сложные классы типов:

# create a type class that will match all tuple and object types
type RecordType = (tuple or object)

proc printFields[T: RecordType](rec: T) =
  for key, value in fieldPairs(rec):
    echo key, " = ", value

Типовые ограничения на обобщенные параметры могут быть сгруппированы с помощью ,, и распространение останавливается с помощью ;, аналогично параметрам макросов и шаблонов:

proc fn1[T; U, V: SomeFloat]() = discard # T is unconstrained
template fn2(t; u, v: SomeFloat) = discard # t is unconstrained

Хотя синтаксис классов типов напоминает ADTs/алгебраические типы данных в языках типа ML, следует понимать, что классы типов являются статическими ограничениями, которые должны быть выполнены при создании экземпляров типов. Классы типов на самом деле не являются типами, а представляют собой систему, предоставляющую обобщенные «проверки», которые в конечном итоге решают какой-то единственный тип. Классы типов не допускают динамизма типов во время выполнения, в отличие от вариантов объектов или методов.

Например, следующее не будет компилироваться:

type TypeClass = int | string
var foo: TypeClass = 2 # foo's type is resolved to an int here
foo = "this will fail" # error here, because foo is an int

Nim позволяет указывать классы типов и обычные типы как типовые ограничения обобщенного параметра типа:

proc onlyIntOrString[T: int|string](x, y: T) = discard

onlyIntOrString(450, 616) # valid
onlyIntOrString(5.0, 0.0) # type mismatch
onlyIntOrString("xy", 50) # invalid as 'T' cannot be both at the same time

proc и iterator классы типов также принимают предикат вызова для ограничения соглашения о вызове для соответствующего типа proc или iterator.

proc onlyClosure[T: proc {.closure.}](x: T) = discard

onlyClosure(proc() = echo "hello") # valid
proc foo() {.nimcall.} = discard
onlyClosure(foo) # type mismatch

Неявные обобщения

Класс типа может быть использован непосредственно как тип параметра.

# create a type class that will match all tuple and object types
type RecordType = (tuple or object)

proc printFields(rec: RecordType) =
  for key, value in fieldPairs(rec):
    echo key, " = ", value

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

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

proc `==`*(x, y: tuple): bool =
  ## requires `x` and `y` to be of the same tuple type
  ## generic `==` operator for tuples that is lifted from the components
  ## of `x` and `y`.
  result = true
  for a, b in fields(x, y):
    if a != b: result = false

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

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

type Matrix[T, Rows, Columns] = object
  ...

proc `[]`(m: Matrix, row, col: int): Matrix.T =
  m.data[col * high(Matrix.Columns) + row]

Вот дополнительные примеры, иллюстрирующие неявные обобщения:

proc p(t: Table; k: Table.Key): Table.Value

# is roughly the same as:

proc p[Key, Value](t: Table[Key, Value]; k: Key): Value
proc p(a: Table, b: Table)

# is roughly the same as:

proc p[Key, Value](a, b: Table[Key, Value])
proc p(a: Table, b: distinct Table)

# is roughly the same as:

proc p[Key, Value, KeyB, ValueB](a: Table[Key, Value], b: Table[KeyB, ValueB])

typedesc в качестве типа параметра также вводит неявное обобщение. typedesc имеет собственный набор правил:

proc p(a: typedesc)

# is roughly the same as:

proc p[T](a: typedesc[T])

typedesc — это тип класса "связать множество":

proc p(a, b: typedesc)

# is roughly the same as:

proc p[T, T2](a: typedesc[T], b: typedesc[T2])

Параметр типа typedesc сам по себе может использоваться как тип. Если он используется как тип, то это базовый тип. Другими словами, один уровень "typedesc"-ности удаляется:

proc p(a: typedesc; b: a) = discard

# is roughly the same as:
proc p[T](a: typedesc[T]; b: T) = discard

# hence this is a valid call:
p(int, 4)
# as parameter 'a' requires a type, but 'b' requires a value.

Ограничения вывода обобщений

Типы var T и typedesc[T] не могут быть выведены в обобщённом экземпляре. Следующее недопустимо:

proc g[T](f: proc(x: T); x: T) =
  f(x)

proc c(y: int) = echo y
proc v(y: var int) =
  y += 100
var i: int

# allowed: infers 'T' to be of type 'int'
g(c, 42)

# not valid: 'T' is not inferred to be of type 'var int'
g(v, i)

# also not allowed: explicit instantiation via 'var int'
g[var int](v, i)

Поиск символов в обобщениях

Открытые и закрытые символы

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

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

type
  Index = distinct int

proc `==` (a, b: Index): bool {.borrow.}

var a = (0, 0.Index)
var b = (0, 0.Index)

echo a == b # works!

В примере оператор обобщённого `==` для кортежей (как определённого в модуле системы) использует операторы == компонентов кортежа. Однако оператор == для типа Index определён после оператора == для кортежей; тем не менее, пример компилируется, так как экземпляризация также учитывает текущие определённые символы.

Инструкции mixin

Символ можно принудительно сделать открытым с помощью объявления mixin:

proc create*[T](): ref T =
  # there is no overloaded 'init' here, so we need to state that it's an
  # open symbol explicitly:
  mixin init
  new result
  init result

Инструкции mixin имеют смысл только в шаблонах и обобщениях.

Инструкции bind

Инструкция bind является аналогом инструкции mixin. Её можно использовать для явного объявления идентификаторов, которые должны быть связаны на ранней стадии (т. е. идентификаторы должны быть найдены в области видимости определения шаблона/обобщения):

# Module A
var
  lastId = 0

template genId*: untyped =
  bind lastId
  inc(lastId)
  lastId
# Module B
import A

echo genId()

Но инструкция bind редко полезна, поскольку связывание символов из области видимости определения является по умолчанию.

Инструкции bind имеют смысл только в шаблонах и обобщениях.

Делегирующие инструкции bind

Следующий пример иллюстрирует проблему, которая может возникнуть, когда обобщённые экземпляризации пересекают несколько разных модулей:

# module A
proc genericA*[T](x: T) =
  mixin init
  init(x)
import C

# module B
proc genericB*[T](x: T) =
  # Without the `bind init` statement C's init proc is
  # not available when `genericB` is instantiated:
  bind init
  genericA(x)
# module C
type O = object
proc init*(x: var O) = discard
# module main
import B, C

genericB O()

В модуле B есть процедура init из модуля C в области видимости, которая не учитывается при экземпляризации genericB, что приводит к экземпляризации genericA. Решение состоит в передаче этих символов с помощью инструкции bind внутри genericB.

Шаблоны

Шаблон — это простая форма макроса: это механизм простой подстановки, который работает с абстрактными синтаксическими деревьями Nim. Он обрабатывается на семантическом этапе компиляции.

Синтаксис вызова шаблона такой же, как вызов процедуры.

Пример:

template `!=` (a, b: untyped): untyped =
  # this definition exists in the system module
  not (a == b)

assert(5 != 6) # the compiler rewrites that to: assert(not (5 == 6))

Операторы !=, >, >=, in, notin, isnot на самом деле являются шаблонами:

a > b преобразуется в b < a.
a in b преобразуется в contains(b, a).
notin и isnot имеют очевидный смысл.

Типы шаблонов могут быть символами untyped, typed или typedesc. Это "метатипы", они могут использоваться только в определённых контекстах. Также могут использоваться обычные типы; это подразумевает, что ожидаются выражения typed.

Типизированные и нетипизированные параметры

Параметр untyped означает, что поиск символов и разрешение типов не выполняется до тех пор, пока выражение не будет передано в шаблон. Это означает, что в шаблон можно передавать необъявленные идентификаторы, например:

template declareInt(x: untyped) =
  var x: int

declareInt(x) # valid
x = 3
template declareInt(x: typed) =
  var x: int

declareInt(x) # invalid, because x has not been declared and so it has no type

Шаблон, где каждый параметр является untyped, называется немедленным шаблоном. По историческим причинам шаблоны могут быть явно помечены предикатом immediate, а затем эти шаблоны не участвуют в разрешении перегрузки, а типы параметров игнорируются компилятором. Явно немедленные шаблоны теперь устарели.

Примечание: По историческим причинам stmt был псевдонимом для typed, а expr был псевдонимом для untyped, но они удалены.

Передача блока кода в шаблон

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

template withFile(f, fn, mode, actions: untyped): untyped =
  var f: File
  if open(f, fn, mode):
    try:
      actions
    finally:
      close(f)
  else:
    quit("cannot open: " & fn)

withFile(txt, "ttempl3.txt", fmWrite):  # special colon
  txt.writeLine("line 1")
  txt.writeLine("line 2")

В примере две инструкции writeLine привязаны к параметру actions.

Обычно, чтобы передать блок кода в шаблон, параметр, принимающий блок, должен иметь тип untyped. Так как поиск символов отложен до времени экземпляризации шаблона:

template t(body: typed) =
  proc p = echo "hey"
  block:
    body

t:
  p()  # fails with 'undeclared identifier: p'

Вышеприведённый код завершается сообщением об ошибке, что p не объявлено. Причина в том, что тело p() проверяется на типы перед передачей параметру body, а проверка типов в Nim подразумевает поиск символов. Тот же код работает с untyped, так как тело, переданное в него, не требует проверки на типы:

template t(body: untyped) =
  proc p = echo "hey"
  block:
    body

t:
  p()  # compiles

Переменное количество аргументов нетипизированных

В дополнение к метатипу untyped, который предотвращает проверку типов, также существует varargs[untyped], так что даже число параметров не фиксировано:

template hideIdentifiers(x: varargs[untyped]) = discard

hideIdentifiers(undeclared1, undeclared2)

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

Связывание символов в шаблонах

Шаблон — это гигиенический макрос и поэтому открывает новую область видимости. Большинство символов связаны из области видимости определения шаблона:

# Module A
var
  lastId = 0

template genId*: untyped =
  inc(lastId)
  lastId
# Module B
import A

echo genId() # Works as 'lastId' has been bound in 'genId's defining scope

Как и в обобщениях, связывание символов можно изменять с помощью инструкций mixin или bind.

Конструирование идентификаторов

В шаблонах идентификаторы можно строить с помощью синтаксиса с обратными кавычками:

template typedef(name: untyped, typ: typedesc) =
  type
    `T name`* {.inject.} = typ
    `P name`* {.inject.} = ref `T name`

typedef(myint, int)
var x: PMyInt

В примере name экземплируется с myint, поэтому `T name` становится Tmyint.

Правила поиска для параметров шаблона

Параметр p в шаблоне даже подставляется в выражение x.p. Таким образом, аргументы шаблона могут использоваться как имена полей, а глобальный символ может быть скрыт одним и тем же именем аргумента, даже при полном квалифицировании:

# module 'm'

type
  Lev = enum
    levA, levB

var abclev = levB

template tstLev(abclev: Lev) =
  echo abclev, " ", m.abclev

tstLev(levA)
# produces: 'levA levA'

Но глобальный символ можно правильно получить с помощью инструкции bind:

# module 'm'

type
  Lev = enum
    levA, levB

var abclev = levB

template tstLev(abclev: Lev) =
  bind m.abclev
  echo abclev, " ", m.abclev

tstLev(levA)
# produces: 'levA levB'

Гигиена в шаблонах

По умолчанию шаблоны гигиеничны: локальные идентификаторы, объявленные в шаблоне, недоступны в контексте экземпляризации:

template newException*(exceptn: typedesc, message: string): untyped =
  var
    e: ref exceptn  # e is implicitly gensym'ed here
  new(e)
  e.msg = message
  e

# so this works:
let e = "message"
raise newException(IoError, e)

То, доступен ли символ, объявленный в шаблоне, в области видимости экземпляризации, контролируется предикатами inject и gensym: символы gensym не доступны, а inject — доступны.

Значение по умолчанию для символов типа type, var, let и const равно gensym. Для proc, iterator, converter, template, macro значение по умолчанию равно inject, но если символ gensym с тем же именем определён в той же области синтаксического уровня, он будет по умолчанию gensym. Это можно переопределить, отметив процедуру как inject.

Если имя сущности передаётся как параметр шаблона, то это символ inject:

template withFile(f, fn, mode: untyped, actions: untyped): untyped =
  block:
    var f: File  # since 'f' is a template parameter, it's injected implicitly
    ...

withFile(txt, "ttempl3.txt", fmWrite):
  txt.writeLine("line 1")
  txt.writeLine("line 2")

Предикаты inject и gensym являются аннотациями второго класса; они не имеют семантики вне определения шаблона и не могут быть обобщены:

{.pragma myInject: inject.}

template t() =
  var x {.myInject.}: int # does NOT work

Чтобы избавиться от гигиены в шаблонах, можно использовать предикат dirty для шаблона. inject и gensym не действуют в шаблонах dirty.

Символы gensym не могут использоваться как field в синтаксисе x.field. Также они не могут использоваться в синтаксических конструкциях ObjectConstruction(field: value) и namedParameterCall(field = value).

Причина в том, что код, подобный

type
  T = object
    f: int

template tmp(x: T) =
  let f = 34
  echo x.f, T(f: 4)

должен работать как ожидается.

Однако это означает, что синтаксис вызова метода недоступен для символов gensym:

template tmp(x) =
  type
    T {.gensym.} = int
  
  echo x.T # invalid: instead use:  'echo T(x)'.

tmp(12)

Ограничения синтаксиса вызова метода

Выражение x в x.f должно быть проверено семантически (то есть поиск символов и проверка типов), прежде чем можно будет решить, что его нужно переписать на f(x). Поэтому синтаксис точки имеет некоторые ограничения при использовании для вызова шаблонов/макросов:

template declareVar(name: untyped) =
  const name {.inject.} = 45

# Doesn't compile:
unknownIdentifier.declareVar

Также нельзя использовать полностью квалифицированные идентификаторы с именем модуля в синтаксисе вызова метода. Порядок, в котором оператор точки связывается со символами, это запрещает.

import std/sequtils

var myItems = @[1,3,3,7]
let N1 = count(myItems, 3) # OK
let N2 = sequtils.count(myItems, 3) # fully qualified, OK
let N3 = myItems.count(3) # OK
let N4 = myItems.sequtils.count(3) # illegal, `myItems.sequtils` can't be resolved

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

Макросы

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

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

  • Семантический анализ распознаёт и разрешает вызов макроса.
  • Компилятор выполняет тело макроса (которое может вызывать другие процедуры).
  • Он заменяет AST вызова макроса на AST, возвращённое макросом.
  • Он повторяет семантический анализ этой части кода.
  • Если AST, возвращённое макросом, содержит другие вызовы макросов, этот процесс повторяется.

Хотя макросы позволяют выполнять сложные преобразования кода во время компиляции, они не могут изменить синтаксис Nim.

Примечание по стилю: для лучшей читаемости кода лучше всего использовать наименее мощный программный конструкт, который остаётся выразительным. Поэтому «чек-лист» следующий:

  1. Если возможно, используйте обычную процедуру/итератор.
  2. В противном случае: используйте общую процедуру/итератор, если возможно.
  3. В противном случае: используйте шаблон, если возможно.
  4. В противном случае: используйте макрос.

Пример отладки

Следующий пример реализует мощную debug команду, которая принимает переменное число аргументов:

# to work with Nim syntax trees, we need an API that is defined in the
# `macros` module:
import std/macros

macro debug(args: varargs[untyped]): untyped =
  # `args` is a collection of `NimNode` values that each contain the
  # AST for an argument of the macro. A macro always has to
  # return a `NimNode`. A node of kind `nnkStmtList` is suitable for
  # this use case.
  result = nnkStmtList.newTree()
  # iterate over any argument that is passed to this macro:
  for n in args:
    # add a call to the statement list that writes the expression;
    # `toStrLit` converts an AST to its string representation:
    result.add newCall("write", newIdentNode("stdout"), newLit(n.repr))
    # add a call to the statement list that writes ": "
    result.add newCall("write", newIdentNode("stdout"), newLit(": "))
    # add a call to the statement list that writes the expressions value:
    result.add newCall("writeLine", newIdentNode("stdout"), n)

var
  a: array[0..10, int]
  x = "some string"
a[0] = 42
a[1] = 45

debug(a[0], a[1], x)

Вызов макроса раскрывается в:

write(stdout, "a[0]")
write(stdout, ": ")
writeLine(stdout, a[0])

write(stdout, "a[1]")
write(stdout, ": ")
writeLine(stdout, a[1])

write(stdout, "x")
write(stdout, ": ")
writeLine(stdout, x)

Аргументы, передаваемые параметру varargs, заключены в выражение конструктора массива. Вот почему debug итерируется по всем дочерним элементам args.

bindSym

Вышеупомянутый debug макрос полагается на то, что write, writeLine и stdout объявлены в системном модуле и, таким образом, видны в контексте инициализации. Существует способ использовать связанные идентификаторы (также называемые символами) вместо использования несвязанных идентификаторов. Для этого можно использовать встроенную функцию bindSym:

import std/macros

macro debug(n: varargs[typed]): untyped =
  result = newNimNode(nnkStmtList, n)
  for x in n:
    # we can bind symbols in scope via 'bindSym':
    add(result, newCall(bindSym"write", bindSym"stdout", toStrLit(x)))
    add(result, newCall(bindSym"write", bindSym"stdout", newStrLitNode(": ")))
    add(result, newCall(bindSym"writeLine", bindSym"stdout", x))

var
  a: array[0..10, int]
  x = "some string"
a[0] = 42
a[1] = 45

debug(a[0], a[1], x)

Вызов макроса раскрывается в:

write(stdout, "a[0]")
write(stdout, ": ")
writeLine(stdout, a[0])

write(stdout, "a[1]")
write(stdout, ": ")
writeLine(stdout, a[1])

write(stdout, "x")
write(stdout, ": ")
writeLine(stdout, x)

В этой версии debug, символы write, writeLine и stdout уже связаны и больше не ищутся. Как показывает пример, bindSym работает с перегруженными символами неявно.

Обратите внимание, что имена символов, передаваемые в bindSym, должны быть константами. Экспериментальная функция dynamicBindSym (экспериментальное руководство) позволяет вычислить это значение динамически.

Блоки после оператора

Макросы могут получать of, elif, else, except, finally и do блоки (включая их различные формы, такие как do с параметрами процедуры) в качестве аргументов, если вызываются в форме оператора.

macro performWithUndo(task, undo: untyped) = ...

performWithUndo do:
  # multiple-line block of code
  # to perform the task
do:
  # code to undo it

let num = 12
# a single colon may be used if there is no initial block
match (num mod 3, num mod 5):
of (0, 0):
  echo "FizzBuzz"
of (0, _):
  echo "Fizz"
of (_, 0):
  echo "Buzz"
else:
  echo num

Макрос цикла for

Макрос, принимающий в качестве единственного входного параметра выражение специального типа system.ForLoopStmt, может переписать весь цикл for:

import std/macros

macro example(loop: ForLoopStmt) =
  result = newTree(nnkForStmt)    # Create a new For loop.
  result.add loop[^3]             # This is "item".
  result.add loop[^2][^1]         # This is "[1, 2, 3]".
  result.add newCall(bindSym"echo", loop[0])

for item in example([1, 2, 3]): discard

Развертывается в:

for item in items([1, 2, 3]):
  echo item

Ещё один пример:

import std/macros

macro enumerate(x: ForLoopStmt): untyped =
  expectKind x, nnkForStmt
  # check if the starting count is specified:
  var countStart = if x[^2].len == 2: newLit(0) else: x[^2][1]
  result = newStmtList()
  # we strip off the first for loop variable and use it as an integer counter:
  result.add newVarStmt(x[0], countStart)
  var body = x[^1]
  if body.kind != nnkStmtList:
    body = newTree(nnkStmtList, body)
  body.add newCall(bindSym"inc", x[0])
  var newFor = newTree(nnkForStmt)
  for i in 1..x.len-3:
    newFor.add x[i]
  # transform enumerate(X) to 'X'
  newFor.add x[^2][^1]
  newFor.add body
  result.add newFor
  # now wrap the whole macro in a block to create a new scope
  result = quote do:
    block: `result`

for a, b in enumerate(items([1, 2, 3])):
  echo a, " ", b

# without wrapping the macro in a block, we'd need to choose different
# names for `a` and `b` here to avoid redefinition errors
for a, b in enumerate(10, [1, 2, 3, 5]):
  echo a, " ", b

Макросы оператора case

Макросы с именем `` case `` могут предоставить реализации операторов case для определённых типов. Ниже приведён пример такой реализации для кортежей, использующий существующий оператор равенства для кортежей (предоставляемый в system.==):

import std/macros

macro `case`(n: tuple): untyped =
  result = newTree(nnkIfStmt)
  let selector = n[0]
  for i in 1 ..< n.len:
    let it = n[i]
    case it.kind
    of nnkElse, nnkElifBranch, nnkElifExpr, nnkElseExpr:
      result.add it
    of nnkOfBranch:
      for j in 0..it.len-2:
        let cond = newCall("==", selector, it[j])
        result.add newTree(nnkElifBranch, cond, it[^1])
    else:
      error "custom 'case' for tuple cannot handle this node", it

case ("foo", 78)
of ("foo", 78): echo "yes"
of ("bar", 88): echo "no"
else: discard

case макросы подлежат разрешению перегрузки. Тип выражения селектора оператора case сопоставляется с типом первого аргумента макроса case. Затем весь оператор case передаётся вместо аргумента, и макрос вычисляется.

Другими словами, макрос должен преобразовать весь оператор case, но для определения вызываемого макроса используется только выражение селектора оператора.

Специальные типы

static[T]

Как следует из их названия, статические параметры должны быть константными выражениями:

proc precompiledRegex(pattern: static string): RegEx =
  var res {.global.} = re(pattern)
  return res

precompiledRegex("/d+") # Replaces the call with a precompiled
                        # regex, stored in a global variable

precompiledRegex(paramStr(1)) # Error, command-line options
                              # are not constant expressions

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

Статические параметры также могут появляться в сигнатурах общих типов:

type
  Matrix[M,N: static int; T: Number] = array[0..(M*N - 1), T]
    # Note how `Number` is just a type constraint here, while
    # `static int` requires us to supply an int value
  
  AffineTransform2D[T] = Matrix[3, 3, T]
  AffineTransform3D[T] = Matrix[4, 4, T]

var m1: AffineTransform3D[float]  # OK
var m2: AffineTransform2D[string] # Error, `string` is not a `Number`

Обратите внимание, что static T — это просто синтаксический удобный способ для базового общего типа static[T]. Параметр типа может быть опущен для получения типа класса всех константных выражений. Более специфический тип класса может быть создан путём инициализации static другим типом класса.

Можно принудительно вычислить выражение как константное выражение, применив к нему приведение к соответствующему типу static:

import std/math

echo static(fac(5)), " ", static[bool](16.isPowerOfTwo)

Компилятор сообщит о любом сбое в вычислении выражения или возможной ошибке несовпадения типов.

typedesc[T]

Во многих контекстах Nim обрабатывает имена типов как обычные значения. Эти значения существуют только во время компиляции, но так как все значения должны иметь тип, typedesc считается их специальным типом.

typedesc действует как общий тип. Например, тип символа int — это typedesc[int]. Как и в случае с обычными общими типами, при опущении общего параметра typedesc обозначает тип класса всех типов. Для удобства синтаксиса можно также использовать typedesc в качестве модификатора.

Процедуры с параметрами typedesc считаются неявно общими. Они будут инициализироваться для каждой уникальной комбинации предоставленных типов, и в теле процедуры имя каждого параметра будет ссылаться на связанный конкретный тип:

proc new(T: typedesc): ref T =
  echo "allocating ", T.name
  new(result)

var n = Node.new
var tree = new(BinaryTree[int])

Когда присутствует несколько параметров типа, они будут связываться свободно с различными типами. Чтобы принудительно установить поведение связывания один раз, можно использовать явный параметр общего типа:

proc acceptOnlyTypePairs[T, U](A, B: typedesc[T]; C, D: typedesc[U])

После связывания параметры типа могут появляться в остальной части сигнатуры процедуры:

template declareVariableWithType(T: typedesc, value: T) =
  var x: T = value

declareVariableWithType int, 42

Разрешение перегрузки может быть дополнительно изменено путём ограничения набора типов, которые будут соответствовать параметру типа. На практике это делается путём добавления атрибутов к типам с помощью шаблонов. Ограничение может быть конкретным типом или типом класса.

template maxval(T: typedesc[int]): int = high(int)
template maxval(T: typedesc[float]): float = Inf

var i = int.maxval
var f = float.maxval
when false:
  var s = string.maxval # error, maxval is not implemented for string

template isNumber(t: typedesc[object]): string = "Don't think so."
template isNumber(t: typedesc[SomeInteger]): string = "Yes!"
template isNumber(t: typedesc[SomeFloat]): string = "Maybe, could be NaN."

echo "is int a number? ", isNumber(int)
echo "is float a number? ", isNumber(float)
echo "is RootObj a number? ", isNumber(RootObj)

Передача typedesc почти идентична, за исключением того, что макрос не инициализируется обобщённо. Выражение типа просто передаётся как NimNode макросу, как и всё остальное.

import std/macros

macro forwardType(arg: typedesc): typedesc =
  # `arg` is of type `NimNode`
  let tmp: NimNode = arg
  result = tmp

var tmp: forwardType(int)

Оператор typeof

Примечание: typeof(x) по историческим причинам также может быть записан как type(x), но type(x) не рекомендуется.

Тип данного выражения можно получить, создав значение typeof из него (во многих других языках это известно как оператор typeof):

var x = 0
var y: typeof(x) # y has type int

Если typeof используется для определения типа результата вызова процедуры/итератора/конвертера c(X) (где X обозначает, возможно, пустой список аргументов), то предпочтительнее интерпретация, где c является итератором, по сравнению с другими интерпретациями, но это поведение можно изменить, передав typeOfProc в качестве второго аргумента typeof:

iterator split(s: string): string = discard
proc split(s: string): seq[string] = discard

# since an iterator is the preferred interpretation, this has the type `string`:
assert typeof("a b c".split) is string

assert typeof("a b c".split, typeOfProc) is seq[string]

Модули

Nim поддерживает разделение программы на части с помощью концепции модуля. Каждый модуль должен находиться в своём файле и имеет свой собственный пространство имён. Модули обеспечивают скрытие информации и раздельную компиляцию. Модуль может получить доступ к символам другого модуля с помощью оператора import. Рекурсивные зависимости модулей разрешены, но немного специфичны. Экспортируются только символы верхнего уровня, помеченные звёздочкой (*). Валидное имя модуля может быть только валидным идентификатором Nim (и, следовательно, его имя файла — identifier.nim).

Алгоритм компиляции модулей:

  • Компилируется весь модуль в обычном режиме, рекурсивно следуя операторам import.
  • Если существует цикл, импортируются только уже проанализированные символы (которые экспортируются); если встречается неизвестный идентификатор, то происходит прерывание.

Это лучше всего иллюстрируется примером:

# Module A
type
  T1* = int  # Module A exports the type `T1`
import B     # the compiler starts parsing B

proc main() =
  var i = p(3) # works because B has been parsed completely here

main()
# Module B
import A  # A is not parsed here! Only the already known symbols
          # of A are imported.

proc p*(x: A.T1): A.T1 =
  # this works because the compiler has already
  # added T1 to A's interface symbol table
  result = x + 1

Оператор import

После ключевого слова import может следовать список имён модулей или одно имя модуля, за которым следует список except для предотвращения импорта некоторых символов:

import std/strutils except `%`, toUpperAscii

# doesn't work then:
echo "$1" % "abc".toUpperAscii

Не проверяется, что список except действительно экспортируется из модуля. Эта функция позволяет компилировать против различных версий модуля, даже если одна версия не экспортирует некоторые из этих идентификаторов.

Оператор import разрешён только на верхнем уровне.

Строковые литералы могут использоваться для операторов import/include. При использовании компилятор выполняет подстановку путей.

Оператор include

Оператор include делает нечто принципиально отличное от импорта модуля: он просто включает содержимое файла. Оператор include полезен для разделения большого модуля на несколько файлов:

include fileA, fileB, fileC

Оператор include может использоваться вне верхнего уровня, например:

# Module A
echo "Hello World!"
# Module B
proc main() =
  include A

main() # => Hello World!

Имена модулей в импортах

Псевдоним модуля может быть введён с помощью ключевого слова as, после чего исходное имя модуля недоступно:

import std/strutils as su, std/sequtils as qu

echo su.format("$1", "lalelu")

Можно использовать обозначения path/to/module или "path/to/module" для ссылки на модуль в подкаталогах:

import lib/pure/os, "lib/pure/times"

Обратите внимание, что имя модуля по-прежнему strutils, а не lib/pure/strutils, поэтому нельзя выполнить:

import lib/pure/strutils
echo lib/pure/strutils.toUpperAscii("abc")

Аналогично, следующее не имеет смысла, так как имя уже strutils:

import lib/pure/strutils as strutils

Коллективный импорт из каталога

Синтаксис import dir / [moduleA, moduleB] может быть использован для импорта нескольких модулей из одного каталога.

Имена путей синтаксически представляют собой либо идентификаторы Nim, либо строковые литералы. Если имя пути не является допустимым идентификатором Nim, оно должно быть строковым литералом:

import "gfx/3d/somemodule" # in quotes because '3d' is not a valid Nim identifier

Псевдо пути импорта/включения

Каталог также может быть так называемым "псевдо каталогом". Они могут использоваться для избежания неоднозначности, когда существует несколько модулей с одинаковым путём.

Существует два псевдо каталога:

  1. std: Псевдо каталог std — это абстрактное расположение стандартной библиотеки Nim. Например, синтаксис import std / strutils используется для однозначной ссылки на модуль strutils стандартной библиотеки.
  2. pkg: Псевдо каталог pkg используется для однозначной ссылки на пакет Nimble. Однако, для технических деталей, выходящих за рамки данного документа, его семантика такова: Используйте путь поиска для поиска имени модуля, но игнорируйте расположения стандартной библиотеки. Другими словами, это противоположность std.

Рекомендуется и предпочтительно, но в настоящее время не навязывается, чтобы все импорты модулей stdlib включали псевдо каталог "std/" в качестве части имени импорта.

Инструкция from import

После ключевого слова from, имя модуля, за которым следует import, для перечисления символов, которые нужно использовать без явной полной квалификации:

from std/strutils import `%`

echo "$1" % "abc"
# always possible: full qualification:
echo strutils.replace("abc", "a", "z")

Также можно использовать from module import nil, если нужно импортировать модуль, но необходимо принудительно обеспечить полную квалификацию доступа ко всем символам в module.

Инструкция export

Инструкция export может быть использована для перенаправления символов, чтобы клиентские модули не нуждались в импорте зависимостей модуля:

# module B
type MyObject* = object
# module A
import B
export B.MyObject

proc `$`*(x: MyObject): string = "my object"
# module C
import A

# B.MyObject has been imported implicitly here:
var x: MyObject
echo $x

Когда экспортируемый символ является другим модулем, все его определения будут перенаправлены. Можно использовать список except, чтобы исключить некоторые символы.

Обратите внимание, что при экспорте необходимо указывать только имя модуля:

import foo/bar/baz
export baz

Правила области видимости

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

Область видимости блока

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

Область видимости кортежа или объекта

Идентификаторы полей внутри определения кортежа или объекта действительны в следующих местах:

  • До конца определения кортежа/объекта.
  • Обозначения полей переменной заданного типа кортежа/объекта.
  • Во всех дочерних типах типа объекта.

Область видимости модуля

Все идентификаторы модуля действительны от точки объявления до конца модуля. Идентификаторы из косвенно зависимых модулей не доступны. Модуль system автоматически импортируется в каждый модуль.

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

  • Квалификация идентификатора как module.identifier разрешает неоднозначность между модулями. (См. ниже случай, когда само имя модуля неоднозначно.)
  • Вызов идентификатора как процедуры приводит к выполнению разрешения перегрузки, что разрешает неоднозначность в случае, если одна перегрузка соответствует сильнее, чем другие.
  • Использование идентификатора в контексте, где компилятор может вывести тип идентификатора, разрешает неоднозначность в случае, если одно определение соответствует типу сильнее, чем другие.

    # Module A
    var x*: string
    proc foo*(a: string) =
      echo "A: ", a
    # Module B
    var x*: int
    proc foo*(b: int) =
      echo "B: ", b
    # Module C
    import A, B
    
    foo("abc") # A: abc
    foo(123) # B: 123
    let inferred: proc (x: string) = foo
    foo("def") # A: def
    
    write(stdout, x) # error: x is ambiguous
    write(stdout, A.x) # no error: qualifier used
    
    proc bar(a: int): int = a + 1
    assert bar(x) == x + 1 # no error: only A.x of type int matches
    
    var x = 4
    write(stdout, x) # not ambiguous: uses the module C's x

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

# Module A/C
proc fb* = echo "fizz"
# Module B/C
proc fb* = echo "buzz"
import A/C
import B/C

C.fb() # Error: ambiguous identifier: 'C'
import A/C as fizz
import B/C

fizz.fb() # Works

Пакеты

Коллекция модулей в древовидной структуре файлов с файлом identifier.nimble в корне дерева называется пакетом Nimble. Допустимое имя пакета может быть только допустимым идентификатором Nim, и поэтому его имя файла — identifier.nimble, где identifier — желаемое имя пакета. Модулю без файла .nimble присваивается идентификатор пакета: unknown.

Различие между пакетами позволяет ограничить сообщения об ошибках компилятора по текущему проекту, в отличии от внешних пакетов.

Сообщения компилятора

Компилятор Nim выводит различные типы сообщений: сообщения hint, warning и error. Сообщение error выводится, если компилятор обнаруживает статическую ошибку.

Директивы

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

Директива deprecated

Директива deprecated используется для маркировки символа как устаревшего:

proc p() {.deprecated.}
var x {.deprecated.}: char

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

proc thing(x: bool) {.deprecated: "use thong instead".}

Директива compileTime

Директива compileTime используется для маркировки процедуры или переменной, предназначенной только для выполнения во время компиляции. Для неё не будет генерироваться код. Процедуры компиляции времени полезны как помощники для макросов. С версии 0.12.0 языка, процедура, использующая system.NimNode в своих типах параметров, неявно объявляется compileTime:

proc astHelper(n: NimNode): NimNode =
  result = n

Это то же самое, что:

proc astHelper(n: NimNode): NimNode {.compileTime.} =
  result = n

compileTime переменные доступны и во время выполнения. Это упрощает определённые идиомы, в которых переменные заполняются во время компиляции (например, таблицы поиска), но к ним осуществляется доступ во время выполнения:

import std/macros

var nameToProc {.compileTime.}: seq[(string, proc (): string {.nimcall.})]

macro registerProc(p: untyped): untyped =
  result = newTree(nnkStmtList, p)
  
  let procName = p[0]
  let procNameAsStr = $p[0]
  result.add quote do:
    nameToProc.add((`procNameAsStr`, `procName`))

proc foo: string {.registerProc.} = "foo"
proc bar: string {.registerProc.} = "bar"
proc baz: string {.registerProc.} = "baz"

doAssert nameToProc[2][1]() == "baz"

Директива noreturn

Директива noreturn используется для маркировки процедуры, которая никогда не возвращает значение.

Директива acyclic

Директива acyclic может быть использована для типов объектов, чтобы пометить их как ациклические, даже если они кажутся циклическими. Это оптимизация для сборщика мусора, чтобы он не рассматривал объекты этого типа как часть цикла:

type
  Node = ref NodeObj
  NodeObj {.acyclic.} = object
    left, right: Node
    data: string

Или если мы напрямую используем объект ref:

type
  Node {.acyclic.} = ref object
    left, right: Node
    data: string

В примере древовидная структура объявлена с типом Node. Обратите внимание, что определение типа рекурсивно, и GC должен предполагать, что объекты этого типа могут образовывать циклическую структуру. Директива acyclic передаёт информацию о том, что этого не произойдёт, сборщику мусора. Если программист использует директиву acyclic для типов данных, которые на самом деле являются циклическими, это может привести к утечкам памяти, но безопасность памяти сохраняется.

Директива final

Директива final может быть использована для типа объекта, чтобы указать, что он не может быть унаследован. Обратите внимание, что наследование доступно только для объектов, которые наследуются от существующего объекта (через синтаксис object of SuperType) или которые были помечены как inheritable.

Директива shallow

Директива shallow влияет на семантику типа: компилятор допускается выполнять поверхностную копию. Это может вызвать серьёзные семантические проблемы и нарушить безопасность памяти! Однако, это может значительно ускорить присваивания, так как семантика Nim требует глубокого копирования последовательностей и строк. Это может быть дорогостоящим, особенно если последовательности используются для построения древовидной структуры:

type
  NodeKind = enum nkLeaf, nkInner
  Node {.shallow.} = object
    case kind: NodeKind
    of nkLeaf:
      strVal: string
    of nkInner:
      children: seq[Node]

Директива pure

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

Тип перечисления может быть помечен как pure. Затем доступ к его полям всегда требует полной квалификации.

Директива asmNoStackFrame

Процедура может быть помечена директивой asmNoStackFrame, чтобы сообщить компилятору, что он не должен генерировать кадр стека для процедуры. Также не генерируются инструкции выхода, такие как return result;, а сгенерированная функция C объявляется как __declspec(naked) или __attribute__((naked)) (в зависимости от используемого компилятора C).

Примечание: эту директиву следует использовать только для процедур, состоящих только из инструкций ассемблера.

Директива error

Директива error используется для того, чтобы компилятор выводил сообщение об ошибке с указанным содержимым. Однако, компиляция не обязательно прерывается после ошибки.

Директива error также может быть использована для аннотации символа (например, итератора или процедуры). Использование символа затем вызывает статическую ошибку. Это особенно полезно для исключения того, что некоторая операция допустима из-за перегрузки и преобразований типов:

## check that underlying int values are compared and not the pointers:
proc `==`(x, y: ptr int): bool {.error.}

Директива fatal

Директива fatal используется для того, чтобы компилятор выводил сообщение об ошибке с указанным содержимым. В отличие от директивы error, компиляция гарантированно прерывается этой директивой. Пример:

when not defined(objc):
  {.fatal: "Compile this program with the objc command!".}

Директива warning

Директива warning используется для того, чтобы компилятор выводил сообщение с предупреждением с указанным содержимым. Компиляция продолжается после предупреждения.

Директива hint

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

Директива line

Директива line может использоваться для влияния на информацию о строке аннотированного оператора, как видно в отслеживании стека:

template myassert*(cond: untyped, msg = "") =
  if not cond:
    # change run-time line information of the 'raise' statement:
    {.line: instantiationInfo().}:
      raise newException(AssertionDefect, msg)

Если используется пragma line с параметром, то этот параметр должен быть tuple[filename: string, line: int]. Если оно используется без параметра, то используется system.instantiationInfo().

Pragma linearScanEnd

Pragma linearScanEnd может использоваться для указания компилятору, как следует скомпилировать оператор case в Nim. Синтаксически он должен использоваться как оператор:

case myInt
of 0:
  echo "most common case"
of 1:
  {.linearScanEnd.}
  echo "second most common case"
of 2: echo "unlikely: use branch table"
else: echo "unlikely too: use branch table for ", myInt

В примере, ветви case 0 и 1 встречаются намного чаще, чем другие. Поэтому сгенерированный код ассемблера должен сначала проверять эти значения, чтобы у предсказателя ветвлений процессора была высокая вероятность успеха (избегая дорогостоящей остановки конвейера процессора). Другие ветви могут быть помещены в таблицу переходов для расхода O(1), но с затратами на (вероятно) остановку конвейера.

Pragma linearScanEnd должен быть помещен в последнюю ветвь, которая должна быть проверена с помощью линейного сканирования. Если он помещен в последнюю ветвь всего оператора case, весь оператор case использует линейное сканирование.

Pragma computedGoto

Pragma computedGoto может использоваться для указания компилятору, как следует скомпилировать оператор case внутри оператора while true. Синтаксически он должен использоваться как оператор внутри цикла:

type
  MyEnum = enum
    enumA, enumB, enumC, enumD, enumE

proc vm() =
  var instructions: array[0..100, MyEnum]
  instructions[2] = enumC
  instructions[3] = enumD
  instructions[4] = enumA
  instructions[5] = enumD
  instructions[6] = enumC
  instructions[7] = enumA
  instructions[8] = enumB
  
  instructions[12] = enumE
  var pc = 0
  while true:
    {.computedGoto.}
    let instr = instructions[pc]
    case instr
    of enumA:
      echo "yeah A"
    of enumC, enumD:
      echo "yeah CD"
    of enumB:
      echo "yeah B"
    of enumE:
      break
    inc(pc)

vm()

Как показывает пример, computedGoto в основном полезен для интерпретаторов. Если базовый бэкенд (компилятор C) не поддерживает расширение computed goto, то пragma просто игнорируется.

Pragma immediate

Pragma immediate устарело. Смотрите Типизированные против нетипизированных параметров.

Pragma redefine

Переопределение символов шаблона с одной и той же сигнатурой разрешено. Это можно сделать явно с помощью pragmy redefine:

template foo: int = 1
echo foo() # 1
template foo: int {.redefine.} = 2
echo foo() # 2
# warning: implicit redefinition of template
template foo: int = 3

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

Pragmas опций компиляции

Перечисленные здесь pragmas могут использоваться для переопределения параметров генерации кода для proc/метода/конвертера.

Текущая реализация предоставляет следующие возможные параметры (могут быть добавлены и другие).

pragma допустимые значения описание
checks on|off Включает или отключает генерацию кода для всех проверок во время выполнения.
boundChecks on|off Включает или отключает генерацию кода для проверок границ массивов.
overflowChecks on|off Включает или отключает генерацию кода для проверок переполнения.
nilChecks on|off Включает или отключает генерацию кода для проверок на nil-указатели.
assertions on|off Включает или отключает генерацию кода для утверждений.
warnings on|off Включает или отключает сообщения о предупреждениях компилятора.
hints on|off Включает или отключает сообщения о подсказках компилятора.
optimization none|speed|size Оптимизировать код для скорости или размера, или отключить оптимизацию.
patterns on|off Включает или отключает шаблоны/макросы переписывания терминов.
callconv cdecl|... Устанавливает стандартную соглашение о вызовах для всех процедур (и типов процедур), которые следуют за ней.

Пример:

{.checks: off, optimization: speed.}
# compile without runtime checks and optimize for speed

Pragmas push и pop

Pragmas push/pop очень похожи на директивы опций, но используются для временного переопределения настроек. Пример:

{.push checks: off.}
# compile this section without runtime checks as it is
# speed critical
# ... some code ...
{.pop.} # restore old settings

push/pop может включать/выключать некоторые pragmas стандартной библиотеки, пример:

{.push inline.}
proc thisIsInlined(): int = 42
func willBeInlined(): float = 42.0
{.pop.}
proc notInlined(): int = 9

{.push discardable, boundChecks: off, compileTime, noSideEffect, experimental.}
template example(): string = "https://nim-lang.org"
{.pop.}

{.push deprecated, used, stackTrace: off.}
proc sample(): bool = true
{.pop.}

Для pragmas сторонних разработчиков, это зависит от их реализации, но используется тот же синтаксис.

Pragma register

Pragma register предназначен только для переменных. Он объявляет переменную как register, давая компилятору подсказку, что переменная должна быть размещена в регистре аппаратного обеспечения для более быстрого доступа. Однако компиляторы C обычно игнорируют это, и по вполне понятным причинам: зачастую они делают свою работу лучше без этого.

Тем не менее, в очень специфических случаях (например, цикл обработки в интерпретаторе байткода) это может дать преимущества.

Pragma global

Pragma global может быть применен к переменной внутри proc, чтобы указать компилятору сохранить ее в глобальном месте и инициализировать один раз при запуске программы.

proc isHexNumber(s: string): bool =
  var pattern {.global.} = re"[0-9a-fA-F]+"
  result = s.match(pattern)

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

Отключение определенных сообщений

Nim генерирует некоторые предупреждения и подсказки, которые могут раздражать пользователя. Предоставляется механизм для отключения определенных сообщений: каждое сообщение о подсказке или предупреждении связано со символом. Это идентификатор сообщения, который может быть использован для включения или отключения сообщения, поместив его в скобки после pragmy:

{.hint[XDeclaredButNotUsed]: off.} # Turn off the hint about declared but not used symbols.

Это часто лучше, чем отключение всех предупреждений сразу.

Pragma used

Nim выдает предупреждение для символов, которые не экспортированы и не используются. Pragma used может быть прикреплен к символу, чтобы подавить это предупреждение. Это особенно полезно, когда символ был сгенерирован макросом:

template implementArithOps(T) =
  proc echoAdd(a, b: T) {.used.} =
    echo a + b
  proc echoSub(a, b: T) {.used.} =
    echo a - b

# no warning produced for the unused 'echoSub'
implementArithOps(int)
echoAdd 3, 5

used также может использоваться как оператор верхнего уровня для маркировки модуля как "используемого". Это предотвращает предупреждение "Unused import":

# module: debughelper.nim
when defined(nimHasUsed):
  # 'import debughelper' is so useful for debugging
  # that Nim shouldn't produce a warning for that import,
  # even if currently unused:
  {.used.}

Pragma experimental

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

Пример:

import std/threadpool
{.experimental: "parallel".}

proc threadedEcho(s: string, i: int) =
  echo(s, " ", $i)

proc useParallel() =
  parallel:
    for i in 0..4:
      spawn threadedEcho("echo in parallel", i)

useParallel()

В качестве оператора верхнего уровня pragma experimental включает функцию для остальной части модуля, в котором она включена. Это проблематично для макросов и обобщений, которые пересекают границы модулей. В настоящее время такие использования должны быть помещены в среду .push/pop:

# client.nim
proc useParallel*[T](unused: T) =
  # use a generic T here to show the problem.
  {.push experimental: "parallel".}
  parallel:
    for i in 0..4:
      echo "echo in parallel"
  
  {.pop.}
import client
useParallel(1)

Pragmas, специфичные для реализации

В этом разделе описываются дополнительные pragmas, которые поддерживает текущая реализация Nim, но которые не должны рассматриваться как часть спецификации языка.

Pragma bitsize

Pragma bitsize предназначен для членов полей объектов. Он объявляет поле как битовое поле в C/C++.

type
  mybitfield = object
    flag {.bitsize:1.}: cuint

генерирует:

struct mybitfield {
  unsigned int flag:1;
};

Pragma size

Nim автоматически определяет размер перечисления. Но при обертывании типа перечисления C, он должен иметь определенный размер. Pragma size pragma позволяет указать размер типа перечисления.

type
  EventType* {.size: sizeof(uint32).} = enum
    QuitEvent,
    AppTerminating,
    AppLowMemory

doAssert sizeof(EventType) == sizeof(uint32)

Pragma size pragma также может указать размер неполного типа объекта importc, чтобы можно было получить его размер во время компиляции, даже если он был объявлен без полей.

type
    AtomicFlag* {.importc: "atomic_flag", header: "<stdatomic.h>", size: 1.} = object
  
  static:
    # if AtomicFlag didn't have the size pragma, this code would result in a compile time error.
    echo sizeof(AtomicFlag)

Pragma size pragma принимает только значения 1, 2, 4 или 8.

Pragma align

Pragma align предназначен для переменных и членов полей объектов. Он изменяет требование выравнивания объявляемого объекта. Аргумент должен быть константой, являющейся степенью двойки. Действительные ненулевые выравнивания, которые слабее, чем другие pragmas align в том же объявлении, игнорируются. Выравнивания, которые слабее, чем требование выравнивания типа, игнорируются.

type
  sseType = object
    sseData {.align(16).}: array[4, float32]
  
  # every object will be aligned to 128-byte boundary
  Data = object
    x: char
    cacheline {.align(128).}: array[128, char] # over-aligned array of char,

proc main() =
  echo "sizeof(Data) = ", sizeof(Data), " (1 byte + 127 bytes padding + 128-byte array)"
  # output: sizeof(Data) = 256 (1 byte + 127 bytes padding + 128-byte array)
  echo "alignment of sseType is ", alignof(sseType)
  # output: alignment of sseType is 16
  var d {.align(2048).}: Data # this instance of data is aligned even stricter

main()

Этот pragma не влияет на бэкенд JS.

Pragma noalias

Начиная с версии 1.4 компилятора Nim, существует аннотация .noalias для переменных и параметров. Она напрямую отображается на ключевое слово C/C++ restrict и означает, что базовый указатель указывает на уникальное место в памяти; других псевдонимов этого местоположения не существует. Это непроверяется, что ограничение псевдонима соблюдается. Если ограничение нарушено, оптимизатор бэкенда свободен неправильно скомпилировать код. Это небезопасная функция языка.

В идеале в будущих версиях языка ограничение будет выполняться во время компиляции. (Поэтому и было выбрано название noalias, а не более длинное название, например, unsafeAssumeNoAlias.)

Pragma volatile

Pragma volatile предназначен только для переменных. Он объявляет переменную как volatile, что бы это ни означало в C/C++ (его семантика не определена в C/C++).

Примечание: Этот pragma не будет существовать для бэкенда LLVM.

Pragma nodecl

Pragma nodecl может быть применен практически к любому символу (переменной, процедуре, типу и т. д.) и иногда полезен для взаимодействия с C: он сообщает Nim, что он не должен генерировать объявление для символа в коде C. Например:

var
  EACCES {.importc, nodecl.}: cint # pretend EACCES was a variable, as
                                   # Nim does not know its value

Однако, pragma header часто является лучшей альтернативой.

Примечание: Это не будет работать для бэкенда LLVM.

Pragma header

Pragma header очень похож на pragma nodecl: Он может быть применен к практически любому символу и указывает, что он не должен быть объявлен, а вместо этого сгенерированный код должен содержать заголовок #include:

type
  PFile {.importc: "FILE*", header: "<stdio.h>".} = distinct pointer
    # import C's FILE* type; Nim will treat it as a new pointer type

Pragma header всегда ожидает строковую константу. Строковая константа содержит заголовочный файл: как обычно для C, заголовочный файл системы заключен в угловые скобки: <>. Если угловые скобки не указаны, Nim заключает заголовочный файл в "" в сгенерированном коде C.

Примечание: Это не будет работать для бэкенда LLVM.

Pragma IncompleteStruct

Pragma incompleteStruct сообщает компилятору не использовать базовый C struct в выражении sizeof:

type
  DIR* {.importc: "DIR", header: "<dirent.h>",
         pure, incompleteStruct.} = object

Pragma compile

Pragma compile может быть использован для компиляции и линковки файла исходного кода C/C++ с проектом:

Этот pragma может принимать три формы. Первая — это простой входной файл:

{.compile: "myfile.cpp".}

Второй вариант — кортеж, где второй аргумент — имя вывода форматировщика strutils:

{.compile: ("file.c", "$1.o").}

Примечание: Nim вычисляет контрольную сумму SHA1 и перекомпилирует файл только в случае его изменения. Можно использовать параметр командной строки -f для принудительной перекомпиляции файла.

Начиная с версии 1.4, также доступен псевдоним compile с таким синтаксисом:

{.compile("myfile.cpp", "--custom flags here").}

Как видно из примера, этот новый вариант позволяет задавать пользовательские флаги, которые передаются компилятору C при перекомпиляции файла.

Псевдоним Link

Псевдоним link можно использовать для подключения дополнительного файла к проекту:

{.link: "myfile.o".}

Псевдоним passc

Псевдоним passc можно использовать для передачи дополнительных параметров компилятору C, как это делается с помощью параметра командной строки --passc:

{.passc: "-Wall -Werror".}

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

{.passc: gorge("pkg-config --cflags sdl").}

Псевдоним localPassC

Псевдоним localPassC можно использовать для передачи дополнительных параметров компилятору C, но только для файла C/C++, сгенерированного из модуля Nim, в котором находится псевдоним:

# Module A.nim
# Produces: A.nim.cpp
{.localPassC: "-Wall -Werror".} # Passed when compiling A.nim.cpp

Псевдоним passl

Псевдоним passl можно использовать для передачи дополнительных параметров компоновщику, как это делается с помощью параметра командной строки --passl:

{.passl: "-lSDLmain -lSDL".}

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

{.passl: gorge("pkg-config --libs sdl").}

Псевдоним Emit

Псевдоним emit можно использовать для непосредственного влияния на вывод генератора кода компилятора. Код затем становится непереносимым в другие генераторы кода/бекенды. Его использование крайне не рекомендуется! Однако он может быть чрезвычайно полезен для взаимодействия с кодом C++ или Objective C.

Пример:

{.emit: """
static int cvariable = 420;
""".}

{.push stackTrace:off.}
proc embedsC() =
  var nimVar = 89
  # access Nim symbols within an emit section outside of string literals:
  {.emit: ["""fprintf(stdout, "%d\n", cvariable + (int)""", nimVar, ");"].}
{.pop.}

embedsC()

nimbase.h определяет NIM_EXTERNC макрос C, который можно использовать для extern "C" кода, чтобы работать с nim c и nim cpp, например:

proc foobar() {.importc:"$1".}
{.emit: """
#include <stdio.h>
NIM_EXTERNC
void fun(){}
""".}
Примечание: Для обратной совместимости, если аргумент оператора emit — строковая константа, символы Nim можно указывать в обратных кавычках. Однако это использование устарело.

Для оператора emit верхнего уровня раздел, куда должен быть вставлен код в сгенерированном файле C/C++, можно задать с помощью префиксов /*TYPESECTION*/ или /*VARSECTION*/ или /*INCLUDESECTION*/:

{.emit: """/*TYPESECTION*/
struct Vector3 {
public:
  Vector3(): x(5) {}
  Vector3(float x_): x(x_) {}
  float x;
};
""".}

type Vector3 {.importcpp: "Vector3", nodecl} = object
  x: cfloat

proc constructVector3(a: cfloat): Vector3 {.importcpp: "Vector3(@)", nodecl}

Псевдоним ImportCpp

Примечание: c2nim может анализировать большой подмножество C++ и знает язык шаблонов псевдонима importcpp. Нет необходимости знать все детали, описанные здесь.

Аналогично псевдониму importc для C, псевдоним importcpp можно использовать для импорта методов C++ или символов C++ в целом. Сгенерированный код затем использует синтаксис вызова методов C++: obj->method(arg). В сочетании с псевдонимами header и emit это позволяет небрежному взаимодействию с библиотеками, написанными на C++:

# Horrible example of how to interface with a C++ engine ... ;-)

{.link: "/usr/lib/libIrrlicht.so".}

{.emit: """
using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;
""".}

const
  irr = "<irrlicht/irrlicht.h>"

type
  IrrlichtDeviceObj {.header: irr,
                      importcpp: "IrrlichtDevice".} = object
  IrrlichtDevice = ptr IrrlichtDeviceObj

proc createDevice(): IrrlichtDevice {.
  header: irr, importcpp: "createDevice(@)".}
proc run(device: IrrlichtDevice): bool {.
  header: irr, importcpp: "#.run(@)".}

Для работы необходимо указать компилятору сгенерировать C++ (команда cpp). Условный символ cpp определен, когда компилятор генерирует код C++.

Пространства имён

В примере с небрежным взаимодействием используется .emit для создания using namespace объявлений. Обычно намного лучше использовать вместо этого ссылку на импортированное имя через запись с namespace::identifier:

type
  IrrlichtDeviceObj {.header: irr,
                      importcpp: "irr::IrrlichtDevice".} = object

Importcpp для перечислений

Когда importcpp применяется к типу перечисления, числовые значения перечисления аннотируются типом C++ перечисления, как в этом примере: ((TheCppEnum)(3)). (Это оказался самый простой способ его реализации.)

Importcpp для процедур

Обратите внимание, что вариант importcpp для процедур использует несколько загадочный язык шаблонов для максимальной гибкости:

  • Символ решётки # заменяется первым или следующим аргументом.
  • Точка после решётки #. указывает, что вызов должен использовать точку или стрелку C++.
  • Символ @ @ заменяется оставшимися аргументами, разделенными запятыми.

Например:

proc cppMethod(this: CppObj, a, b, c: cint) {.importcpp: "#.CppMethod(@)".}
var x: ptr CppObj
cppMethod(x[], 1, 2, 3)

Производит:

x->CppMethod(1, 2, 3)

Как специальное правило для сохранения обратной совместимости со старыми версиями псевдонима importcpp, если нет специального символа шаблона (любого из # ' @), предполагается использование точки или стрелки C++, поэтому вышеприведённый пример также можно записать как:

proc cppMethod(this: CppObj, a, b, c: cint) {.importcpp: "CppMethod".}

Обратите внимание, что язык шаблонов естественным образом также охватывает возможности перегрузки операторов C++:

proc vectorAddition(a, b: Vec3): Vec3 {.importcpp: "# + #".}
proc dictLookup(a: Dict, k: Key): Value {.importcpp: "#[#]".}
  • Апостроф ', за которым следует целое число i в диапазоне 0..9, заменяется типом i-го параметра. Ноль — тип результата. Это можно использовать для передачи типов шаблонам функций C++. Между ' и цифрой можно использовать звездочку, чтобы получить базовый тип типа. (Таким образом, она "удаляет звездочку" из типа; T* становится T.) Можно использовать две звёздочки, чтобы получить тип элемента элемента и т. д.

Например:

type Input {.importcpp: "System::Input".} = object
proc getSubsystem*[T](): ptr T {.importcpp: "SystemManager::getSubsystem<'*0>()", nodecl.}

let x: ptr Input = getSubsystem[Input]()

Производит:

x = SystemManager::getSubsystem<System::Input>()
  • #@ — особый случай для поддержки операции cnew. Это необходимо для того, чтобы выражение вызова было встроено непосредственно, без прохождения через временное местоположение. Это требуется только для обхода ограничения текущего генератора кода.

Например, оператор C++ new можно "импортировать" так:

proc cnew*[T](x: T): ptr T {.importcpp: "(new '*0#@)", nodecl.}

# constructor of 'Foo':
proc constructFoo(a, b: cint): Foo {.importcpp: "Foo(@)".}

let x = cnew constructFoo(3, 4)

Производит:

x = new Foo(3, 4)

Однако, в зависимости от случая использования, new Foo также можно обернуть следующим образом:

proc newFoo(a, b: cint): ptr Foo {.importcpp: "new Foo(@)".}

let x = newFoo(3, 4)

Обертывание конструкторов

Иногда класс C++ имеет закрытый конструктор копирования, и поэтому код вроде Class c = Class(1,2); не должен генерироваться, а вместо этого должен быть Class c(1,2);. Для этой цели процедура Nim, которая оборачивает конструктор C++, должна быть аннотирована псевдонимом constructor. Этот псевдоним также помогает генерировать более быстрый код C++, так как при создании не вызывается конструктор копирования:

# a better constructor of 'Foo':
proc constructFoo(a, b: cint): Foo {.importcpp: "Foo(@)", constructor.}

Обертывание деструкторов

Поскольку Nim генерирует C++ напрямую, любой деструктор вызывается неявно компилятором C++ при выходе из области видимости. Это означает, что часто можно обойтись без обертывания деструктора вообще! Однако, когда его нужно вызвать явно, его нужно обернуть. Язык шаблонов предоставляет всё необходимое:

proc destroyFoo(this: var Foo) {.importcpp: "#.~Foo()".}

Importcpp для объектов

Объекты, обобщённые с помощью importcpp, отображаются в шаблоны C++. Это означает, что шаблоны C++ можно довольно легко импортировать без необходимости языка шаблонов для типов объектов:

type
  StdMap[K, V] {.importcpp: "std::map", header: "<map>".} = object
proc `[]=`[K, V](this: var StdMap[K, V]; key: K; val: V) {.
  importcpp: "#[#] = #", header: "<map>".}

var x: StdMap[cint, cdouble]
x[6] = 91.4

Производит:

std::map<int, double> x;
x[6] = 91.4;
  • Если требуется более точный контроль, в предоставленном шаблоне можно использовать апостроф ' для обозначения конкретных параметров типа обобщённого типа. Подробнее об использовании оператора апострофа в шаблонах процедур.

    type
      VectorIterator[T] {.importcpp: "std::vector<'0>::iterator".} = object
    
    var x: VectorIterator[cint]

    Производит:

    std::vector<int>::iterator x;

Псевдоним ImportJs

Аналогично псевдониму importcpp для C++, псевдоним importjs можно использовать для импорта методов Javascript или символов в целом. Сгенерированный код затем использует синтаксис вызова методов Javascript: obj.method(arg).

Псевдоним ImportobjC

Аналогично псевдониму importc для C, псевдоним importobjc можно использовать для импорта методов Objective C. Сгенерированный код затем использует синтаксис вызова методов Objective C: [obj method param1: arg]. В сочетании с псевдонимами header и emit это позволяет небрежному взаимодействию с библиотеками, написанными на Objective C:

# horrible example of how to interface with GNUStep ...

{.passl: "-lobjc".}
{.emit: """
#include <objc/Object.h>
@interface Greeter:Object
{
}

- (void)greet:(long)x y:(long)dummy;
@end

#include <stdio.h>
@implementation Greeter

- (void)greet:(long)x y:(long)dummy
{
  printf("Hello, World!\n");
}
@end

#include <stdlib.h>
""".}

type
  Id {.importc: "id", header: "<objc/Object.h>", final.} = distinct int

proc newGreeter: Id {.importobjc: "Greeter new", nodecl.}
proc greet(self: Id, x, y: int) {.importobjc: "greet", nodecl.}
proc free(self: Id) {.importobjc: "free", nodecl.}

var g = newGreeter()
g.greet(12, 34)
g.free()

Для работы необходимо указать компилятору сгенерировать Objective C (команда objc). Условный символ objc определен, когда компилятор генерирует код Objective C.

Псевдоним CodegenDecl

Псевдоним codegenDecl можно использовать для прямого влияния на генератор кода Nim. Он получает строку формата, которая определяет, как переменная, процедура или тип объекта объявляются в сгенерированном коде.

Для переменных $1 в строке формата представляет тип переменной, $2 — имя переменной, и каждое появление $# соответствует $1/$2 соответственно по его позиции.

Следующий код Nim:

var
  a {.codegenDecl: "$# progmem $#".}: int

сгенерирует этот код C:

int progmem a

Для процедур $1 — тип возвращаемого значения процедуры, $2 — имя процедуры, $3 — список параметров, и каждое появление $# соответствует $1/$2/$3 соответственно по его позиции.

Следующий код nim:

proc myinterrupt() {.codegenDecl: "__interrupt $# $#$#".} =
  echo "realistic interrupt handler"

сгенерирует этот код:

__interrupt void myinterrupt()

Для типов объектов $1 представляет имя типа объекта, $2 — список полей, а $3 — базовый тип.

const strTemplate = """
  struct $1 {
    $2
  };
"""
type Foo {.codegenDecl:strTemplate.} = object
  a, b: int

сгенерирует этот код:

struct Foo {
  NI a;
  NI b;
};

cppNonPod псевдоним

Псевдоним cppNonPod следует использовать для типов не-POD importcpp, чтобы они корректно работали (в частности, в отношении конструкторов и деструкторов) для threadvar переменных. Это требует --tlsEmulation:off.

type Foo {.cppNonPod, importcpp, header: "funs.h".} = object
  x: cint
proc main()=
  var a {.threadvar.}: Foo

Макросы определения во время компиляции

Перечисленные здесь макросы могут быть использованы для опционального принятия значений из опции -d/--define во время компиляции.

Текущая реализация предоставляет следующие возможные опции (в дальнейшем могут быть добавлены другие).

макрос описание
intdefine Считывает определение времени сборки как целое число
strdefine Считывает определение времени сборки как строку
booldefine Считывает определение времени сборки как булево значение
const FooBar {.intdefine.}: int = 5
echo FooBar
nim c -d:FooBar=42 foobar.nim

В приведённом примере, предоставление флага -d приводит к перезаписи символа FooBar во время компиляции, выведя 42. Если бы -d:FooBar=42 был опущен, использовалось бы значение по умолчанию 5. Для проверки, было ли предоставлено значение, можно использовать defined(FooBar).

Синтаксис -d:flag фактически является сокращением для -d:flag=true.

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

const FooBar {.intdefine: "package.FooBar".}: int = 5
echo FooBar
nim c -d:package.FooBar=42 foobar.nim

Это помогает устранить неоднозначность имён определений в разных пакетах.

См. также макрос `define` для версии этих макросов, которая определяет тип определения на основе значения константы.

Пользовательские макросы

Макрос pragma

Макрос pragma может быть использован для объявления пользовательских макросов. Это полезно, потому что шаблоны и макросы Nim не влияют на макросы. Пользовательские макросы находятся в отдельном модульном пространстве имён, отличном от всех других символов. Их нельзя импортировать из модуля.

Пример:

when appType == "lib":
  {.pragma: rtl, exportc, dynlib, cdecl.}
else:
  {.pragma: rtl, importc, dynlib: "client.dll", cdecl.}

proc p*(a, b: int): int {.rtl.} =
  result = a + b

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

Пользовательские аннотации

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

template dbTable(name: string, table_space: string = "") {.pragma.}
template dbKey(name: string = "", primary_key: bool = false) {.pragma.}
template dbForeignKey(t: typedesc) {.pragma.}
template dbIgnore {.pragma.}

Рассмотрим этот стилизованный пример возможной реализации Object Relation Mapping (ORM):

const tblspace {.strdefine.} = "dev" # switch for dev, test and prod environments

type
  User {.dbTable("users", tblspace).} = object
    id {.dbKey(primary_key = true).}: int
    name {.dbKey"full_name".}: string
    is_cached {.dbIgnore.}: bool
    age: int
  
  UserProfile {.dbTable("profiles", tblspace).} = object
    id {.dbKey(primary_key = true).}: int
    user_id {.dbForeignKey: User.}: int
    read_access: bool
    write_access: bool
    admin_access: bool

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

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

Модуль макросов включает вспомогательные функции, которые могут быть использованы для упрощения доступа к пользовательским макросам hasCustomPragma, getCustomPragmaVal. Обратитесь к документации модуля macros за подробностями. Эти макросы не являются магическими; всё, что они делают, можно также реализовать, пройдясь по абстрактному синтаксическому дереву (AST) представления объекта.

Дополнительные примеры с пользовательскими макросами:

  • Лучший контроль сериализации/десериализации:

    type MyObj = object
      a {.dontSerialize.}: int
      b {.defaultDeserialize: 5.}: int
      c {.serializationKey: "_c".}: string
  • Принятие типа для инспектора gui в движке игры:

    type MyComponent = object
      position {.editable, animatable.}: Vector3
      alpha {.editRange: [0.0..1.0], animatable.}: float32

Макросы макросов

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

template command(name: string, def: untyped) = discard

proc p() {.command("print").} = discard

Это преобразуется в:

command("print"):
  proc p() = discard

type
  AsyncEventHandler = proc (x: Event) {.async.}

Это преобразуется в:

type
  AsyncEventHandler = async(proc (x: Event))

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

Есть ещё несколько применений макросов макросов, таких как в объявлениях типов, переменных и констант, но это поведение считается экспериментальным и документировано в экспериментальном руководстве вместо этого.

Интерфейс внешних функций

FFI (Foreign Function Interface) Nim является обширным, и здесь документированы только те части, которые масштабируются для других будущих бэкендов (например, бэкендов LLVM/JavaScript).

Макрос Importc

Макрос importc предоставляет способ импорта процедуры или переменной из C. Необязательный аргумент — строка, содержащая идентификатор C. Если аргумент отсутствует, имя C — это идентификатор Nim точно так, как он написан:

proc printf(formatstr: cstring) {.header: "<stdio.h>", importc: "printf", varargs.}

Когда importc применяется к оператору let, он может опустить своё значение, которое затем ожидается из C. Это может быть использовано для импорта C const:

{.emit: "const int cconst = 42;".}

let cconst {.importc, nodecl.}: cint

assert cconst == 42

Обратите внимание, что этот макрос в прошлом использовался также и для работы с бэкэндом JS для объектов и функций JS. Другие бэкэнды предоставляют ту же функцию под тем же именем. Кроме того, когда целевой язык не C, доступны другие макросы:

  • importcpp
  • importobjc
  • importjs

Строковый литерал, переданный в importc, может быть строкой форматирования:

proc p(s: cstring) {.importc: "prefix$1".}

В примере внешнее имя p установлено на prefixp. Доступен только $1, и буквальной знак доллара необходимо написать как $$.

Макрос Exportc

Макрос exportc предоставляет способ экспорта типа, переменной или процедуры в C. Перечислимые типы и константы экспортировать нельзя. Необязательный аргумент — строка, содержащая идентификатор C. Если аргумент отсутствует, имя C — это идентификатор Nim точно так, как он написан:

proc callme(formatstr: cstring) {.exportc: "callMe", varargs.}

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

Строковый литерал, переданный в exportc, может быть строкой форматирования:

proc p(s: string) {.exportc: "prefix$1".} =
  echo s

В примере внешнее имя p установлено на prefixp. Доступен только $1, и буквальной знак доллара необходимо написать как $$.

Если символ также должен быть экспортирован в динамическую библиотеку, необходимо использовать макрос dynlib дополнительно к макросу exportc. См. Макрос Dynlib для экспорта.

Макрос Extern

Как exportc или importc, макрос extern влияет на имя манглинга. Строковый литерал, переданный в extern, может быть строкой форматирования:

proc p(s: string) {.extern: "prefix$1".} =
  echo s

В примере внешнее имя p установлено на prefixp. Доступен только $1, и буквальной знак доллара необходимо написать как $$.

Макрос Bycopy

Макрос bycopy может быть применён к типу объекта или кортежа, или параметру процедуры. Он сообщает компилятору передавать тип по значению в процедуры:

type
  Vector {.bycopy.} = object
    x, y, z: float

Компилятор Nim автоматически определяет, передаётся ли параметр по значению или по ссылке, исходя из размера типа параметра. Если параметр должен передаваться по значению или по ссылке (например, при взаимодействии с библиотекой C), используйте макросы bycopy или byref. Обратите внимание, что параметры, помеченные как byref, имеют приоритет над типами, помеченными как bycopy.

Макрос Byref

Макрос byref может быть применён к типу объекта или кортежа, или параметру процедуры. При применении к типу он сообщает компилятору передавать тип по ссылке (скрытый указатель) в процедуры. При применении к параметру он имеет приоритет, даже если тип был помечен как bycopy. Когда тип importc имеет макрос byref или параметры помечены как byref в процедуре importc, эти параметры преобразуются в указатели. Когда тип importcpp имеет макрос byref, эти параметры преобразуются в ссылки C++ &.

{.emit: """/*TYPESECTION*/
typedef struct {
  int x;
} CStruct;
""".}

{.emit: """
#ifdef __cplusplus
extern "C"
#endif
int takesCStruct(CStruct* x) {
  return x->x;
}
""".}

type
  CStruct {.importc, byref.} = object
    x: cint

proc takesCStruct(x: CStruct): cint {.importc.}

или

type
  CStruct {.importc.} = object
    x: cint

proc takesCStruct(x {.byref.}: CStruct): cint {.importc.}
{.emit: """/*TYPESECTION*/
struct CppStruct {
  int x;
  
  int takesCppStruct(CppStruct& y) {
    return x + y.x;
  }
};
""".}

type
  CppStruct {.importcpp, byref.} = object
    x: cint

proc takesCppStruct(x, y: CppStruct): cint {.importcpp.}

Макрос Varargs

Макрос varargs может быть применён только к процедурам (и типам процедур). Он сообщает Nim, что процедура может принимать переменное количество параметров после последнего указанного параметра. Значения строк Nim автоматически преобразуются в строки C.

proc printf(formatstr: cstring) {.header: "<stdio.h>", varargs.}

printf("hallo %s", "world") # "world" will be passed as C string

Макрос Union

Макрос union может быть применён к любому типу object. Это означает, что все поля объекта накладываются в памяти. Это создаёт union вместо struct в сгенерированном C/C++ коде. Объявление объекта затем не должно использовать наследование или память, управляемую сборщиком мусора, но это в настоящее время не проверяется.

Будущие направления: в союзах должна быть разрешена память, управляемая сборщиком мусора, и сборщик мусора должен сканировать союзы консервативно.

Макрос Packed

Макрос packed может быть применён к любому типу object. Он гарантирует, что поля объекта упаковываются друг за другом в памяти. Это полезно для хранения пакетов или сообщений из/в сетевых или аппаратных драйверов и для межплатформенной совместимости с C. Объединение макроса packed с наследованием не определено, и его не следует использовать с памятью, управляемой сборщиком мусора (ref).

Будущие направления: использование памяти, управляемой сборщиком мусора, в макросе packed приведёт к статической ошибке. Использование с наследованием должно быть определено и документировано.

Макрос Dynlib для импорта

С помощью макроса dynlib процедура или переменная могут быть импортированы из динамической библиотеки (файлы .dll для Windows, файлы lib*.so для UNIX). Необязательный аргумент должен быть именем динамической библиотеки:

proc gtk_image_new(): PGtkWidget
  {.cdecl, dynlib: "libgtk-x11-2.0.so", importc.}

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

Механизм импорта dynlib поддерживает схему версионирования:

proc Tcl_Eval(interp: pTcl_Interp, script: cstring): int {.cdecl,
  importc, dynlib: "libtcl(|8.5|8.4|8.3).so.(1|0)".}

Во время выполнения, динамическая библиотека ищется (в данном порядке):

libtcl.so.1
libtcl.so.0
libtcl8.5.so.1
libtcl8.5.so.0
libtcl8.4.so.1
libtcl8.4.so.0
libtcl8.3.so.1
libtcl8.3.so.0

Директива dynlib поддерживает не только константные строки в качестве аргумента, но и строковые выражения в общем случае:

import std/os

proc getDllName: string =
  result = "mylib.dll"
  if fileExists(result): return
  result = "mylib2.dll"
  if fileExists(result): return
  quit("could not load dynamic library")

proc myImport(s: cstring) {.cdecl, importc, dynlib: getDllName().}

Примечание: Шаблоны, подобные libtcl(|8.5|8.4).so, поддерживаются только в константных строках, так как они предварительно компилируются.

Примечание: Передача переменных в директиву dynlib приведёт к ошибке во время выполнения из-за проблем с порядком инициализации.

Примечание: Импорт dynlib может быть переопределён опцией командной строки --dynlibOverride:name. Более подробная информация содержится в Руководстве пользователя компилятора.

Директива Dynlib для экспорта

С помощью директивы dynlib процедура также может быть экспортирована в динамическую библиотеку. Тогда директива не имеет аргумента и должна использоваться совместно с директивой exportc:

proc exportme(): int {.cdecl, exportc, dynlib.}

Это полезно только в том случае, если программа компилируется как динамическая библиотека с помощью опции командной строки --app:lib.

Потоки

Переключатель командной строки --threads:on включён по умолчанию. Модуль typedthreads затем содержит несколько примитивов для работы с потоками. Более подробная информация доступна в разделе spawn.

Единственные способы создания потока — это spawn или createThread.

Директива Thread

Процедура, выполняемая как новый поток, должна быть помечена директивой thread для повышения читабельности. Компилятор проверяет соблюдение ограничения без совместного использования кучи: Это ограничение подразумевает, что некорректно создавать структуру данных, состоящую из памяти, выделенной из разных (локальных для потока) куч.

Процедуру потока можно передать в createThread или spawn.

Директива Threadvar

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

var checkpoints* {.threadvar.}: seq[string]

Из-за ограничений реализации переменные, локальные для потока, не могут быть инициализированы в секции var. (Каждая переменная, локальная для потока, должна быть дублирована при создании потока.)

Потоки и исключения

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

Защитные блоки и блокировки

Nim предоставляет общие механизмы низкоуровневой конкуренции, такие как блокировки, атомарные функции или переменные условия.

Nim существенно улучшает безопасность этих функций с помощью дополнительных директив:

  1. Вводится аннотация guard для предотвращения гонок данных.
  2. Каждый доступ к защищённой области памяти должен происходить в соответствующем операторе locks.

Секции защитных блоков и блокировок

Защита глобальных переменных

Поля объектов и глобальные переменные могут быть помечены с помощью директивы guard:

import std/locks

var glock: Lock
var gdata {.guard: glock.}: int

Затем компилятор гарантирует, что каждый доступ к gdata происходит внутри секции locks:

proc invalid =
  # invalid: unguarded access:
  echo gdata

proc valid =
  # valid access:
  {.locks: [glock].}:
    echo gdata

Доступы к gdata на верхнем уровне всегда разрешены, чтобы их можно было удобно инициализировать. Предполагается (но не проверяется), что каждый оператор на верхнем уровне выполняется до начала любой одновременной операции.

Секция locks намеренно выглядит некрасиво, потому что у неё нет семантики во время выполнения и её не следует использовать напрямую! Она должна использоваться только в шаблонах, которые также реализуют некую форму блокировки во время выполнения:

template lock(a: Lock; body: untyped) =
  pthread_mutex_lock(a)
  {.locks: [a].}:
    try:
      body
    finally:
      pthread_mutex_unlock(a)

Защитный блок не должен быть какого-либо определённого типа. Он достаточно гибкий, чтобы моделировать механизмы низкоуровневых блокировок без ожидания:

var dummyLock {.compileTime.}: int
var atomicCounter {.guard: dummyLock.}: int

template atomicRead(x): untyped =
  {.locks: [dummyLock].}:
    memoryReadBarrier()
    x

echo atomicRead(atomicCounter)

Директива locks принимает список выражений блокировки locks: [a, b, ...], чтобы поддержать операторы многократной блокировки.

Защита общих областей

Аннотация guard также может использоваться для защиты полей в объекте. Защитный блок затем должен быть другим полем в том же объекте или глобальной переменной.

Поскольку объекты могут находиться в куче или на стеке, это значительно повышает выразительность языка:

import std/locks

type
  ProtectedCounter = object
    v {.guard: L.}: int
    L: Lock

proc incCounters(counters: var openArray[ProtectedCounter]) =
  for i in 0..counters.high:
    lock counters[i].L:
      inc counters[i].v

Доступ к полю x.v разрешён, так как его защитный блок x.L активен. После расширения шаблона это равносильно:

proc incCounters(counters: var openArray[ProtectedCounter]) =
  for i in 0..counters.high:
    pthread_mutex_lock(counters[i].L)
    {.locks: [counters[i].L].}:
      try:
        inc counters[i].v
      finally:
        pthread_mutex_unlock(counters[i].L)

Существует анализ, проверяющий, что counters[i].L — это блокировка, соответствующая защищённой области counters[i].v. Этот анализ называется анализом траекторий, потому что он обрабатывает пути к таким областям, как obj.field[i].fieldB[j].

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

Это означает, что следующее компилируется (пока), хотя на самом деле не должно:

{.locks: [a[i].L].}:
  inc i
  access a[i].v

© 2006–2024 Andreas Rumpf
Licensed under the MIT License.
https://nim-lang.org/docs/manual.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API