Spec-Zone.ru › Nim

Экспериментальные возможности Nim

Исходный кодРедактировать

О документе

В данном документе описаны экспериментальные возможности Nim. Некоторые из них не охвачены пragma .experimental или переключателем --experimental, поскольку они уже реализованы с использованием специального синтаксиса, и пользователи библиотек Nim могут использовать эти возможности без их непосредственного применения.

Примечание: Если не указано иное, эти возможности не будут удалены, а будут доработаны и переработаны.

Тип void

Тип void обозначает отсутствие значения, т.е. это тип, не содержащий значений. Следовательно, для параметров типа void нельзя указать значение, и функция с типом возвращаемого значения void не может вернуть значение:

proc nothing(x, y: void): void =
  echo "ha"

nothing() # writes "ha" to stdout

Тип void особенно полезен для обобщённого кода:

proc callProc[T](p: proc (x: T), x: T) =
  when T is void:
    p()
  else:
    p(x)

proc intProc(x: int) = discard
proc emptyProc() = discard

callProc[int](intProc, 12)
callProc[void](emptyProc)

Однако, тип void не может быть выведен в обобщённом коде:

callProc(emptyProc)
# Error: type mismatch: got (proc ())
# but expected one of:
# callProc(p: proc (T), x: T)

Тип void допустим только для параметров и типов возвращаемых значений; другие символы не могут иметь тип void.

Обобщённый пragma define

Помимо typed define pragmas для констант, существует обобщённый пragma {.define.}, который интерпретирует значение define в зависимости от типа константного значения.

const foo {.define: "package.foo".} = 123
const bar {.define: "package.bar".} = false
nim c -d:package.foo=456 -d:package.bar foobar.nim

Поддерживаются следующие типы:

  • string и cstring
  • Целочисленные типы со знаком и без знака
  • bool
  • Перечисления

Выведение типов сверху вниз

В выражениях, таких как:

let a: T = ex

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

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

let foo: (float, uint8, cstring) = (1, 2, "abc")

Выражение кортежа имеет ожидаемый тип (float, uint8, cstring). Поскольку это литерал кортежа, мы можем использовать эту информацию для предположения типов его элементов. Ожидаемые типы для выражений 1, 2 и "abc" соответственно float, uint8 и cstring; и эти выражения могут быть статически преобразованы в эти типы.

Без этой информации тип выражения кортежа предполагался бы (int, int, string). Таким образом, тип выражения кортежа не соответствовал бы типу переменной, и было бы выдано сообщение об ошибке.

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

Выведенные обобщённые параметры

В выражениях, использующих обобщённые процедуры или шаблоны, ожидаемые (не связанные) типы часто могут быть выведены на основе контекста. Эта функция должна быть включена с помощью {.experimental: "inferGenericTypes".}

{.experimental: "inferGenericTypes".}

import std/options

var x = newSeq[int](1)
# Do some work on 'x'...

# Works!
# 'x' is 'seq[int]' so 'newSeq[int]' is implied
x = newSeq(10)

# Works!
# 'T' of 'none' is bound to the 'T' of 'noneProducer', passing it along.
# Effectively 'none.T = noneProducer.T'
proc noneProducer[T](): Option[T] = none()
let myNone = noneProducer[int]()

# Also works
# 'myOtherNone' binds its 'T' to 'float' and 'noneProducer' inherits it
# noneProducer.T = myOtherNone.T
let myOtherNone: Option[float] = noneProducer()

# Works as well
# none.T = myOtherOtherNone.T
let myOtherOtherNone: Option[int] = none()

Это достигается за счёт уменьшения типов в левой и правой частях, пока в левой части не останутся только типы, такие как T. Хотя левая и правая части уменьшаются вместе, это не означает, что правая часть также останется только с плоским типом Z, она может иметь вид MyType[Z].

После уменьшения типов типы T связываются с типами, оставшимися в правой части.

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

Пример неудачного вывода можно найти при передаче обобщённого выражения в вызов функции/шаблона:

{.experimental: "inferGenericTypes".}

proc myProc[T](a, b: T) = discard

# Fails! Unable to infer that 'T' is supposed to be 'int'
myProc(newSeq[int](), newSeq(1))

# Works! Manual specification of 'T' as 'int' necessary
myProc(newSeq[int](), newSeq[int](1))

Сочетание обобщённого вывода с типом auto также не поддерживается:

{.experimental: "inferGenericTypes".}

proc produceValue[T]: auto = default(T)
let a: int = produceValue() # 'auto' cannot be inferred here

Примечание: Описанный вывод не позволяет создавать переопределения на основе типа возвращаемого значения процедуры. Это механизм сопоставления, который не пытается выполнить более глубокий вывод, и не изменяет то, что является допустимым переопределением.

# Doesn't affect the following code, it is invalid either way
{.experimental: "inferGenericTypes".}

proc a: int = 0
proc a: float = 1.0 # Fails! Invalid code and not recommended

Литералы последовательностей

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

let x: seq[seq[float]] = @[@[1, 2, 3], @[4, 5, 6]]

Это поведение связано с перегрузками @ в модуле system, поэтому перегрузка @ может отключить это поведение. Это можно обойти, указав перегрузку `` system.@ ``.

proc `@`(x: string): string = "@" & x

# does not compile:
let x: seq[float] = @[1, 2, 3]
# compiles:
let x: seq[float] = system.`@`([1, 2, 3])

Объекты на уровне пакета

Каждый модуль Nim находится в пакете (nimble). Тип объекта может быть прикреплен к пакету, в котором он находится. В этом случае к типу можно получить доступ из других модулей как к типу незавершенного объекта. Эта функция позволяет разделить рекурсивные зависимости типов между границами модулей. Незавершенные типы объектов всегда передаются byref и могут использоваться только в контекстах указателей (var/ref/ptr IncompleteObject) в общем случае, так как компилятор ещё не знает размер объекта. Для завершения незавершенного объекта необходимо использовать пragma package. package подразумевает byref.

Пока тип T является незавершенным, никакая информация о времени выполнения для T недоступна.

Пример:

# module A (in an arbitrary package)
type
  Pack.SomeObject = object # declare as incomplete object of package 'Pack'
  Triple = object
    a, b, c: ref SomeObject # pointers to incomplete objects are allowed

# Incomplete objects can be used as parameters:
proc myproc(x: SomeObject) = discard
# module B (in package "Pack")
type
  SomeObject* {.package.} = object # Use 'package' to complete the object
    s, t: string
    x, y: int

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

Импорт частных символов

В некоторых ситуациях может быть полезно импортировать все символы (публичные или частные) из модуля. Синтаксис import foo {.all.} может использоваться для импорта всех символов из модуля foo. Обратите внимание, что импорт частных символов, как правило, не рекомендуется.

См. также экспериментальный модуль importutils.

Переупорядочение кода

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

Пример:

{.experimental: "codeReordering".}

proc foo(x: int) =
  bar(x)

proc bar(x: int) =
  echo(x)

foo(10)

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

{.experimental: "codeReordering".}

proc a() =
  echo(foo)

var foo = 5

a() # outputs: "5"

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

{.experimental: "codeReordering".}

proc a() =
  b()
  proc b() =
    echo("Hello!")

a()

Эта функция, вероятно, будет заменена на лучшее решение, которое устранит необходимость в объявлениях вперёд.

Специальные операторы

Операторы точки

Примечание: Операторы точки всё ещё находятся на стадии эксперимента, поэтому их необходимо включить с помощью {.experimental: "dotOperators".}.

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

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

a.b # becomes `.`(a, b)
a.b(c, d) # becomes `.`(a, b, c, d)

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

template `.`(js: PJsonNode, field: untyped): JSON = js[astToStr(field)]

var js = parseJson("{ x: 1, y: 2}")
echo js.x # outputs 1
echo js.y # outputs 2

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

Оператор .

Этот оператор будет сопоставляться как с доступом к полям, так и с вызовами методов.

Оператор .()

Этот оператор будет сопоставляться исключительно с вызовами методов. Он имеет более высокий приоритет, чем оператор . и это позволяет по-разному обрабатывать выражения, такие как x.y и x.y(), например, при взаимодействии со скриптовым языком.

Оператор .=

Этот оператор будет сопоставляться с присваиванием отсутствующим полям.

a.b = c # becomes `.=`(a, b, c)

Оператор вызова

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

{.experimental: "callOperator".}

