Экспериментальные возможности Nim
О данном документе
В данном документе описываются экспериментальные возможности Nim. Некоторые из них не покрываются пragma .experimental или переключателем --experimental, поскольку они уже реализованы в виде специального синтаксиса, и можно использовать библиотеки Nim, использующие эти возможности, без их непосредственного применения.
Примечание: Если не указано иное, эти возможности не будут удалены, а будут доработаны и пересмотрены.
Объекты уровня пакета
Каждый модуль Nim находится в (nimble) пакете. Тип объекта может быть присоединен к пакету, в котором он находится. Если это сделано, тип может быть использован в других модулях как тип объекта неполный. Эта возможность позволяет разделить рекурсивные зависимости типов между модулями. Неполные типы объектов всегда передаются byref и могут использоваться только в контексте типа указателя (var/ref/ptr IncompleteObject) в общем случае, так как компилятор пока не знает размер объекта. Для завершения неполного объекта необходимо использовать пragma package. package подразумевает byref.
Пока тип T неполный, недоступны ни sizeof(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 Тип 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.
Ковариантность
Ковариантность в Nim может быть введена только через типы, подобные указателям, такие как ptr и ref. Типы Sequence, Array и OpenArray, созданные с типами, подобными указателям, будут считаться ковариантными только в том случае, если они также неизменяемы. Введение модификатора var или дополнительных ptr или ref косвенных указателей приведет к инвариантному обработке этих типов.
Типы proc в настоящее время всегда являются инвариантными, но в будущих версиях Nim этот принцип может быть изменен.
Пользовательские обобщенные типы также могут быть ковариантными по отношению к некоторым из своих параметров. По умолчанию все обобщенные параметры считаются инвариантными, но вы можете применить модификатор префикса in к параметру, чтобы сделать его контравариантным, или out – чтобы сделать его ковариантным:
type
AnnotatedPtr[out T] =
metadata: MyTypeInfo
p: ref T
RingBuffer[out T] =
startPos: int
data: seq[T]
Action {.importcpp: "std::function<void ('0)>".} [in T] = object
Когда указанный обобщенный параметр используется для создания типа, подобного указателю, как в случае с AnnotatedPtr выше, получившийся обобщенный тип также будет иметь ковариантность, подобную указателю:
type GuiWidget = object of RootObj Button = object of GuiWidget ComboBox = object of GuiWidget var widgetPtr: AnnotatedPtr[GuiWidget] buttonPtr: AnnotatedPtr[Button] ... proc drawWidget[T](x: AnnotatedPtr[GuiWidget]) = ... # you can call procs expecting base types by supplying a derived type drawWidget(buttonPtr) # and you can convert more-specific pointer types to more general ones widgetPtr = buttonPtr
Так же, как и с обычными указателями, ковариантность будет включена только для неизменяемых значений:
proc makeComboBox[T](x: var AnnotatedPtr[GuiWidget]) =
x.p = new(ComboBox)
makeComboBox(buttonPtr) # Error, AnnotatedPtr[Button] cannot be modified
# to point to a ComboBox
С другой стороны, в примере RingBuffer выше, указанный обобщенный параметр используется для создания типа seq, который не является типом указателя, это означает, что полученный обобщенный тип будет иметь ковариантность, аналогичную массиву или последовательности (т.е. он будет ковариантным только при создании с типами ptr и ref):
type
Base = object of RootObj
Derived = object of Base
proc consumeBaseValues(b: RingBuffer[Base]) = ...
var derivedValues: RingBuffer[Derived]
consumeBaseValues(derivedValues) # Error, Base and Derived values may differ
# in size
proc consumeBasePointers(b: RingBuffer[ptr Base]) = ...
var derivedPointers: RingBuffer[ptr Derived]
consumeBaseValues(derivedPointers) # This is legal
Обратите внимание, что Nim будет рассматривать пользовательские типы, подобные указателям, как надлежащие альтернативы встроенным типам указателей. То есть, типы, такие как seq[AnnotatedPtr[T]] или RingBuffer[AnnotatedPtr[T]] также будут считаться ковариантными, и вы можете создавать новые типы, подобные указателям, создавая экземпляры других пользовательских типов, подобных указателям.
Контравариантные параметры, введенные с помощью модификатора in в настоящее время полезны только при взаимодействии с импортированными типами, имеющими такую семантику.
Автоматическое разыменование
Автоматическое разыменование выполняется для первого аргумента вызова процедуры. Эта функция должна быть включена только через {.experimental: "implicitDeref".}:
{.experimental: "implicitDeref".}
proc depth(x: NodeObj): int = ...
var
n: Node
new(n)
echo n.depth
# no need to write n[].depth either Переупорядочение кода
Функция переупорядочения кода может неявно переупорядочить определения процедур, шаблонов и макросов, а также объявления и инициализации переменных в области видимости верхнего уровня, так что программист в значительной степени может не беспокоиться о правильном порядке определения или быть вынужден использовать объявления вперед для определения внутри модуля.
Пример:
{.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, но является нестабильной функцией.
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
Нотация do
В качестве специальной более удобной нотации, выражения proc, участвующие в вызовах процедур, могут использовать ключевое слово do:
sort(cities) do (x,y: string) -> int:
cmp(x.len, y.len)
# Less parenthesis using the method plus command syntax:
cities = cities.map do (x:string) -> string:
"City of " & x
# In macros, the do notation is often used for quasi-quoting
macroResults.add quote do:
if not `ex`:
echo `info`, ": Check failed: ", `expString`
do записывается после скобок, окружающих обычные параметры proc. Выражение proc, представленное блоком do, добавляется к ним. При вызовах с использованием командного синтаксиса блок do будет привязан к непосредственно предшествующему выражению, преобразуя его в вызов.
do со скобками является анонимной proc; однако do без скобок – просто блок кода. Нотация do может использоваться для передачи нескольких блоков макросу:
macro performWithUndo(task, undo: untyped) = ... performWithUndo do: # multiple-line block of code # to perform the task do: # code to undo it
Специальные операторы
Операторы с точкой
Примечание: Операторы с точкой все еще находятся на стадии разработки и поэтому должны быть включены с помощью {.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)
Аннотация not nil
Примечание: Это экспериментальная функция. Она может быть включена с помощью {.experimental: "notnil"}.
Все типы, для которых nil является допустимым значением, могут быть аннотированы аннотацией not nil для исключения nil в качестве допустимого значения:
{.experimental: "notnil"}
type
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:
var x: PObject
p(x)
Компилятор гарантирует, что каждая ветвь кода инициализирует переменные, содержащие ссылки на не nil-указатели. Подробности этого анализа должны быть указаны здесь.
Концепции
Концепции, также известные как «пользовательские классы типов», используются для указания произвольного набора требований, которые должен удовлетворять соответствующий тип.
Концепции записываются в следующем формате:
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
Концепция считается совпадением, если:
- все выражения в теле могут быть скомпилированы для проверяемого типа
- все статически вычисляемые логические выражения в теле должны быть истинными
Идентификаторы после ключевого слова 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 позволяет легко проверить точные сигнатуры типов требуемых операций, но поскольку вывод типов и параметры по умолчанию все еще применяются в теле концепции, можно также описать протоколы использования, которые не раскрывают реализацию деталей.
Как и обобщения, концепции создаются ровно один раз для каждого проверяемого типа, и любой статический код, включенный в тело, выполняется только один раз.
Диагностика концепций
По умолчанию компилятор будет сообщать об ошибках соответствия в концепциях только тогда, когда не может быть выбран другой перегруз, и возникает обычная ошибка компиляции. Когда вам необходимо понять, почему компилятор не сопоставляет определенную концепцию и, как следствие, выбирает неправильный перегруз, вы можете применить пragma explain к телу концепции или конкретной точке вызова.
type
MyConcept {.explain.} = concept ...
overloadedProc(x, y, z) {.explain.}
Это предоставит подсказки в выводе компилятора, либо каждый раз, когда концепция не соответствует, либо только в конкретной точке вызова.
Обобщенные концепции и правила привязки типов
Типы концепции могут быть параметрическими, как и обычные обобщенные типы:
### matrixalgo.nim
import 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], просто по их короткому имени. Подобно обычным дженерическим типам, концепция будет автоматически инстанцирована с bind после auto type вместо каждого отсутствующего параметра дженерика.
Обратите внимание, что общие концепции, такие как Enumerable[T], могут быть сопоставлены с конкретными типами, такими как string. Nim не требует, чтобы тип концепции имел такое же количество параметров, как тип, с которым производится сопоставление. Если вы хотите выразить требование к дженерическим параметрам сопоставляемого типа, вы можете использовать оператор сопоставления типов, такой как genericHead или stripGenericParams внутри тела концепции, чтобы получить неинстанцированную версию типа, которую вы можете затем попытаться инстанцировать любым необходимым способом. Например, вот как можно определить классическую концепцию Functor из Haskell и затем продемонстрировать, что тип Option[T] Nim является её экземпляром:
import 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 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 Операции, связанные с типом
Существует 4 операции, связанные с типом:
- Присваивание
- Перемещения
- Уничтожение
- Глубокое копирование для обмена данными между потоками
Эти операции могут быть переопределены вместо перегрузки. Это означает, что реализация автоматически подключается к структурированным типам. Например, если у типа T есть переопределённый оператор присваивания =, этот оператор также используется для присваивания типа seq[T]. Поскольку эти операции связаны с типом, они должны быть связаны с номинальным типом по соображениям простоты реализации: это означает, что переопределённый deepCopy для ref T действительно связан с T, а не с ref T. Это также означает, что нельзя переопределить deepCopy для ptr T и ref T одновременно; вместо этого для одного типа указателя должен использоваться вспомогательный отдельный или объектный тип.
Присваивания, перемещения и уничтожение описаны в документе destructors.
deepCopy
=deepCopy — встроенная функция, которая вызывается всякий раз, когда данные передаются в процедуру с модификатором spawn, чтобы обеспечить безопасность памяти. Программист может переопределить её поведение для конкретного типа ref или ptr T. (В более поздних версиях языка это ограничение может быть ослаблено.)
Подпись должна быть такой:
proc `=deepCopy`(x: T): T
Этот механизм будет использоваться большинством структур данных, поддерживающих общую память, таких как каналы, для реализации безопасного для потоков автоматического управления памятью.
Встроенная функция deepCopy может даже клонировать замыкания и их среды. См. документацию по spawn для получения подробностей.
Макросы оператора case
Макрос, который должен вызываться match, может использоваться для переписывания case операторов для реализации сопоставления с образцом для определённых типов. Следующий пример реализует упрощённую форму сопоставления с образцом для кортежей, используя существующий оператор равенства для кортежей (как предоставлено в system.==):
{.experimental: "caseStmtMacros".}
import macros
macro match(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 "'match' cannot handle this node", it
echo repr result
case ("foo", 78)
of ("foo", 78): echo "yes"
of ("bar", 88): echo "no"
else: discard
В настоящее время макросы оператора case необходимо явно включать с помощью {.experimental: "caseStmtMacros".}.
match макросы подвергаются разрешению перегрузки. Сначала выражение селектора case используется для определения того, какой макрос match вызывать. Затем этому макросу передаётся всё тело оператора case и макрос вычисляется.
Другими словами, макросу необходимо преобразовать всё оператор case, но для определения того, какой макрос вызывать, используется только выражение селектора оператора.
Макросы переписывания терминов
Макросы переписывания терминов — это макросы или шаблоны, которые имеют не только название, но и образец, поиск которого выполняется после фазы семантического анализа компилятора: это означает, что они предоставляют лёгкий способ повышения эффективности компиляционного процесса с помощью пользовательских оптимизаций:
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 Операторы шаблонов
Операторы *, **, |, ~ имеют специальное значение в шаблонах, если они записаны в инфиксной форме.
Оператор |
Оператор | в качестве инфиксного оператора создаёт упорядоченный выбор:
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 Оператор ~
Оператор ~ — это оператор не в шаблонах:
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 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) Пример: Частичная оценка
Следующий пример демонстрирует, как можно реализовать простую частичную оценку с помощью переписывания терминов:
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 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 имеет два типа параллелизма:
-
Структурированный параллелизм с помощью оператора
parallel. -
Неструктурированный параллелизм с помощью оператора
spawn.
Nim имеет встроенный пул потоков, который можно использовать для задач, связанных с интенсивными вычислениями на процессоре. Для задач, связанных с интенсивной работой с вводом-выводом, следует использовать возможности async и await. Для работы как параллельного, так и запускающего оператора требуется модуль 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 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не должны иметь типvar. Это означает, что необходимо использовать сырыеptrдля передачи данных, напоминая программисту о необходимости быть внимательным. -
Параметры
refглубоко копируются, что является тонким изменением семантики и может привести к проблемам производительности, но гарантирует безопасность памяти. Эта глубокая копия выполняется с помощьюsystem.deepCopyи поэтому может быть переопределена. - Для безопасного обмена данными между
fи вызывающим объектом необходимо использовать глобальнуюTChannel. Однако, поскольку spawn может возвращать результат, часто дальнейшее общение не требуется.
spawn выполняет переданное выражение в пуле потоков и возвращает переменную потока данных FlowVar[T], которую можно прочитать. Чтение с помощью оператора ^ является блокирующим. Однако можно использовать blockUntilAny для одновременного ожидания нескольких переменных потока:
import 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 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.
Блокировки и охраны
Помимо spawn и parallel Nim также предоставляет все распространённые механизмы низкоуровневой конкурентности, такие как блокировки, атомарные внутренние функции или переменные состояния.
Nim значительно улучшает безопасность этих функций с помощью дополнительных директив:
- Введена аннотация guard для предотвращения гонок за данные.
- Каждый доступ к охраняемому расположению памяти должен происходить в соответствующем операторе locks.
- Блокировки и процедуры могут быть аннотированы уровнями блокировок для обнаружения потенциальных тупиков во время семантического анализа.
Охраны и раздел блокировок
Защита глобальных переменных
Поля объектов и глобальные переменные могут быть аннотированы с помощью директивы guard.
var glock: TLock
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: TLock; 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 также может использоваться для защиты полей внутри объекта. Охрана затем должна быть другим полем в том же объекте или глобальной переменной.
Так как объекты могут располагаться в куче или на стеке, это значительно повышает выразительность языка:
type
ProtectedCounter = object
v {.guard: L.}: int
L: TLock
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 Уровни блокировок
Уровни блокировок используются для обеспечения глобального порядка блокировок, чтобы обнаружить потенциальные тупики во время семантического анализа. Уровень блокировки — это целое число константа в диапазоне 0..1_000. Уровень блокировки 0 означает, что блокировка вообще не приобретается.
Если блок кода держит блокировку уровня M, то он также может приобрести любую блокировку уровня N < M. Другая блокировка уровня M не может быть приобретена. Блокировки одного уровня могут быть приобретены одновременно только в рамках одного раздела locks.
var a, b: TLock[2]
var x: TLock[1]
# invalid locking order: TLock[1] cannot be acquired before TLock[2]:
{.locks: [x].}:
{.locks: [a].}:
...
# valid locking order: TLock[2] acquired before TLock[1]:
{.locks: [a].}:
{.locks: [x].}:
...
# invalid locking order: TLock[2] acquired before TLock[2]:
{.locks: [a].}:
{.locks: [b].}:
...
# valid locking order, locks of the same level acquired at the same time:
{.locks: [a, b].}:
...
Вот как можно реализовать типичный оператор многократной блокировки в Nim. Обратите внимание на то, как требуется проверка времени выполнения, чтобы гарантировать глобальную упорядоченность для двух блокировок a и b одного уровня блокировки:
template multilock(a, b: ptr TLock; body: untyped) =
if cast[ByteAddress](a) < cast[ByteAddress](b):
pthread_mutex_lock(a)
pthread_mutex_lock(b)
else:
pthread_mutex_lock(b)
pthread_mutex_lock(a)
{.locks: [a, b].}:
try:
body
finally:
pthread_mutex_unlock(a)
pthread_mutex_unlock(b)
Целые процедуры также могут быть аннотированы директивой locks, которая принимает уровень блокировки. Это означает, что процедура может приобретать блокировки до этого уровня. Это важно для того, чтобы процедуры можно было вызывать в разделе locks.
proc p() {.locks: 3.} = discard
var a: TLock[4]
{.locks: [a].}:
# p's locklevel (3) is strictly less than a's (4) so the call is allowed:
p()
Как обычно, locks — это выводимый эффект, и существует отношение подтипа: proc () {.locks: N.} является подтипом proc () {.locks: M.} тогда и только тогда, когда (M <= N).
Директива locks также может принимать специальное значение "unknown". Это полезно в контексте динамического диспетчеризации методов. В следующем примере компилятор может вывести уровень блокировки 0 для случая base. Однако один из перегруженных методов вызывает procvar, который потенциально блокирует. Таким образом, уровень блокировки вызова g.testMethod не может быть статически выведен, что приводит к предупреждениям компилятора. Используя {.locks: "unknown".}, базовый метод можно явно пометить как имеющий неизвестный уровень блокировки:
type SomeBase* = ref object of RootObj
type SomeDerived* = ref object of SomeBase
memberProc*: proc ()
method testMethod(g: SomeBase) {.base, locks: "unknown".} = discard
method testMethod(g: SomeDerived) =
if g.memberProc != nil:
g.memberProc() Директива noRewrite
Макросы и шаблоны переписывания терминов в настоящее время жадные, и они будут переписывать до тех пор, пока существует соответствие. Не было способа гарантировать, что какое-то переписывание происходит только один раз, например, когда термин переписывается в тот же термин плюс дополнительное содержимое.
Директива noRewrite может фактически предотвратить дальнейшее переписывание помеченного кода, например, в данном примере echo("ab") будет переписан только один раз:
template pwnEcho{echo(x)}(x: expr) =
{.noRewrite.}: echo("pwned!")
echo "ab"
Директива noRewrite может быть полезна для управления рекурсией макросов переписывания терминов.
Режим загрязнения
Компилятор Nim и большая часть стандартной библиотеки поддерживают режим загрязнения. Строки ввода объявляются с помощью типа строки TaintedString, объявленного в модуле system.
Если режим загрязнения включён (через опцию командной строки --taintMode:on), это отдельный тип строки, который помогает обнаруживать ошибки валидации ввода:
echo "your name: " var name: TaintedString = stdin.readline # it is safe here to output the name without any input validation, so # we simply convert `name` to string to make the compiler happy: echo "hi, ", name.string
Если режим загрязнения выключен, то TaintedString просто является псевдонимом для string.
Ограничения алиасинга при передаче параметров
Примечание: Ограничения алиасинга в настоящее время не проверяются реализацией и должны быть более подробно проработаны.
«Алиасинг» здесь означает, что соответствующие места хранения перекрываются в памяти во время выполнения. «Параметр вывода» — это параметр типа var T, а параметр ввода — любой параметр, не являющийся параметром типа var.
- Два параметра вывода никогда не должны быть алиасами.
- Параметр ввода и параметр вывода не должны быть алиасами.
- Параметр вывода никогда не должен быть алиасом глобальной или локальной для потока переменной, на которую ссылается вызываемая процедура.
- Параметр ввода не должен быть алиасом глобальной или локальной для потока переменной, обновлённой вызываемой процедурой.
Одна из проблем с правилами 3 и 4 заключается в том, что они влияют на конкретные глобальные или локальные для потока переменные, но отслеживание эффектов Nim отслеживает только «не использует глобальную переменную» через .noSideEffect. Правила 3 и 4 также можно приблизить другим правилом:
- Глобальная или локальная для потока переменная (или расположение, полученное из такого расположения) может передаваться только в параметр процедуры
.noSideEffect.
Аннотация noalias
С версии 1.4 компилятора Nim существует аннотация .noalias для переменных и параметров. Она непосредственно отображается на ключевое слово C/C++ restrict и означает, что указатель подразумевает единственное расположение в памяти; другие псевдонимы этого расположения отсутствуют. Не проверяется, соблюдается ли это ограничение алиасинга; если ограничение нарушено, оптимизатор бэкенда свободно может скомпилировать код неправильно. Это небезопасная функция языка.
В идеале в последующих версиях языка это ограничение будет проверяться во время компиляции. (Поэтому и было выбрано имя noalias вместо более громоздкого названия, такого как unsafeAssumeNoAlias.)
Строгие функции
С версии 1.4 доступно более строгое определение «побочного эффекта». В дополнение к уже существующему правилу, что побочный эффект — это вызов функции с побочными эффектами, также выполняется следующее правило:
Любое изменение объекта считается побочным эффектом, если этот объект доступен через параметр, не объявленный как параметр var.
Например:
{.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) =
let m = n # is the statement that connected the mutation to the parameter
m.data = "yeah" # the mutation is here
# Error: 'mut' can have side effects
# an object reachable from 'n' is potentially mutated
Алгоритм этого анализа описан в разделе алгоритм типов представления.
Типы представлений
Примечание: --experimental:views эффективнее с --experimental:strictFuncs.
Тип представления — это тип, который содержит или включает один из следующих типов:
-
var T(изменяемое представление вT) -
lent T(неизменяемое представление вT) -
openArray[T](пара (указатель на массив изT, размер))
Например:
type View1 = var int View2 = openArray[byte] View3 = lent string View4 = Table[openArray[char], int]
Исключения из этого правила — типы, созданные с помощью ptr или proc . Например, следующие типы не являются типами представлений:
type NotView1 = proc (x: openArray[int]) NotView2 = ptr openArray[char] NotView3 = ptr array[4, var int]
Изменяемый тип представления — это тип, который содержит или включает тип var T . Неизменяемый тип представления — это тип представления, который не является изменяемым типом представления.
Представление — это символ (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,unsafeAddr e— это выражение пути. - Преобразование типа
T(e)— это выражение пути. - Выражение приведения типа
cast[T](e)— это выражение пути. -
f(e, ...)— это выражение пути, если возвращаемый типfявляется типом представления. Поскольку представление может быть заимствовано только изe, мы знаем, что владельцемf(e, ...)являетсяe.
Если тип представления используется в качестве возвращаемого типа, расположение должно заимствовать из расположения, полученного из первого параметра, переданного процедуре. Подробности о том, как это делается для var T , см. в https://nim-lang.org/docs/manual.html#procedures-var-return-type.
Изменяемое представление может заимствовать из изменяемого расположения, неизменяемое представление может заимствовать из изменяемого или неизменяемого расположения.
Продолжительность займа — это отрезок команд, начинающийся с присваивания представлению и заканчивающийся последним использованием представления.
В течение операции займа нельзя выполнять изменения в заимствованных расположениях, кроме как через потенциально изменяемое представление, которое заимствовало из расположения. Заимствованное расположение считается запечатанным в течение займа.
{.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.
Алгоритм
Следующий раздел — это описание алгоритма, используемого текущей реализацией. Алгоритм выполняет два обхода по АСТ процедуры или глобального раздела кода, использующего переменную представления. Итераций по фиксированной точке не выполняется, сложность анализа составляет O(N), где N — количество узлов АСТ.
Первый проход по АСТ вычисляет срок жизни каждой локальной переменной на основе понятия «абстрактного времени», в реализации это просто целое число, которое увеличивается для каждого посещенного узла.
На втором проходе вычисляется информация об «объектных графах» (underlying object "graphs"). Пусть 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— это изменяемое представление, аvиспользуется для фактического изменения заимствованного расположения, тоbдолжно быть изменяемым расположением. Примечание: если это не используется для изменения, то заимствование изменяемого представления из неизменяемого расположения разрешено! Это позволяет использовать многие важные конструкции, и это будет оправдано в предстоящем RFC. - В течение срока жизни
vG(b)может быть изменён толькоv(и только еслиv— это изменяемое представление). - Если
v— этоresult, тоbдолжно быть расположением, полученным из первого формального параметра или из константного расположения. - Представление не может использоваться для чтения или записи до присвоения.
© 2006–2021 Andreas Rumpf
Licensed under the MIT License.
https://nim-lang.org/docs/manual_experimental.html