template `()`(a: int, b: float): untyped = $(a, b)

block:
  let a = 1.0
  let b = 2
  doAssert b(a) == `()`(b, a)
  doAssert a.b == `()`(b, a)

block:
  let a = 1.0
  proc b(): int = 2
  doAssert not compiles(b(a))
  doAssert not compiles(a.b) # `()` not called

block:
  let a = 1.0
  proc b(x: float): int = int(x + 1)
  let c = 3.0
  
  doAssert not compiles(a.b(c)) # gives a type mismatch error same as b(a, c)
  doAssert (a.b)(c) == `()`(a.b, c)

Расширенные макросные pragmas

Макросные pragmas, как описано в руководстве, также могут применяться к объявлениям типов, переменных и констант.

Для типов:

type
  MyObject {.schema: "schema.protobuf".} = object

Это преобразуется в вызов макроса schema с узлом AST nnkTypeDef , захватывающим левую часть, оставшиеся pragmas и правую часть определения. Макрос может вернуть либо секцию типа, либо другой узел nnkTypeDef , оба из которых заменят исходную строку в секции типа.

В будущем этот аргумент nnkTypeDef может быть заменён на унарный узел секции типа, содержащий определение типа, или какой-либо другой узел, который может быть более удобным для работы. Возможно также будет поддерживаться возможность возвращать узлы, отличные от определений типов, однако в настоящее время это неудобно при работе с взаимной рекурсией типов. Пока макросы могут возвращать незадействованное определение типа, где узел справа имеет тип nnkStmtListType. Объявления в этом узле будут присоединены к той же области видимости, что и родительская область видимости секции типа.


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

var
  a = ...
  b {.importc, foo, nodecl.} = ...
  c = ...

Предполагая, что foo является макросом или шаблоном, это примерно эквивалентно:

var a = ...
foo:
  var b {.importc, nodecl.} = ...
var c = ...

Символы как вызовы шаблонов/макросов (синтаксис псевдонима)

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

type Foo = object
  bar: int

var foo = Foo(bar: 10)
template bar: int = foo.bar
assert bar == 10
bar = 15
assert bar == 15

Аннотация «не nil»

Примечание: Это экспериментальная функция. Её можно включить с помощью {.experimental: "notnil".}.

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

{.experimental: "notnil".}

type
  TObj = object
  PObject = ref TObj not nil
  TProc = (proc (x, y: int)) not nil

proc p(x: PObject) =
  echo "not nil"

# compiler catches this:
p(nil)

# and also this:
proc foo =
  var x: PObject
  p(x)

foo()

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

Строгая проверка на nil

Примечание: Эта функция находится в стадии разработки. Вам необходимо включить её с помощью

{.experimental: "strictNotNil".}

или

nim c --experimental:strictNotNil <program>

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

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

Её реализация отличается от реализации notnil, которая определена в strictNotNil. Имейте в виду разницу в именах опций, будьте внимательны при их различении.

Мы проверяем возможность наличия nil для нескольких типов:

  • типов ссылок
  • типов указателей
  • типов процедур
  • c-строк

nil

По умолчанию типы, допускающие nil, имеют тип nil: они могут иметь значение nil. Если у вас есть тип, не допускающий nil, T, вы можете использовать T nil для получения типа, допускающего nil.

не nil

Вы можете снабдить аннотацией тип, для которого nil недопустимо, значением not nil.

type
    NilableObject = ref object
      a: int
    Object = NilableObject not nil
    
    Proc = (proc (x, y: int))
  
  proc p(x: Object) =
    echo x.a # ensured to dereference without an error
  # compiler catches this:
  p(nil)
  # and also this:
  var x: NilableObject
  if x.isNil:
    p(x)
  else:
    p(x) # ok

Если тип может включать nil в качестве допустимого значения, обращение к значениям этого типа проверяется компилятором: если значение, которое может быть nil, обращается к нему, это по умолчанию вызывает предупреждение; вы можете сделать это ошибкой, используя параметры компилятора --warningAsError:strictNotNil.

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

Включение/выключение проверки nil на уровне функций/модулей

Вы по-прежнему можете отключить проверку nil на уровне функции/модуля, используя директиву {.strictNotNil: off.}.

Состояние nil

В настоящее время допустимые состояния для значения, допускающего nil, могут быть Safe, MaybeNil или Nil : мы используем внутренне Parent и Unreachable, но это реализующий деталь (родительский уровень имеет фактическое состояние nil).

  • Safe означает, что в данный момент значение не должно быть nil: например, после присваивания не-nil значения или проверки not a.isNil
  • MaybeNil означает, что значение может быть nil, но также может и не быть: например, аргумент, аргумент вызова или значение после проверки if и else.
  • Nil означает, что значение должно быть nil в данный момент; например, после присваивания nil или проверки .isNil.
  • Unreachable означает, что доступ к этому значению в данном ветвлении невозможен: поэтому мы также генерируем предупреждение.

Мы выводим ошибку для каждого обращения к значению ([], .field, [index] () и т.д.), которое относится к отслеживаемому выражению, находящемуся в состоянии MaybeNil или Nil.

Состояние nil типа

Типы могут быть либо допускающими nil, либо не допускающими nil. При передаче параметра или значения по умолчанию мы используем тип: для типов, допускающих nil, мы возвращаем MaybeNil, а для типов, не допускающих nil, Safe.

Правила для параметров

Состояние nil параметра определяется на основе состояния nil типа. Мы используем тип аргумента для определения состояния nil.

Правила для присваивания

Предположим, у нас есть left = right.

При присваивании мы передаём состояние nil правой части в выражение левой. Должны быть особые правила обработки алиасов и сложных выражений, которые мы укажем в соответствующих разделах. (Присваивание является возможным алиасом move или move out).

Правила для аргументов вызова

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

callByVar(a)

Здесь callByVar может повторно присвоить a, поэтому это может изменить состояние nil a, поэтому мы изменяем его на MaybeNil. Это также возможный алиас move out (перемещение за пределы текущего набора алиасов).

call(a)

Здесь call может изменить поле или элемент a, поэтому, если у нас есть зависимое выражение a, например, a.field. Зависимые выражения становятся MaybeNil.

Правила для ветвлений

Ветвления — причина, по которой мы делаем проверку на nil таким образом: с помощью проверки потока. Источники ветвления — это if, while, for, and, or, case, try и комбинации с return, break, continue и raise

Для каждой ветви мы создаём новый уровень/«область», где мы отображаем выражения в соответствии с состоянием nil. Это происходит при «разветвлении»: обычно в начале конструкции. Когда ветви «соединяются», мы обычно объединяем их карты выражений и/или состояния nil.

Объединение обычно объединяет карты и наборы алиасов: состояния nil объединяются следующим образом:

template union(l: Nilability, r: Nilability): Nilability =
  ## unify two states
  if l == r:
    l
  else:
    MaybeNil

Особое внимание уделяется .isNil и == nil, а также not, and и or.

not инвертирует состояние nil, and аналогично «разветвлению»: выражение справа проверяется на уровне, полученном из выражения слева, а or аналогично «объединению»: выражения справа и слева должны быть проверены на исходном уровне.

isNil, == nil делают выражения Nil. Если есть not или != nil, они делают их Safe. Мы также инвертируем состояние nil в противоположной ветви: например, else.

Сложные выражения: выражения поля и индекса

Мы хотим отслеживать также выражения поля (точка) и индекса (квадратные скобки).

Мы отслеживаем некоторые из этих сложных выражений, которые могут быть допускающими nil, как зависимости их основ: a.field изменяется, если a перемещается (переприсваивается), аналогично a[index] зависит от a и a.field.field от a.field.

При перемещении базы мы обновляем зависимые выражения на MaybeNil. В противном случае мы обычно начинаем с состояния nil типа.

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

Для выражений поля a.field, мы вычисляем целое значение на основе хэша дерева и просто принимаем эквивалентные деревья как эквивалентные выражения.

Для выражений элемента a[index], мы также вычисляем целое значение на основе хэша дерева и принимаем эквивалентные деревья как эквивалентные выражения: только для статических значений. На данный момент мы поддерживаем только постоянные индексы: мы не отслеживаем выражения с индексами, не являющимися константами. Для них мы просто сообщаем предупреждение, даже если они сейчас безопасны: можно использовать локальную переменную для обхода. Для циклов это может быть раздражающим, поэтому можно локально отключить предупреждение, используя {.warning[StrictNotNil]:off.}.

Для выражений в квадратных скобках в будущем мы можем считать a[<any>] одинаковыми общими выражениями. Это означает, что мы должны учесть индекс, но в остальном обрабатывать его так же, как при присваивании (возможно, «алиасировать» все нестатистические элементы) и различать только для статических: например, a[0] и a[1].

Отслеживание элементов

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

var a = Nilable(field: Nilable()) # a : Safe, a.field: Safe

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

Правила для неструктурированного управления потоком

Ключевые слова неструктурированного управления потоком, такие как return, break, continue, raise означают, что мы переходим из ветвления наружу. Это означает, что если есть код после завершения ветвления, он будет выполнен, если не была достигнута непосредственная родительская ветвь этих операторов: поэтому это похоже на else. В таких случаях для локальных выражений условия мы должны использовать обратные состояния nil. Например:

for a in c:
  if not a.isNil:
    b()
    break
  code # here a: Nil , because if not, we would have breaked

Алиасирование

Мы поддерживаем обнаружение алиасов для локальных выражений.

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

move: Перемещение left в right означает, что мы удаляем left из его текущего набора и объединяем его с набором right Это означает, что он больше не алиасируется со своими предыдущими алиасами.

var left = b
left = right # moving left to right

move out: Перемещение left может удалить его из текущего набора и гарантировать, что он находится в собственном наборе как отдельный элемент. Например:

var left = b
left = nil # moving out

Предупреждения и ошибки

Мы выводим ошибку для каждого обращения к значению ([], .field, [index] () и т.д.), которое относится к отслеживаемому выражению, находящемуся в состоянии MaybeNil или Nil.

Мы также можем показать историю переходов и причин, которые могут изменить состояние nil выражения.

Ограничения на алиасинг при передаче параметров

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

"Алиасинг" здесь означает, что в процессе выполнения программы подлежащие хранилища в памяти перекрываются. "Параметр вывода" — это параметр типа var T, а параметр ввода — любой параметр, который не является типа var.

  1. Два параметра вывода не должны быть алиасами.
  2. Параметр ввода и параметр вывода не должны быть алиасами.
  3. Параметр вывода никогда не должен быть алиасом глобальной или локальной по потоку переменной, к которой обращается вызываемый процедурой.
  4. Параметр ввода не должен быть алиасом глобальной или локальной по потоку переменной, обновляемой вызываемой процедурой.

Одна из проблем правил 3 и 4 заключается в том, что они влияют на конкретные глобальные или локальные по потоку переменные, но отслеживание эффектов в Nim отслеживает только «не использует глобальные переменные» через .noSideEffect. Правила 3 и 4 также могут быть приближены другим правилом:

  1. Глобальная или локальная по потоку переменная (или местоположение, полученное из такого местоположения) может быть передана только в параметр процедуры .noSideEffect.

Строгие функции

С версии 1.4 доступно более строгое определение «побочного эффекта». В дополнение к существующему правилу, что побочный эффект — это вызов функции с побочными эффектами, также применяется следующее правило:

Запись в кучу через ref или ptr косвенные указания не допускается.

Например:

{.experimental: "strictFuncs".}

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

func len(n: Node): int =
  # valid: len does not have side effects
  var it = n
  while it != nil:
    inc result
    it = it.ri

func mut(n: Node) =
  var it = n
  while it != nil:
    it.data = "yeah" # forbidden mutation
    it = it.ri

Типы представлений

Подсказка: --experimental:views более эффективен с --experimental:strictFuncs.

Тип представления — это тип, который содержит один из следующих типов:

  • lent T (представление в T)
  • openArray[T] (пара из (указатель на массив из T, размер))

Например:

type
  View1 = openArray[byte]
  View2 = lent string
  View3 = Table[openArray[char], int]

Исключениями из этого правила являются типы, созданные с помощью ptr или proc. Например, следующие типы не являются типами представлений:

type
  NotView1 = proc (x: openArray[int])
  NotView2 = ptr openArray[char]
  NotView3 = ptr array[4, lent int]

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

Представление — это символ (let, var, const и т. д.), имеющий тип представления.

С версии 1.4 Nim позволяет использовать типы представлений в качестве локальных переменных. Эта функция должна быть включена через {.experimental: "views".}.

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

Например:

{.experimental: "views".}

proc take(a: openArray[int]) =
  echo a.len

proc main(s: seq[int]) =
  var x: openArray[int] = s # 'x' is a view into 's'
  # it is checked that 'x' does not outlive 's' and
  # that 's' is not mutated.
  for i in 0 .. high(x):
    echo x[i]
  take(x)
  
  take(x.toOpenArray(0, 1)) # slicing remains possible
  let y = x  # create a view from a view
  take y
  # it is checked that 'y' does not outlive 'x' and
  # that 'x' is not mutated as long as 'y' lives.


main(@[11, 22, 33])

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

Пусть p — процедура, которая анализируется на корректность операции заимствования.

Пусть source — это:

  • Формальный параметр p. Обратите внимание, что это не включает параметры внутренних процедур.
  • Символ result процедуры p.
  • Локальный var или let или const процедуры p. Обратите внимание, что это не включает локальные переменные внутренних процедур.
  • Локальная по потоку var или let.
  • Глобальный let или const.
  • Конструктор массива/последовательности/объекта/кортежа констант.

Выражения путей

Местоположение, полученное из source определяется как выражение пути, у которого source является владельцем. Выражение пути e определяется рекурсивно:

  • source само по себе является выражением пути.
  • Доступ к контейнерам, например, e[i] является выражением пути.
  • Доступ к кортежу e[0] является выражением пути.
  • Доступ к полю объекта e.field является выражением пути.
  • system.toOpenArray(e, ...) является выражением пути.
  • Разыменование указателя e[] является выражением пути.
  • Адрес addr e является выражением пути.
  • Преобразование типа T(e) является выражением пути.
  • Выражение приведения типа cast[T](e) является выражением пути.
  • f(e, ...) является выражением пути, если тип возврата f является типом представления. Поскольку представление может быть заимствовано только из e, мы знаем, что владелец f(e, ...) равен e.

Если тип представления используется в качестве типа возврата, местоположение должно быть заимствовано из местоположения, полученного из первого параметра, передаваемого процедуре. См. справочник для получения подробностей о том, как это делается для var T.

Изменяемое представление может быть заимствовано из изменяемого местоположения, неизменяемое представление может быть заимствовано из изменяемого или неизменяемого местоположения.

Если представление заимствует из изменяемого местоположения, представление может использоваться для обновления местоположения. В противном случае оно не может использоваться для изменений.

Продолжительность заимствования — это период команд, начинающийся с присваивания представлению и заканчивающийся последним использованием представления.

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

{.experimental: "views".}

type
  Obj = object
    field: string

proc dangerous(s: var seq[Obj]) =
  let v: lent Obj = s[0] # seal 's'
  s.setLen 0  # prevented at compile-time because 's' is sealed.
  echo v.field

Область действия представления не имеет значения:

proc valid(s: var seq[Obj]) =
  let v: lent Obj = s[0]  # begin of borrow
  echo v.field            # end of borrow
  s.setLen 0  # valid because 'v' isn't used afterwards

Анализ требует такой же точности в отношении изменений, как это возможно получить, поэтому он более эффективен с экспериментальной функцией строгих функций. Другими словами, --experimental:views работает лучше с --experimental:strictFuncs.

Анализ в настоящее время нечувствителен к потоку управления:

proc invalid(s: var seq[Obj]) =
  let v: lent Obj = s[0]
  if false:
    s.setLen 0
  echo v.field

В этом примере компилятор предполагает, что s.setLen 0 делает операцию заимствования v недействительной, хотя человек легко может увидеть, что это никогда не произойдет во время выполнения.

Начало заимствования

Заимствование начинается с одного из следующих действий:

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

Окончание заимствования

Операция заимствования заканчивается последним использованием переменной представления.

Повторные заимствования

Представление v может быть заимствовано из нескольких различных местоположений. Однако заимствование всегда охватывает весь период времени жизни v и каждое заимствованное местоположение запечатывается в течение времени жизни v.

Алгоритм

В этом разделе представлен обзор алгоритма, который используется в текущей реализации. Алгоритм выполняет два обхода по AST процедуры или глобального раздела кода, использующего переменную представления. Итерации по фиксированной точке не выполняются, сложность анализа составляет O(N), где N — количество узлов AST.

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

Во втором проходе вычисляется информация об «объектных графах». Пусть v — параметр или локальная переменная. Пусть G(v) — граф, к которому относится v. Граф определяется набором переменных, относящихся к графу. Изначально для всех v: G(v) = {v}.

Каждая переменная может принадлежать только одному графу.

Присваивания, такие как a = b «соединяют» две переменные, обе переменные попадают в один и тот же граф {a, b} = G(a) = G(b). К сожалению, образец для поиска намного сложнее и может включать несколько целей и источников присваивания:

f(x, y) = g(a, b)

соединяет x и y с a и b: G(x) = G(y) = G(a) = G(b) = {x, y, a, b}. Анализ алиасов на основе типов исключает некоторые из этих комбинаций, например, значение string не может быть соединено со значением seq[int].

Шаблон, подобный v[] = value или v.field = value помечает G(v) как измененный. После второго прохода был вычислен набор непересекающихся графов.

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

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

  • Время жизни v не должно превышать время жизни b.
  • Если v используется для изменения, b тоже должно быть изменяемым местоположением.
  • В течение времени жизни v G(b) может быть изменено только v (и только если v — изменяемое представление).
  • Если v является result, то b должно быть местоположением, полученным из первого формального параметра или из постоянного местоположения.
  • Представление не может использоваться для чтения или записи до того, как ему было присвоено значение.

Концепции

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

Концепции записываются в следующем формате:

type
  Comparable = concept x, y
    (x < y) is bool
  
  Stack[T] = concept s, var v
    s.pop() is T
    v.push(T)
    
    s.len is Ordinal
    
    for value in s:
      value is T

Концепция совпадает, если:

  1. все выражения внутри тела могут быть скомпилированы для тестируемого типа
  2. все статически вычисляемые булевы выражения в теле истинны
  3. все указанные модификаторы типов соответствуют их определениям

Идентификаторы, следующие за ключевым словом concept, представляют собой экземпляры текущего сопоставленного типа. Вы можете применить любые стандартные модификаторы типов, такие как var, ref, ptr и static, чтобы указать более конкретный тип экземпляра. Вы также можете применить модификатор type для создания именованного экземпляра самого типа:

type
  MyConcept = concept x, var v, ref r, ptr p, static s, type T
    ...

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

type
  OutputStream = concept var s
    s.write(string)

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

type
  # Let's imagine a user-defined casting framework with operators
  # such as `val.to(string)` and `val.to(JSonValue)`. We can test
  # for these with the following concept:
  MyCastables = concept x
    x.to(type string)
    x.to(type JSonValue)
  
  # Let's define a couple of concepts, known from Algebra:
  AdditiveMonoid* = concept x, y, type T
    x + y is T
    T.zero is T # require a proc such as `int.zero` or 'Position.zero'
  
  AdditiveGroup* = concept x, y, type T
    x is AdditiveMonoid
    -x is T
    x - y is T

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

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

Диагностика концепций

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

type
  MyConcept {.explain.} = concept ...

overloadedProc(x, y, z) {.explain.}

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

Обобщённые концепции и правила привязки типов

Типы концепций могут быть параметрическими, как и обычные дженерик-типы:

### matrixalgo.nim

import std/typetraits

type
  AnyMatrix*[R, C: static int; T] = concept m, var mvar, type M
    M.ValueType is T
    M.Rows == R
    M.Cols == C
    
    m[int, int] is T
    mvar[int, int] = T
    
    type TransposedType = stripGenericParams(M)[C, R, T]
  
  AnySquareMatrix*[N: static int, T] = AnyMatrix[N, N, T]
  
  AnyTransform3D* = AnyMatrix[4, 4, float]

proc transposed*(m: AnyMatrix): m.TransposedType =
  for r in 0 ..< m.R:
    for c in 0 ..< m.C:
      result[r, c] = m[c, r]

proc determinant*(m: AnySquareMatrix): int =
  ...

proc setPerspectiveProjection*(m: AnyTransform3D) =
  ...

--------------
### matrix.nim

type
  Matrix*[M, N: static int; T] = object
    data: array[M*N, T]

proc `[]`*(M: Matrix; m, n: int): M.T =
  M.data[m * M.N + n]

proc `[]=`*(M: var Matrix; m, n: int; v: M.T) =
  M.data[m * M.N + n] = v

# Adapt the Matrix type to the concept's requirements
template Rows*(M: typedesc[Matrix]): int = M.M
template Cols*(M: typedesc[Matrix]): int = M.N
template ValueType*(M: typedesc[Matrix]): typedesc = M.T

-------------
### usage.nim

import matrix, matrixalgo

var
  m: Matrix[3, 3, int]
  projectionMatrix: Matrix[4, 4, float]

echo m.transposed.determinant
setPerspectiveProjection projectionMatrix

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

Неопределённые типы могут появляться как параметры вызовов, например s.push(T), и справа от оператора is в случаях, таких как x.pop is T и x.data is seq[T].

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

type
  MatrixReducer[M, N: static int; T] = concept x
    x.reduce(SquareMatrix[N, T]) is array[M, int]

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

Как и в обычных типах классов, Nim различает bind once и bind many типы при сопоставлении с концепцией. Можно добавить distinct модификатор к любому из типов, которые можно вывести, чтобы получить тип, который будет сопоставлен без постоянного вывода. Это может быть полезно, когда нужно сопоставить несколько процедур, принимающих один и тот же широкий класс типов:

type
  Enumerable[T] = concept e
    for v in e:
      v is T

type
  MyConcept = concept o
    # this could be inferred to a type such as Enumerable[int]
    o.foo is distinct Enumerable
    
    # this could be inferred to a different type such as Enumerable[float]
    o.bar is distinct Enumerable
    
    # it's also possible to give an alias name to a `bind many` type class
    type Enum = distinct Enumerable
    o.baz is Enum

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

type
  MyConcept = concept x
    type T1 = auto
    x.foo(T1)
    x.bar(T1) # both procs must accept the same type
    
    type T2 = seq[SomeNumber]
    x.alpha(T2)
    x.omega(T2) # both procs must accept the same type
                # and it must be a numeric sequence

Как видно из предыдущих примеров, можно ссылаться на общие концепции, такие как Enumerable[T], просто по их короткому имени. Подобно обычным дженерик-типам, концепция будет автоматически инстанцирована с привязкой, как только тип «авто» появится на месте каждого отсутствующего параметра дженерика.

Обратите внимание, что общие концепции, такие как Enumerable[T], могут быть сопоставлены с конкретными типами, такими как string. Nim не требует, чтобы тип концепции имел такое же количество параметров, как тип, с которым производится сопоставление. Если вы хотите выразить требование к дженерик-параметрам сопоставляемого типа, можно использовать оператор сопоставления типов, такой как genericHead или stripGenericParams, внутри тела концепции, чтобы получить неинициализированную версию типа, которую затем можно попытаться инициализировать любым необходимым образом. Например, вот как можно определить классическую Functor концепцию из Haskell, а затем продемонстрировать, что тип Option[T] Nim является её экземпляром:

import std/[sugar, typetraits]

type
  Functor[A] = concept f
    type MatchedGenericType = genericHead(typeof(f))
      # `f` will be a value of a type such as `Option[T]`
      # `MatchedGenericType` will become the `Option` type
    
    f.val is A
      # The Functor should provide a way to obtain
      # a value stored inside it
    
    type T = auto
    map(f, A -> T) is MatchedGenericType[T]
      # And it should provide a way to map one instance of
      # the Functor to a instance of a different type, given
      # a suitable `map` operation for the enclosed values

import std/options
echo Option[int] is Functor # prints true

Производные значения концепций

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

type
  DateTime = concept t1, t2, type T
    const Min = T.MinDate
    T.Now is T
    
    t1 < t2 is bool
    
    type TimeSpan = typeof(t1 - t2)
    TimeSpan * int is TimeSpan
    TimeSpan + TimeSpan is TimeSpan
    
    t1 + TimeSpan is T

proc eventsJitter(events: Enumerable[DateTime]): float =
  var
    # this variable will have the inferred TimeSpan type for
    # the concrete Date-like value the proc was called with:
    averageInterval: DateTime.TimeSpan
    
    deviation: float
  ...

Уточнение концепций

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

type
  Graph = concept g, type G of EquallyComparable, Copyable
    type
      VertexType = G.VertexType
      EdgeType = G.EdgeType
    
    VertexType is Copyable
    EdgeType is Copyable
    
    var
      v: VertexType
      e: EdgeType
  
  IncidendeGraph = concept of Graph
    # symbols such as variables and types from the refined
    # concept are automatically in scope:
    
    g.source(e) is VertexType
    g.target(e) is VertexType
    
    g.outgoingEdges(v) is Enumerable[EdgeType]
  
  BidirectionalGraph = concept g, type G
    # The following will also turn the concept into a refinement when it
    # comes to overload resolution, but it doesn't provide the convenient
    # symbol inheritance
    g is IncidendeGraph
    
    g.incomingEdges(G.VertexType) is Enumerable[G.EdgeType]

proc f(g: IncidendeGraph)
proc f(g: BidirectionalGraph) # this one will be preferred if we pass a type
                              # matching the BidirectionalGraph concept

Динамические аргументы для bindSym

Эта экспериментальная функция позволяет динамически вычислять аргумент имени символа в macros.bindSym.

{.experimental: "dynamicBindSym".}

import std/macros

macro callOp(opName, arg1, arg2): untyped =
  result = newCall(bindSym($opName), arg1, arg2)

echo callOp("+", 1, 2)
echo callOp("-", 5, 4)

Макросы переписывания терминов

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

template optMul{`*`(a, 2)}(a: int): int = a + a

let x = 3
echo x * 2

Компилятор теперь переписывает x * 2 как x + x. Код внутри фигурных скобок — это шаблон для сопоставления. Операторы *, **, |, ~ имеют специальное значение в шаблонах, если они написаны в инфиксной форме, поэтому для соответствия буквально * необходимо использовать обычный синтаксис вызова функции.

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

К сожалению, оптимизации сложно реализовать правильно, и даже этот небольшой пример неверен:

template optMul{`*`(a, 2)}(a: int): int = a + a

proc f(): int =
  echo "side effect!"
  result = 55

echo f() * 2

Мы не можем дублировать 'a', если он обозначает выражение, имеющее побочный эффект! К счастью, Nim поддерживает анализ побочных эффектов:

template optMul{`*`(a, 2)}(a: int{noSideEffect}): int = a + a

proc f(): int =
  echo "side effect!"
  result = 55

echo f() * 2 # not optimized ;-)

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

Так что же насчёт 2 * a? Мы должны сказать компилятору, что * является коммутативным. Однако мы не можем этого сделать, так как следующий код просто меняет аргументы:

template mulIsCommutative{`*`(a, b)}(a, b: int): int = b * a

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

template canonMul{`*`(a, b)}(a: int{lit}, b: int): int = b * a

Параметр int{lit} шаблона соответствует выражению типа int, но только если это литерал.

Ограничения параметров

Выражение ограничения параметров может использовать операторы | (или), & (и) и ~ (не) и следующие предикаты:

Предикат Значение
atom Узел сопоставления не имеет дочерних элементов.
lit Узел сопоставления — литерал, например "abc", 12.
sym Сопоставляемый узел должен быть символом (связанным идентификатором).
ident Сопоставляемый узел должен быть идентификатором (несвязанным идентификатором).
call Сопоставляемое выражение AST должно быть выражением вызова/применения.
lvalue Сопоставляемое выражение AST должно быть lvalue.
sideeffect Сопоставляемое выражение AST должно иметь побочный эффект.
nosideeffect Сопоставляемое выражение AST не должно иметь побочного эффекта.
param Символ, являющийся параметром.
genericparam Символ, являющийся параметром дженерика.
module Символ, являющийся модулем.
type Символ, являющийся типом.
var Символ, являющийся переменной.
let Символ, являющийся let переменной.
const Символ, являющийся константой.
result Специальная result переменная.
proc Символ, являющийся процедурой.
method Символ, являющийся методом.
iterator Символ, являющийся итератором.
converter Символ, являющийся конвертером.
macro Символ, являющийся макросом.
template Символ, являющийся шаблоном.
field Символ, являющийся полем в кортеже или объекте.
enumfield Символ, являющийся полем в перечислении.
forvar Переменная цикла for.
label Метка (используется в block операторах).
nk* Сопоставляемое выражение AST должно иметь указанный тип. (Например, nkIfStmt обозначает оператор if.)
alias Указано, что указанный параметр должен быть алиасом какого-либо другого параметра.
noalias Указано, что каждый другой параметр не должен быть алиасом указанного параметра.

Предикаты, которые совпадают по имени с ключевым словом, должны быть экранированы с помощью обратных кавычек. Предикаты alias и noalias относятся не только к сопоставляемому выражению AST, но и к каждому другому связанному параметру; синтаксически они должны появляться после обычных предикатов AST:

template ex{a = b + c}(a: int{noalias}, b, c: int) =
  # this transformation is only valid if 'b' and 'c' do not alias 'a':
  a = b
  inc a, c

Другой пример:

proc somefunc(s: string)                 = assert s == "variable"
proc somefunc(s: string{nkStrLit})       = assert s == "literal"
proc somefunc(s: string{nkRStrLit})      = assert s == r"raw"
proc somefunc(s: string{nkTripleStrLit}) = assert s == """triple"""
proc somefunc(s: static[string])         = assert s == "constant"

# Use parameter constraints to provide overloads based on both the input parameter type and form.
var variable = "variable"
somefunc(variable)
const constant = "constant"
somefunc(constant)
somefunc("literal")
somefunc(r"raw")
somefunc("""triple""")

Операторы шаблонов

Операторы *, **, |, ~ имеют специальное значение в шаблонах, если они написаны в инфиксной форме.

Оператор |

Оператор | при использовании в качестве инфиксного оператора создаёт упорядоченный выбор:

template t{0|1}(): untyped = 3
let a = 1
# outputs 3:
echo a

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

template t{0|1}(): untyped = 3
# outputs 1:
echo 1

Причина в том, что компилятор уже преобразовал 1 в "1" для оператора echo. Однако макрос переписывания терминов не должен изменять семантику. На самом деле, их можно отключить с помощью опции командной строки --patterns:off или временно с помощью директивы patterns.

Оператор {}

Выражение шаблона может быть привязано к параметру шаблона с помощью обозначения expr{param}.

template t{(0|1|2){x}}(x: untyped): untyped = x + 1
let a = 1
# outputs 2:
echo a

Оператор ~

Оператор ~ является оператором 'not' в шаблонах.

template t{x = (~x){y} and (~x){z}}(x, y, z: bool) =
  x = y
  if x: x = z

var
  a = false
  b = true
  c = false
a = b and c
echo a

Оператор *

Оператор * может распаковать вложенное бинарное выражение, например a & b & c, до &(a, b, c).

var
  calls = 0

proc `&&`(s: varargs[string]): string =
  result = s[0]
  for i in 1..len(s)-1: result.add s[i]
  inc calls

template optConc{ `&&` * a }(a: string): untyped = &&a

let space = " "
echo "my" && (space & "awe" && "some " ) && "concat"

# check that it's been optimized properly:
doAssert calls == 1

Второй оператор * должен быть параметром; он используется для сбора всех аргументов. Выражение "my" && (space & "awe" && "some " ) && "concat" передаётся optConc в a в качестве специального списка (типа nkArgList), который распаковывается в выражение вызова; таким образом, вызов optConc даёт:

`&&`("my", space & "awe", "some ", "concat")

Оператор **

Оператор ** очень похож на оператор *, за исключением того, что он собирает не только все аргументы, но и сопоставленные операторы в обратной польской записи:

import std/macros

type
  Matrix = object
    dummy: int

proc `*`(a, b: Matrix): Matrix = discard
proc `+`(a, b: Matrix): Matrix = discard
proc `-`(a, b: Matrix): Matrix = discard
proc `$`(a: Matrix): string = result = $a.dummy
proc mat21(): Matrix =
  result.dummy = 21

macro optM{ (`+`|`-`|`*`) ** a }(a: Matrix): untyped =
  echo treeRepr(a)
  result = newCall(bindSym"mat21")

var x, y, z: Matrix

echo x + y * z - x

Это передаёт выражение x + y * z - x макросу optM как узел nnkArgList содержащий:

Arglist
  Sym "x"
  Sym "y"
  Sym "z"
  Sym "*"
  Sym "+"
  Sym "x"
  Sym "-"

(Это обратная польская запись для x + y * z - x.)

Параметры

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

template optWrite{
  write(f, x)
  ((write|writeLine){w})(f, y)
}(x, y: varargs[untyped], f: File, w: untyped) =
  w(f, x, y)

Директива noRewrite

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

Директива noRewrite может фактически предотвратить дальнейшее переписывание в отмеченном коде, например, в приведённом примере echo("ab") будет переписан только один раз:

template pwnEcho{echo(x)}(x: untyped) =
  {.noRewrite.}: echo("pwned!")

echo "ab"

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

Пример: Частичная оценка

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

proc p(x, y: int; cond: bool): int =
  result = if cond: x + y else: x - y

template optP1{p(x, y, true)}(x, y: untyped): untyped = x + y
template optP2{p(x, y, false)}(x, y: untyped): untyped = x - y

Пример: Поднятие

Следующий пример демонстрирует, как можно реализовать некую форму поднятия:

import std/pegs

template optPeg{peg(pattern)}(pattern: string{lit}): Peg =
  var gl {.global, gensym.} = peg(pattern)
  gl

for i in 0 .. 3:
  echo match("(a b c)", peg"'(' @ ')'")
  echo match("W_HI_Le", peg"\y 'while'")

Шаблон optPeg оптимизирует случай конструктора Peg со строковым литералом, так что шаблон будет парситься только один раз при запуске программы и храниться в глобальной переменной gl, которая затем используется повторно. Эта оптимизация называется поднятием, потому что она сопоставима с классическим поднятием циклов.

Перегрузка на основе AST

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

proc optLit(a: string{lit|`const`}) =
  echo "string literal"
proc optLit(a: string) =
  echo "no string literal"

const
  constant = "abc"

var
  variable = "xyz"

optLit("literal")
optLit(constant)
optLit(variable)

Однако ограничения alias и noalias недоступны в обычных процедурах.

Параллелизм и порождение

В Nim есть два варианта параллелизма:

  1. Структурированный параллелизм с помощью оператора parallel.
  2. Неструктурированный параллелизм с помощью оператора spawn.

Nim имеет встроенный пул потоков, который можно использовать для задач, требующих интенсивных вычислений на ЦП. Для задач, требующих интенсивных операций ввода-вывода, следует использовать возможности async и await. Для работы как параллельного, так и оператора spawn требуется модуль threadpool.

Несколько неоднозначно, что spawn также используется в операторе parallel с немного другими семантиками. spawn всегда принимает выражение вызова вида f(a, ...). Пусть T является типом возврата f. Если T является void, то тип возврата spawn также является void, иначе он является FlowVar[T].

Внутри блока parallel оператор FlowVar[T] иногда устраняется до T. Это происходит, когда T не содержит памяти, управляемой сборщиком мусора. Компилятор может гарантировать, что расположение в location = spawn f(...) не читается преждевременно внутри блока parallel, и поэтому нет необходимости в накладных расходах косвенного доступа через FlowVar[T] для обеспечения корректности.

Примечание: В настоящее время исключения не передаются между задачами, помеченными spawn!

Эта функция, вероятно, будет удалена в будущем, так как внешние пакеты могут иметь лучшие решения.

Оператор spawn

Оператор spawn может использоваться для передачи задачи в пул потоков:

import std/threadpool

proc processLine(line: string) =
  discard "do some heavy lifting here"

for x in lines("myinput.txt"):
  spawn processLine(x)
sync()

По причинам безопасности типов и простоты реализации выражение, которое принимает spawn, ограничено:

  • Это должно быть выражение вызова f(a, ...).
  • f должен быть gcsafe.
  • f не должен иметь соглашение о вызове closure.
  • Параметры f не могут быть типа ptr. Это означает, что для передачи данных необходимо использовать необработанные ptr, предупреждая программиста о необходимости быть осторожным.
  • Параметры ref глубоко копируются, что является тонким изменением семантики и может привести к проблемам производительности, но гарантирует безопасность памяти. Эта глубокая копия выполняется через system.deepCopy, поэтому её можно переопределить.
  • Для безопасного обмена данными между f и вызывающей стороной требуется использовать глобальную переменную Channel. Однако, поскольку spawn может возвращать результат, часто дальнейшая коммуникация не требуется.

spawn выполняет переданное выражение в пуле потоков и возвращает переменную потока данных FlowVar[T], которую можно прочитать. Чтение с оператором ^ является блокирующим. Однако можно использовать blockUntilAny для одновременного ожидания нескольких переменных потока.

import std/threadpool, ...

# wait until 2 out of 3 servers received the update:
proc main =
  var responses = newSeq[FlowVarBase](3)
  for i in 0..2:
    responses[i] = spawn tellServer(Update, "key", "value")
  var index = blockUntilAny(responses)
  assert index >= 0
  responses.del(index)
  discard blockUntilAny(responses)

Переменные потока данных гарантируют, что гонки данных невозможны. По техническим причинам не каждый тип T может использоваться в переменной потока данных: T должен быть ref, string, seq или типа, не содержащего типов, управляемых сборщиком мусора. Это ограничение на практике несложно обойти.

Оператор parallel

Пример:

# Compute pi in an inefficient way
import std/[strutils, math, threadpool]
{.experimental: "parallel".}

proc term(k: float): float = 4 * math.pow(-1, k) / (2*k + 1)

proc pi(n: int): float =
  var ch = newSeq[float](n + 1)
  parallel:
    for k in 0..ch.high:
      ch[k] = spawn term(float(k))
  for k in 0..ch.high:
    result += ch[k]

echo formatFloat(pi(5000))

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

Подмножество на самом деле является полным языком с последующими ограничениями/изменениями:

  • spawn внутри блока parallel имеет специальную семантику.
  • Каждое местоположение вида a[i], a[i..j] и dest, где dest является частью шаблона dest = spawn f(...) должно быть доказуемо независимым. Это называется проверкой независимости.
  • Каждое другое сложное местоположение loc, используемое в порождённой процедуре (spawn f(loc)), должно быть неизменным на протяжении всего блока parallel секции. Это называется проверкой неизменности. В настоящее время не определено, что именно означает «сложное местоположение». Нам нужно сделать это оптимизацией!
  • Каждый доступ к массиву должен быть доказуемо в пределах границ. Это называется проверкой границ.
  • Срезы оптимизированы таким образом, чтобы не выполнялась копия. Эта оптимизация пока не выполняется для обычных срезов вне блока parallel.

Строгие определения и параметры out

С experimental: "strictDefs" каждая локальная переменная должна быть явно инициализирована, прежде чем её можно будет использовать:

{.experimental: "strictDefs".}

proc test =
  var s: seq[string]
  s.add "abc" # invalid!

Следует записать как:

{.experimental: "strictDefs".}

proc test =
  var s: seq[string] = @[]
  s.add "abc" # valid!

Проводится анализ потока управления, чтобы доказать, что переменная была записана до её использования. Таким образом, следующее допустимо:

{.experimental: "strictDefs".}

proc test(cond: bool) =
  var s: seq[string]
  if cond:
    s = @["y"]
  else:
    s = @[]
  s.add "abc" # valid!

В этом примере каждый путь устанавливает s в значение перед его использованием.

{.experimental: "strictDefs".}

proc test(cond: bool) =
  let s: seq[string]
  if cond:
    s = @["y"]
  else:
    s = @[]

С experimental: "strictDefs", операторы let могут не иметь начального значения, но каждый путь должен установить s в значение перед его использованием.

Параметры out

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

proc myopen(f: out File; name: string): bool =
  f = default(File)
  result = open(f, name)

Хотя обычно лучше использовать тип возврата для возврата результатов, соображения API и ABI могут сделать это невозможным. Например, для var T Nim отображает out T на скрытый указатель. Например, процедура stat POSIX может быть обернута следующим образом:

proc stat*(a1: cstring, a2: out Stat): cint {.importc, header: "<sys/stat.h>".}

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

proc p(x: out int; y: out string; cond: bool) =
  x = 4
  if cond:
    y = "abc"
  # error: not every path initializes 'y'

Параметры вывода и обработка исключений

Анализ должен учитывать исключения (но в настоящее время это не делается):

proc p(x: out int; y: out string; cond: bool) =
  x = canRaise(45)
  y = "abc" # <-- error: not every path initializes 'y'

После того, как реализация учла исключения, достаточно легко использовать outParam = default(typeof(outParam)) в начале тела процедуры.

Параметры-выходы и наследование

Некорректно передавать lvalue супертипа в параметр out T:

type
  Superclass = object of RootObj
    a: int
  Subclass = object of Superclass
    s: string

proc init(x: out Superclass) =
  x = Superclass(a: 8)

var v: Subclass
init v
use v.s # the 's' field was never initialized!

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

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

Строгие объекты case

С experimental: "strictCaseObjects" каждый доступ к полю проверяется на корректность во время компиляции. Поле находится в разделе case объекта object.

{.experimental: "strictCaseObjects".}

type
  Foo = object
    case b: bool
    of false:
      s: string
    of true:
      x: int

var x = Foo(b: true, x: 4)
case x.b
of true:
  echo x.x # valid
of false:
  echo "no"

case x.b
of false:
  echo x.x # error: field access outside of valid case branch: x.x
of true:
  echo "no"

Примечание: Реализация «строгих объектов case» экспериментальная, но концепция надёжна, и ожидается, что в последующих версиях этот режим станет стандартным.

Особые (нестандартные) процедуры

Стандартная стратегия генерации кода исключений в модели ARC/ORC — это так называемая реализация --exceptions:goto. Эта реализация вставляет проверку после каждого вызова, который потенциально может вызвать исключение. Типичная последовательность инструкций для этого на 64-разрядной машине x86 выглядит так:

cmp DWORD PTR [rbx], 0
je  .L1

Это обращение к памяти, за которым следует переход. (Идеальная реализация использовала бы флаг переноса и одну инструкцию, например, jc .L1.)

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

proc wontRaise(x: int) {.quirky.} =
  if x != 0:
    # because of `quirky` this will continue even if `write` raised an IO exception:
    write x
    wontRaise(x-1)

wontRaise 10

Если используемая модель исключений не --exceptions:goto, то псевдоним quirky не оказывает влияния и игнорируется.

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

when defined(nimHasQuirky):
  {.push quirky: on.}

proc doRaise() = raise newException(ValueError, "")

proc f(): string = "abc"

proc q(cond: bool) =
  if cond:
    doRaise()
  echo f()

q(true)

when defined(nimHasQuirky):
  {.pop.}

Предупреждение: Псевдоним quirky влияет только на генерацию кода, проверка на корректность не выполняется!

Потоки в ARC/ORC

ARC/ORC изначально поддерживает общую кучу. Это означает, что сообщения могут передаваться между потоками без копирования. Однако без копирования данных существует потенциальная опасность гонок данных. Гонки данных предотвращаются во время компиляции, если обеспечить, что отправляются только изолированные подграфы.

Изоляция

Модуль стандартной библиотеки isolation.nim предоставляет универсальный тип Isolated[T], который отражает важную идею, что ничего другого не может ссылаться на график, упакованный внутри Isolated[T]. Именно это должна использовать реализация канала для предотвращения гонок данных:

proc send*[T](c: var Channel[T]; msg: sink Isolated[T])
proc recv*[T](c: var Channel[T]): T
  ## Note: Returns T, not Isolated[T] for convenience.

proc recvIso*[T](c: var Channel[T]): Isolated[T]
  ## remembers the data is Isolated[T].

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

Конструкция должна гарантировать, что инвариант соблюдается, а именно, что обернутый T свободен от внешних ссылок на него. isolate гарантирует этот инвариант. Он вдохновлён конструкцией Pony recover.

func isolate(x: sink T): Isolated[T] {.magic: "Isolate".}

Как вы можете видеть, это новый встроенный элемент, поскольку проверка, которую он выполняет на x, нетривиальна:

Если T не содержит тип ref или closure, то он изолирован. В противном случае анализируется синтаксическая структура x:

  • Литералы, такие как nil, 4, "abc" изолированы.
  • Локальная переменная или параметр процедуры изолированы, если выполняется одно из этих условий:
    1. Его тип помечен псевдонимом .sendable . Обратите внимание, что Isolated[T] помечен как .sendable.
    2. Его тип содержит потенциально опасные типы ref и proc {.closure} только в местах, защищённых контейнером .sendable.
  • Конструктор массива [x...] изолирован, если каждый элемент x изолирован.
  • Конструктор объекта Obj(x...) изолирован, если каждый элемент x изолирован.
  • Выражение if или case изолировано, если все возможные значения, которые может вернуть выражение, изолированы.
  • Преобразование типов C(x) изолировано, если x изолировано. Аналогично для выражений cast.
  • Вызов функции f(x...) изолирован, если f .noSideEffect и для каждого аргумента x:
    • x изолирован или
    • Тип возвращаемого значения f не может ссылаться на тип x . Это проверяется с помощью формы анализа ссылок, как объяснено в следующем абзаце.

Анализ ссылок

Начнём с важного простого случая, который должен быть корректным: передача результата parseJson в канал. Поскольку сигнатура func parseJson(input: string): JsonNode, легко видеть, что JsonNode никогда не может быть просто представлением input, который является string.

Другой случай — функция идентичности id, send id(myJsonGraph) должна быть некорректной, так как нам неизвестно, сколько ссылок на myJsonGraph существует в другом месте.

В общем случае тип A может ссылаться на тип T , если:

  • A и T — это одни и те же типы.
  • A — это отдельный тип, полученный от T.
  • A — это поле внутри T , если T — это окончательный тип объекта.
  • T — это наследоваемый тип объекта. (Наследуемый тип всегда может содержать field: A).
  • T — это тип закрытия. Причина: среда T может содержать поле типа A.
  • A — это элемент типа T , если T — это тип массива, последовательности или указателя.

Псевдоним sendable

Тип контейнера может быть помечен как .sendable. .sendable объявляет, что тип инкапсулирует тип ref достаточно эффективно, чтобы переменная этого типа контейнера могла использоваться в контексте isolate:

type
  Isolated*[T] {.sendable.} = object ## Isolated data can only be moved, not copied.
    value: T

proc `=copy`*[T](dest: var Isolated[T]; src: Isolated[T]) {.error.}

proc `=sink`*[T](dest: var Isolated[T]; src: Isolated[T]) {.inline.} =
  # delegate to value's sink operation
  `=sink`(dest.value, src.value)

proc `=destroy`*[T](dest: var Isolated[T]) {.inline.} =
  # delegate to value's destroy operation
  `=destroy`(dest.value)

Сам псевдоним .sendable является экспериментальной, непроверяемой, небезопасной аннотацией. В настоящее время он используется только Isolated[T].

Псевдоним virtual

virtual предназначен для расширения или создания виртуальных функций при целевой работе с cpp-бекендом. Когда процедура помечена как virtual, она предварительно объявляет заголовок процедуры в теле типа.

Вот пример использования псевдонима virtual:

proc newCpp*[T](): ptr T {.importcpp: "new '*0()".}
type
  Foo = object of RootObj
  FooPtr = ptr Foo
  Boo = object of Foo
  BooPtr = ptr Boo

proc salute(self: FooPtr) {.virtual.} =
  echo "hello foo"

proc salute(self: BooPtr) {.virtual.} =
  echo "hello boo"

let foo = newCpp[Foo]()
let boo = newCpp[Boo]()
let booAsFoo = cast[FooPtr](newCpp[Boo]())

foo.salute() # prints hello foo
boo.salute() # prints hello boo
booAsFoo.salute() # prints hello boo
В этом примере функция salute является виртуальной как в типах Foo, так и в Boo. Это позволяет использовать полиморфизм.

Псевдоним virtual также поддерживает специальный синтаксис для выражения ограничений C++. Вот как это работает:

$1 относится к имени функции 'idx относится к типу аргумента в позиции idx. Где idx = 1 — это аргумент this . #idx относится к имени аргумента.

Тип возвращаемого значения может быть указан как -> '0, но это необязательно и часто не требуется.

{.emit:"""/*TYPESECTION*/
#include <iostream>
 class CppPrinter {
 public:
   
   virtual void printConst(char* message) const {
       std::cout << "Const Message: " << message << std::endl;
   }
   virtual void printConstRef(char* message, const int& flag) const {
       std::cout << "Const Ref Message: " << message << std::endl;
   }
};
""".}

type
 CppPrinter {.importcpp, inheritable.} = object
 NimPrinter {.exportc.} = object of CppPrinter

proc printConst(self: CppPrinter; message:cstring) {.importcpp.}
CppPrinter().printConst(message)

# override is optional.
proc printConst(self: NimPrinter; message: cstring) {.virtual: "$1('2 #2) const override".} =
 echo "NimPrinter: " & $message

proc printConstRef(self: NimPrinter; message: cstring; flag:int32) {.virtual: "$1('2 #2, const '3& #3 ) const override".} =
 echo "NimPrinterConstRef: " & $message

NimPrinter().printConst(message)
var val: int32 = 10
NimPrinter().printConstRef(message, val)

Псевдоним constructor

Псевдоним constructor может быть использован двумя способами: в сочетании с importcpp для импорта C++-конструктора и для объявления конструкторов, функционирующих подобно virtual.

Рассмотрим:

type Foo* = object
  x: int32

proc makeFoo(x: int32): Foo {.constructor.} =
  result.x = x

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

Как и virtual, constructor также поддерживает синтаксис, позволяющий выражать ограничения C++.

Например:

{.emit:"""/*TYPESECTION*/
struct CppClass {
  int x;
  int y;
  CppClass(int inX, int inY) {
    this->x = inX;
    this->y = inY;
  }
  //CppClass() = default;
};
""".}

type
  CppClass* {.importcpp, inheritable.} = object
    x: int32
    y: int32
  NimClass* = object of CppClass

proc makeNimClass(x: int32): NimClass {.constructor:"NimClass('1 #1) : CppClass(0, #1)".} =
  result.x = x

# Optional: define the default constructor explicitly
proc makeCppClass(): NimClass {.constructor: "NimClass() : CppClass(0, 0)".} =
  result.x = 1

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

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

Инициализатор конструктора

По умолчанию Nim инициализирует типы importcpp значениями {}. Это может быть проблематично при импорте типов с удалённым конструктором по умолчанию. Чтобы избежать этого, можно указать значения по умолчанию для конструктора, указав значения по умолчанию для параметров процедуры в процедуре constructor.

Например:

{.emit: """/*TYPESECTION*/
struct CppStruct {
  CppStruct(int x, char* y): x(x), y(y){}
  int x;
  char* y;
};
""".}
type
  CppStruct {.importcpp, inheritable.} = object

proc makeCppStruct(a: cint = 5, b:cstring = "hello"): CppStruct {.importcpp: "CppStruct(@)", constructor.}

(proc (s: CppStruct) = echo "hello")(makeCppStruct())
# If one removes a default value from the constructor and passes it to the call explicitly, the C++ compiler will complain.
Пропуск инициализаторов в членах полей

Используя noInit в объявлении типа или поля, компилятор пропустит инициализатор. Сделав это, можно явно инициализировать эти значения в конструкторе владельца типа.

Например:

{.emit: """/*TYPESECTION*/
  struct Foo {
    Foo(int a){};
  };
  struct Boo {
    Boo(int a){};
  };
  
  """.}

type
  Foo {.importcpp.} = object
  Boo {.importcpp, noInit.} = object
  Test {.exportc.} = object
    foo {.noInit.}: Foo
    boo: Boo

proc makeTest(): Test {.constructor: "Test() : foo(10), boo(1)".} =
  discard

proc main() =
  var t = makeTest()

main()

Результат будет:

struct Test {
        Foo foo;
        Boo boo;
  N_LIB_PRIVATE N_NOCONV(, Test)(void);
};

Обратите внимание, что без noInit он бы выдал Foo foo {} и Boo boo {}

Псевдоним member

Как и псевдонимы constructor и virtual, псевдоним member может использоваться для привязки процедуры к типу C++. Он более гибкий, чем псевдоним virtual, в том смысле, что принимает не только имена, но и операторы и деструкторы.

Например:

proc print(s: cstring) {.importcpp: "printf(@)", header: "<stdio.h>".}

type
  Doo {.exportc.} = object
    test: int

proc memberProc(f: Doo) {.member.} =
  echo $f.test

proc destructor(f: Doo) {.member: "~'1()", used.} =
  print "destructing\n"

proc `==`(self, other: Doo): bool {.member: "operator==('2 const & #2) const -> '0".} =
  self.test == other.test

let doo = Doo(test: 2)
doo.memberProc()
echo doo == Doo(test: 1)

Вывод:

2
false
destructing
destructing

Обратите внимание, как C++-деструктор вызывается автоматически. Также обратите внимание на двойную реализацию == в качестве оператора в Nim, а также в C++. Это полезно, если вам нужно, чтобы тип соответствовал некоторым C++ concept или trait при работе с ними.

Побочным эффектом возможности объявления C++ операторов является то, что теперь вы также можете создать C++-функтор для бесшовной работы с C++ лямбда-выражениями (синтаксическим сахаром для функторов).

Например:

type
  NimFunctor = object
    discard
proc invoke(f: NimFunctor; n: int) {.member: "operator ()('2 #2)".} =
  echo "FunctorSupport!"

{.experimental: "callOperator".}
proc `()`(f: NimFunctor; n:int) {.importcpp: "#(@)" .}
NimFunctor()(1)
Обратите внимание, что мы используем перегрузку () для обеспечения одинаковой семантики в Nim, но в importcpp мы импортируем функтор как функцию. Это позволяет легко взаимодействовать с функциями, которые принимают, например, оператор const в своей сигнатуре.

Вставленные символы в обобщенных процедурах и шаблонах

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

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

const value = "captured"
template foo(x: int, body: untyped): untyped =
  let value {.inject.} = "injected"
  body

proc old[T](): string =
  foo(123):
    return value # warning: a new `value` has been injected, use `bind` or turn on `experimental:openSym`
echo old[int]() # "captured"

template oldTempl(): string =
  block:
    foo(123):
      value # warning: a new `value` has been injected, use `bind` or turn on `experimental:openSym`
echo oldTempl() # "captured"

{.experimental: "openSym".}

proc bar[T](): string =
  foo(123):
    return value
assert bar[int]() == "injected" # previously it would be "captured"

proc baz[T](): string =
  bind value
  foo(123):
    return value
assert baz[int]() == "captured"

template barTempl(): string =
  block:
    foo(123):
      value
assert barTempl() == "injected" # previously it would be "captured"

template bazTempl(): string =
  bind value
  block:
    foo(123):
      value
assert bazTempl() == "captured"

Эта опция также генерирует новый тип узла nnkOpenSym, который содержит ровно 1 узел nnkSym. В будущем это может быть объединено с незначительно измененным узлом nnkOpenSymChoice, но макросы, которые хотят поддерживать экспериментальную функцию, все равно должны обрабатывать nnkOpenSym, поскольку тип узла просто не будет сгенерирован, а не удален.

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

const value = "captured"
template foo(x: int, body: untyped): untyped =
  let value {.inject.} = "injected"
  {.push experimental: "genericsOpenSym".}
  body
  {.pop.}

proc bar[T](): string =
  foo(123):
    return value
echo bar[int]() # "injected"

template barTempl(): string =
  block:
    var res: string
    foo(123):
      res = value
    res
assert barTempl() == "injected"

VTable для методов

Методы теперь поддерживают реализации, основанные на VTable, с помощью --experimental:vtables. Обратите внимание, что опция должна быть включена глобально. Таблица виртуальных методов хранится в информации о типе объекта, которая представляет собой массив указателей на функции.

method foo(x: Base, ...) {.base.}
method foo(x: Derived, ...) {.base.}

Примерно генерируется диспетчер, подобный:

proc foo_dispatch(x: Base, ...) =
  x.typeinfo.vtable[method_index](x, ...) # method_index is the index of the sorted order of a method

Методы должны находиться в том же модуле, где определен их тип.

# types.nim
type
  Base* = ref object
import types

method foo(x: Base) {.base.} = discard

Это приводит к ошибке: метод foo может быть определен только в том же модуле, что и его тип (Base).

Директива asmSyntax

Директива asmSyntax используется для указания целевого синтаксиса встроенного ассемблера в инструкции asm.

Она предотвращает компиляцию кода с различным синтаксисом встроенного ассемблера целевого компилятора, т.е. не позволит коду встроенного ассемблера gcc компилироваться с vcc.

proc nothing() =
  asm {.asmSyntax: "gcc".}"""
    nop
  """

Текущая реализация бэкенда C(C++) не может генерировать код для gcc и vcc одновременно. Например, {.asmSyntax: "vcc".} с компилятором ICC не сгенерирует код с синтаксисом ассемблера intel, даже если ICC может использовать как синтаксис ассемблера gcc, так и vcc.

© 2006–2024 Andreas Rumpf
Licensed under the MIT License.
https://nim-lang.org/docs/manual_experimental.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API