spec for view types (#15424)

* spec for view types
* spec additions
* refactoring; there are two different kinds of views
* refactorings and spec additions
* enforce that view types are initialized
* enforce borrowing from the first formal parameter
* enforce lifetimes for borrowing of locals
* typo in the manual
* clarify in the implementation what a borrow operation really is
This commit is contained in:
Andreas Rumpf 2020-09-29 23:42:38 +02:00 • committed by GitHub
commit 4058801607
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 446 additions and 66 deletions

View file

@ -1403,9 +1403,6 @@ proc copyType*(t: PType, owner: PSym, keepId: bool): PType =
proc exactReplica*(t: PType): PType = copyType(t, t.owner, true) proc exactReplica*(t: PType): PType = copyType(t, t.owner, true)
template requiresInit*(t: PType): bool =
t.flags * {tfRequiresInit, tfNotNil} != {}
proc copySym*(s: PSym): PSym = proc copySym*(s: PSym): PSym =
result = newSym(s.kind, s.name, s.owner, s.info, s.options) result = newSym(s.kind, s.name, s.owner, s.info, s.options)
#result.ast = nil # BUGFIX; was: s.ast which made problems #result.ast = nil # BUGFIX; was: s.ast which made problems

View file

@ -1601,7 +1601,7 @@ proc takeImplicitAddr(c: PContext, n: PNode; isLent: bool): PNode =
proc asgnToResultVar(c: PContext, n, le, ri: PNode) {.inline.} = proc asgnToResultVar(c: PContext, n, le, ri: PNode) {.inline.} =
if le.kind == nkHiddenDeref: if le.kind == nkHiddenDeref:
var x = le[0] var x = le[0]
if (x.typ.kind in {tyVar, tyLent} or isViewType(x.typ)) and x.kind == nkSym and x.sym.kind == skResult: if (x.typ.kind in {tyVar, tyLent} or classifyViewType(x.typ) != noView) and x.kind == nkSym and x.sym.kind == skResult:
n[0] = x # 'result[]' --> 'result' n[0] = x # 'result[]' --> 'result'
n[1] = takeImplicitAddr(c, ri, x.typ.kind == tyLent) n[1] = takeImplicitAddr(c, ri, x.typ.kind == tyLent)
x.typ.flags.incl tfVarIsPtr x.typ.flags.incl tfVarIsPtr
@ -1712,8 +1712,8 @@ proc semAsgn(c: PContext, n: PNode; mode=asgnNormal): PNode =
if le == nil: if le == nil:
localError(c.config, a.info, "expression has no type") localError(c.config, a.info, "expression has no type")
elif (skipTypes(le, {tyGenericInst, tyAlias, tySink}).kind notin {tyVar} and elif (skipTypes(le, {tyGenericInst, tyAlias, tySink}).kind notin {tyVar} and
isAssignable(c, a) in {arNone, arLentValue}) or isAssignable(c, a) in {arNone, arLentValue}) or (
skipTypes(le, abstractVar).kind in {tyOpenArray, tyVarargs}: skipTypes(le, abstractVar).kind in {tyOpenArray, tyVarargs} and views notin c.features):
# Direct assignment to a discriminant is allowed! # Direct assignment to a discriminant is allowed!
localError(c.config, a.info, errXCannotBeAssignedTo % localError(c.config, a.info, errXCannotBeAssignedTo %
renderTree(a, {renderNoComments})) renderTree(a, {renderNoComments}))

View file

@ -10,7 +10,7 @@
import import
intsets, ast, astalgo, msgs, renderer, magicsys, types, idents, trees, intsets, ast, astalgo, msgs, renderer, magicsys, types, idents, trees,
wordrecg, strutils, options, guards, lineinfos, semfold, semdata, wordrecg, strutils, options, guards, lineinfos, semfold, semdata,
modulegraphs, varpartitions modulegraphs, varpartitions, typeallowed
when defined(useDfa): when defined(useDfa):
import dfa import dfa
@ -1244,7 +1244,7 @@ proc trackProc*(c: PContext; s: PSym, body: PNode) =
var mutationInfo = MutationInfo() var mutationInfo = MutationInfo()
if {strictFuncs, views} * c.features != {}: if {strictFuncs, views} * c.features != {}:
var partitions = computeGraphPartitions(s, body) var partitions = computeGraphPartitions(s, body, g.config)
if not t.hasSideEffect and t.hasDangerousAssign: if not t.hasSideEffect and t.hasDangerousAssign:
t.hasSideEffect = varpartitions.hasSideEffect(partitions, mutationInfo) t.hasSideEffect = varpartitions.hasSideEffect(partitions, mutationInfo)
if views in c.features: if views in c.features:

View file

@ -187,42 +187,77 @@ proc typeAllowed*(t: PType, kind: TSymKind; c: PContext; flags: TTypeAllowedFlag
var marker = initIntSet() var marker = initIntSet()
result = typeAllowedAux(marker, t, kind, c, flags) result = typeAllowedAux(marker, t, kind, c, flags)
proc isViewTypeAux(marker: var IntSet, t: PType): bool type
ViewTypeKind* = enum
noView, immutableView, mutableView
proc isViewTypeNode(marker: var IntSet, n: PNode): bool = proc combine(dest: var ViewTypeKind, b: ViewTypeKind) {.inline.} =
case dest
of noView, mutableView:
dest = b
of immutableView:
if b == mutableView: dest = b
proc classifyViewTypeAux(marker: var IntSet, t: PType): ViewTypeKind
proc classifyViewTypeNode(marker: var IntSet, n: PNode): ViewTypeKind =
case n.kind case n.kind
of nkSym: of nkSym:
result = isViewTypeAux(marker, n.typ) result = classifyViewTypeAux(marker, n.typ)
of nkOfBranch: of nkOfBranch:
result = isViewTypeNode(marker, n.lastSon) result = classifyViewTypeNode(marker, n.lastSon)
else: else:
result = noView
for child in n: for child in n:
result = isViewTypeNode(marker, child) result.combine classifyViewTypeNode(marker, child)
if result: break if result == mutableView: break
proc isViewTypeAux(marker: var IntSet, t: PType): bool = proc classifyViewTypeAux(marker: var IntSet, t: PType): ViewTypeKind =
if containsOrIncl(marker, t.id): return false if containsOrIncl(marker, t.id): return noView
case t.kind case t.kind
of tyVar, tyLent, tyVarargs, tyOpenArray: of tyVar:
result = true result = mutableView
of tyLent, tyVarargs, tyOpenArray:
result = immutableView
of tyGenericInst, tyDistinct, tyAlias, tyInferred, tySink, tyOwned, of tyGenericInst, tyDistinct, tyAlias, tyInferred, tySink, tyOwned,
tyUncheckedArray, tySequence, tyArray, tyRef, tyStatic, tyFromExpr: tyUncheckedArray, tySequence, tyArray, tyRef, tyStatic:
result = isViewTypeAux(marker, lastSon(t)) result = classifyViewTypeAux(marker, lastSon(t))
of tyFromExpr:
if t.len > 0:
result = classifyViewTypeAux(marker, lastSon(t))
else:
result = noView
of tyTuple: of tyTuple:
result = noView
for i in 0..<t.len: for i in 0..<t.len:
result = isViewTypeAux(marker, t[i]) result.combine classifyViewTypeAux(marker, t[i])
if result: break if result == mutableView: break
of tyObject: of tyObject:
result = false result = noView
if t.n != nil: if t.n != nil:
result = isViewTypeNode(marker, t.n) result = classifyViewTypeNode(marker, t.n)
if t[0] != nil: if t[0] != nil:
result = result or isViewTypeAux(marker, t[0]) result.combine classifyViewTypeAux(marker, t[0])
else: else:
# it doesn't matter what these types contain, 'ptr openArray' is not a # it doesn't matter what these types contain, 'ptr openArray' is not a
# view type! # view type!
result = false result = noView
proc isViewType*(t: PType): bool = proc classifyViewType*(t: PType): ViewTypeKind =
var marker = initIntSet() var marker = initIntSet()
result = isViewTypeAux(marker, t) result = classifyViewTypeAux(marker, t)
proc directViewType*(t: PType): ViewTypeKind =
# does classify 't' without looking recursively into 't'.
case t.kind
of tyVar:
result = mutableView
of tyLent, tyOpenArray:
result = immutableView
of abstractInst-{tyTypeDesc}:
result = directViewType(t.lastSon)
else:
result = noView
proc requiresInit*(t: PType): bool =
(t.flags * {tfRequiresInit, tfNotNil} != {}) or classifyViewType(t) != noView

View file

@ -8,7 +8,8 @@
# #
## Partition variables into different graphs. Used for ## Partition variables into different graphs. Used for
## Nim's write tracking and also for the cursor inference. ## Nim's write tracking, borrow checking and also for the
## cursor inference.
## The algorithm is a reinvention / variation of Steensgaard's ## The algorithm is a reinvention / variation of Steensgaard's
## algorithm. ## algorithm.
## The used data structure is "union find" with path compression. ## The used data structure is "union find" with path compression.
@ -23,12 +24,27 @@
## borrowed from locations that are connected to a graph ## borrowed from locations that are connected to a graph
## that is mutated during the liveness of the cursor. ## that is mutated during the liveness of the cursor.
## (We track all possible mutations of a graph.) ## (We track all possible mutations of a graph.)
##
## See https://nim-lang.github.io/Nim/manual_experimental.html#view-types-algorithm
## for a high-level description of how borrow checking works.
import ast, types, lineinfos, options, msgs, renderer import ast, types, lineinfos, options, msgs, renderer, typeallowed
from trees import getMagic, whichPragma from trees import getMagic, whichPragma
from wordrecg import wNoSideEffect from wordrecg import wNoSideEffect
from isolation_check import canAlias from isolation_check import canAlias
from typeallowed import isViewType
type
AbstractTime = distinct int
const
MaxTime = AbstractTime high(int)
MinTime = AbstractTime(-1)
proc `<=`(a, b: AbstractTime): bool {.borrow.}
proc `<`(a, b: AbstractTime): bool {.borrow.}
proc inc(x: var AbstractTime; diff = 1) {.borrow.}
proc dec(x: var AbstractTime; diff = 1) {.borrow.}
type type
SubgraphFlag = enum SubgraphFlag = enum
@ -56,21 +72,24 @@ type
flags: set[VarFlag] flags: set[VarFlag]
sym: PSym sym: PSym
reassignedTo: int reassignedTo: int
aliveStart, aliveEnd: int # the range for which the variable is alive. aliveStart, aliveEnd: AbstractTime # the range for which the variable is alive.
borrowsFrom: seq[int] # indexes into Partitions.s
MutationInfo* = object MutationInfo* = object
param: PSym param: PSym
mutatedHere, connectedVia: TLineInfo mutatedHere, connectedVia: TLineInfo
flags: set[SubgraphFlag] flags: set[SubgraphFlag]
maxMutation, minConnection: int maxMutation, minConnection: AbstractTime
mutations: seq[int] mutations: seq[AbstractTime]
Partitions* = object Partitions* = object
abstractTime: int abstractTime: AbstractTime
s: seq[VarIndex] s: seq[VarIndex]
graphs: seq[MutationInfo] graphs: seq[MutationInfo]
unanalysableMutation, performCursorInference: bool unanalysableMutation, performCursorInference: bool
inAsgnSource, inConstructor, inNoSideEffectSection: int inAsgnSource, inConstructor, inNoSideEffectSection: int
owner: PSym
config: ConfigRef
proc mutationAfterConnection(g: MutationInfo): bool {.inline.} = proc mutationAfterConnection(g: MutationInfo): bool {.inline.} =
#echo g.maxMutation, " ", g.minConnection, " ", g.param #echo g.maxMutation, " ", g.minConnection, " ", g.param
@ -108,7 +127,7 @@ proc variableId(c: Partitions; x: PSym): int =
proc registerResult(c: var Partitions; n: PNode) = proc registerResult(c: var Partitions; n: PNode) =
if n.kind == nkSym: if n.kind == nkSym:
c.s.add VarIndex(con: Connection(kind: isEmptyRoot), sym: n.sym, reassignedTo: 0, c.s.add VarIndex(con: Connection(kind: isEmptyRoot), sym: n.sym, reassignedTo: 0,
aliveStart: high(int), aliveEnd: c.abstractTime) aliveStart: MaxTime, aliveEnd: c.abstractTime)
proc registerParam(c: var Partitions; n: PNode) = proc registerParam(c: var Partitions; n: PNode) =
assert n.kind == nkSym assert n.kind == nkSym
@ -118,7 +137,7 @@ proc registerParam(c: var Partitions; n: PNode) =
aliveStart: c.abstractTime, aliveEnd: c.abstractTime) aliveStart: c.abstractTime, aliveEnd: c.abstractTime)
c.graphs.add MutationInfo(param: n.sym, mutatedHere: unknownLineInfo, c.graphs.add MutationInfo(param: n.sym, mutatedHere: unknownLineInfo,
connectedVia: unknownLineInfo, flags: {connectsConstParam}, connectedVia: unknownLineInfo, flags: {connectsConstParam},
maxMutation: -1, minConnection: high(int), maxMutation: MinTime, minConnection: MaxTime,
mutations: @[]) mutations: @[])
else: else:
c.s.add VarIndex(con: Connection(kind: isEmptyRoot), sym: n.sym, reassignedTo: 0, c.s.add VarIndex(con: Connection(kind: isEmptyRoot), sym: n.sym, reassignedTo: 0,
@ -152,7 +171,7 @@ proc potentialMutation(v: var Partitions; s: PSym; info: TLineInfo) =
v.s[r].con = Connection(kind: isRootOf, graphIndex: v.graphs.len) v.s[r].con = Connection(kind: isRootOf, graphIndex: v.graphs.len)
v.graphs.add MutationInfo(param: if isConstParam(s): s else: nil, mutatedHere: info, v.graphs.add MutationInfo(param: if isConstParam(s): s else: nil, mutatedHere: info,
connectedVia: unknownLineInfo, flags: {isMutated}, connectedVia: unknownLineInfo, flags: {isMutated},
maxMutation: v.abstractTime, minConnection: high(int), maxMutation: v.abstractTime, minConnection: MaxTime,
mutations: @[v.abstractTime]) mutations: @[v.abstractTime])
of isRootOf: of isRootOf:
let g = addr v.graphs[v.s[r].con.graphIndex] let g = addr v.graphs[v.s[r].con.graphIndex]
@ -192,7 +211,7 @@ proc connect(v: var Partitions; a, b: PSym; info: TLineInfo) =
# for now we always make 'rb' the slave and 'ra' the master: # for now we always make 'rb' the slave and 'ra' the master:
var rbFlags: set[SubgraphFlag] = {} var rbFlags: set[SubgraphFlag] = {}
var mutatedHere = unknownLineInfo var mutatedHere = unknownLineInfo
var mut = 0 var mut = AbstractTime 0
var con = v.abstractTime var con = v.abstractTime
var gb: ptr MutationInfo = nil var gb: ptr MutationInfo = nil
if v.s[rb].con.kind == isRootOf: if v.s[rb].con.kind == isRootOf:
@ -223,6 +242,80 @@ proc connect(v: var Partitions; a, b: PSym; info: TLineInfo) =
else: else:
assert false, "cannot happen" assert false, "cannot happen"
proc borrowFromConstExpr(n: PNode): bool =
case n.kind
of nkCharLit..nkNilLit:
result = true
of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv,
nkCast, nkObjUpConv, nkObjDownConv:
result = borrowFromConstExpr(n.lastSon)
of nkCurly, nkBracket, nkPar, nkTupleConstr, nkObjConstr, nkClosure, nkRange:
result = true
for i in ord(n.kind == nkObjConstr)..<n.len:
if not borrowFromConstExpr(n[i]): return false
of nkCallKinds:
if getMagic(n) == mArrToSeq:
result = true
for i in 1..<n.len:
if not borrowFromConstExpr(n[i]): return false
else: discard
proc pathExpr(node: PNode; owner: PSym): PNode =
#[ From the spec:
- ``source`` itself is a path expression.
- Container access like ``e[i]`` is a path expression.
- Tuple access ``e[0]`` is a path expression.
- Object field access ``e.field`` is a path expression.
- ``system.toOpenArray(e, ...)`` is a path expression.
- Pointer dereference ``e[]`` is a path expression.
- An address ``addr e``, ``unsafeAddr e`` is a path expression.
- A type conversion ``T(e)`` is a path expression.
- A cast expression ``cast[T](e)`` is a path expression.
- ``f(e, ...)`` is a path expression if ``f``'s return type is a view type.
Because the view can only have been borrowed from ``e``, we then know
that owner of ``f(e, ...)`` is ``e``.
Returns the owner of the path expression. Returns ``nil``
if it is not a valid path expression.
]#
var n = node
result = nil
while true:
case n.kind
of nkSym:
case n.sym.kind
of skParam, skTemp, skResult, skForVar:
if n.sym.owner == owner: result = n
of skVar:
if n.sym.owner == owner or sfThread in n.sym.flags: result = n
of skLet, skConst:
if n.sym.owner == owner or {sfThread, sfGlobal} * n.sym.flags != {}:
result = n
else:
discard
break
of nkDotExpr, nkDerefExpr, nkBracketExpr, nkHiddenDeref,
nkCheckedFieldExpr, nkAddr, nkHiddenAddr:
n = n[0]
of nkHiddenStdConv, nkHiddenSubConv, nkConv, nkCast,
nkObjUpConv, nkObjDownConv:
n = n.lastSon
of nkCallKinds:
if n.len > 1:
if (n.typ != nil and classifyViewType(n.typ) != noView) or getMagic(n) == mSlice:
n = n[1]
else:
break
else:
break
else:
break
# borrowFromConstExpr(n) is correct here because we need 'node'
# stripped off the path suffixes:
if result == nil and borrowFromConstExpr(n):
result = n
proc allRoots(n: PNode; result: var seq[PSym]; followDotExpr = true) = proc allRoots(n: PNode; result: var seq[PSym]; followDotExpr = true) =
case n.kind case n.kind
of nkSym: of nkSym:
@ -385,13 +478,57 @@ proc pretendOwnsData(c: var Partitions, s: PSym) =
const const
explainCursors = false explainCursors = false
proc borrowFrom(c: var Partitions; dest: PSym; src: PNode) =
const
url = "; see https://nim-lang.github.io/Nim/manual_experimental.html#view-types-algorithm-path-expressions for details"
let s = pathExpr(src, c.owner)
if s == nil:
localError(c.config, src.info, "cannot borrow from " & $src & ", it is not a path expression; " & url)
elif s.kind == nkSym:
if dest.kind == skResult:
if s.sym.kind != skParam or s.sym.position != 0:
localError(c.config, src.info, "'result' must borrow from the first parameter")
let vid = variableId(c, dest)
if vid >= 0:
var sourceIdx = variableId(c, s.sym)
if sourceIdx < 0:
sourceIdx = c.s.len
c.s.add VarIndex(con: Connection(kind: isEmptyRoot), sym: s.sym, reassignedTo: 0,
aliveStart: MinTime, aliveEnd: MaxTime)
c.s[vid].borrowsFrom.add sourceIdx
else:
discard "a valid borrow location that is a deeply constant expression so we have nothing to track"
proc borrowingCall(c: var Partitions; destType: PType; n: PNode; i: int) =
let v = pathExpr(n[i], c.owner)
if v.kind == nkSym:
for j in i+1..<n.len:
if getMagic(n[j]) == mSlice:
borrowFrom(c, v.sym, n[j])
else:
localError(c.config, n[i].info, "cannot determine the target of the borrow")
proc trackBorrow(c: var Partitions; dest, src: PNode) =
if dest.kind == nkSym:
let vk = directViewType(dest.typ)
if vk != noView:
borrowFrom(c, dest.sym, src)
proc deps(c: var Partitions; dest, src: PNode) = proc deps(c: var Partitions; dest, src: PNode) =
if not c.performCursorInference:
trackBorrow(c, dest, src)
var targets, sources: seq[PSym] var targets, sources: seq[PSym]
allRoots(dest, targets) allRoots(dest, targets)
allRoots(src, sources) allRoots(src, sources)
proc wrap(t: PType): bool {.nimcall.} = t.kind in {tyRef, tyPtr} proc wrap(t: PType): bool {.nimcall.} = t.kind in {tyRef, tyPtr}
let destIsComplex = types.searchTypeFor(dest.typ, wrap) or isViewType(dest.typ)
let destIsComplex = types.searchTypeFor(dest.typ, wrap)
for t in targets: for t in targets:
if dest.kind != nkSym and c.inNoSideEffectSection == 0: if dest.kind != nkSym and c.inNoSideEffectSection == 0:
@ -483,6 +620,13 @@ proc traverse(c: var Partitions; n: PNode) =
for r in roots: potentialMutation(c, r, it.info) for r in roots: potentialMutation(c, r, it.info)
for r in roots: noCursor(c, r) for r in roots: noCursor(c, r)
if not c.performCursorInference:
# a call like 'result.add toOpenArray()' can also be a borrow
# operation. We know 'paramType' is a tyVar and we really care if
# 'paramType[0]' is still a view type, this is not a typo!
if directViewType(paramType[0]) == noView and classifyViewType(paramType[0]) != noView:
borrowingCall(c, paramType[0], n, i)
of nkAddr, nkHiddenAddr: of nkAddr, nkHiddenAddr:
traverse(c, n[0]) traverse(c, n[0])
when false: when false:
@ -614,8 +758,8 @@ proc computeLiveRanges(c: var Partitions; n: PNode) =
else: else:
for child in n: computeLiveRanges(c, child) for child in n: computeLiveRanges(c, child)
proc computeGraphPartitions*(s: PSym; n: PNode; cursorInference = false): Partitions = proc computeGraphPartitions*(s: PSym; n: PNode; config: ConfigRef; cursorInference = false): Partitions =
result = Partitions(performCursorInference: cursorInference) result = Partitions(performCursorInference: cursorInference, owner: s, config: config)
if s.kind notin {skModule, skMacro}: if s.kind notin {skModule, skMacro}:
let params = s.typ.n let params = s.typ.n
for i in 1..<params.len: for i in 1..<params.len:
@ -624,8 +768,8 @@ proc computeGraphPartitions*(s: PSym; n: PNode; cursorInference = false): Partit
registerResult(result, s.ast[resultPos]) registerResult(result, s.ast[resultPos])
computeLiveRanges(result, n) computeLiveRanges(result, n)
# resart the timer for the second pass: # restart the timer for the second pass:
result.abstractTime = 0 result.abstractTime = AbstractTime 0
traverse(result, n) traverse(result, n)
proc dangerousMutation(g: MutationInfo; v: VarIndex): bool = proc dangerousMutation(g: MutationInfo; v: VarIndex): bool =
@ -653,14 +797,24 @@ proc cannotBorrow(config: ConfigRef; s: PSym; g: MutationInfo) =
proc checkBorrowedLocations*(par: var Partitions; body: PNode; config: ConfigRef) = proc checkBorrowedLocations*(par: var Partitions; body: PNode; config: ConfigRef) =
for i in 0 ..< par.s.len: for i in 0 ..< par.s.len:
let s = par.s[i].sym let v = par.s[i].sym
if s.kind != skParam and isViewType(s.typ): if v.kind != skParam and classifyViewType(v.typ) != noView:
let rid = root(par, i) let rid = root(par, i)
if par.s[rid].con.kind == isRootOf and dangerousMutation(par.graphs[par.s[rid].con.graphIndex], par.s[i]): if rid >= 0:
cannotBorrow(config, s, par.graphs[par.s[rid].con.graphIndex]) for b in par.s[rid].borrowsFrom:
let sid = root(par, b)
if sid >= 0:
if par.s[sid].con.kind == isRootOf and dangerousMutation(par.graphs[par.s[sid].con.graphIndex], par.s[i]):
cannotBorrow(config, v, par.graphs[par.s[sid].con.graphIndex])
if par.s[sid].sym.kind != skParam and par.s[sid].aliveEnd < par.s[rid].aliveEnd:
localError(config, v.info, "'" & v.name.s & "' borrows from location '" & par.s[sid].sym.name.s &
"' which does not live long enough")
#if par.s[rid].con.kind == isRootOf and dangerousMutation(par.graphs[par.s[rid].con.graphIndex], par.s[i]):
# cannotBorrow(config, s, par.graphs[par.s[rid].con.graphIndex])
proc computeCursors*(s: PSym; n: PNode; config: ConfigRef) = proc computeCursors*(s: PSym; n: PNode; config: ConfigRef) =
var par = computeGraphPartitions(s, n, true) var par = computeGraphPartitions(s, n, config, true)
for i in 0 ..< par.s.len: for i in 0 ..< par.s.len:
let v = addr(par.s[i]) let v = addr(par.s[i])
if v.flags * {ownsData, preventCursor} == {} and v.sym.kind notin {skParam, skResult} and if v.flags * {ownsData, preventCursor} == {} and v.sym.kind notin {skParam, skResult} and

View file

@ -655,7 +655,7 @@ Precedence level Operators First
================ ======================================================= ================== =============== ================ ======================================================= ================== ===============
Whether an operator is used a prefix operator is also affected by preceding Whether an operator is used as a prefix operator is also affected by preceding
whitespace (this parsing change was introduced with version 0.13.0): whitespace (this parsing change was introduced with version 0.13.0):
.. code-block:: nim .. code-block:: nim

View file

@ -1814,18 +1814,49 @@ For example:
# an object reachable from 'n' is potentially mutated # an object reachable from 'n' is potentially mutated
The algorithm behind this analysis is currently not documented. The algorithm behind this analysis is described in
the `view types section <#view-types-algorithm>`_.
View types View types
========== ==========
A view type is a type that contains one of the following types: **Note**: ``--experimental:views`` is more effective
with ``--experimental:strictFuncs``.
A view type is a type that is or contains one of the following types:
- ``var T`` (mutable view into ``T``) - ``var T`` (mutable view into ``T``)
- ``lent T`` (immutable view into ``T``) - ``lent T`` (immutable view into ``T``)
- ``openArray[T]`` (pair of (pointer to array of ``T``, size)) - ``openArray[T]`` (pair of (pointer to array of ``T``, size))
For example:
.. code-block:: nim
type
View1 = var int
View2 = openArray[byte]
View3 = lent string
View4 = Table[openArray[char], int]
Exceptions to this rule are types constructed via ``ptr`` or ``proc``.
For example, the following types are **not** view types:
.. code-block:: nim
type
NotView1 = proc (x: openArray[int])
NotView2 = ptr openArray[char]
NotView3 = ptr array[4, var int]
A *mutable* view type is a type that is or contains a ``var T`` type.
An *immutable* view type is a view type that is not a mutable view type.
A *view* is a symbol (a let, var, const, etc.) that has a view type.
Since version 1.4 Nim allows view types to be used as local variables. Since version 1.4 Nim allows view types to be used as local variables.
This feature needs to be enabled via ``{.experimental: "views".}``. This feature needs to be enabled via ``{.experimental: "views".}``.
@ -1860,10 +1891,159 @@ For example:
main(@[11, 22, 33]) main(@[11, 22, 33])
A local variable of a view type can borrow from a location
derived from a parameter, another local variable, a global ``const`` or ``let``
symbol or a thread-local ``var`` or ``let``.
If a view type is used as a return type, the location must borrow from the Let ``p`` the proc that is analysed for the correctness of the borrow operation.
first parameter that is passed to the proc.
Let ``source`` be one of:
- A formal parameter of ``p``. Note that this does not cover parameters of
inner procs.
- The ``result`` symbol of ``p``.
- A local ``var`` or ``let`` or ``const`` of ``p``. Note that this does
not cover locals of inner procs.
- A thread-local ``var`` or ``let``.
- A global ``let`` or ``const``.
- A constant arraq/seq/object/tuple constructor.
Path expressions
----------------
A location derived from ``source`` is then defined as a path expression that
has ``source`` as the owner. A path expression ``e`` is defined recursively:
- ``source`` itself is a path expression.
- Container access like ``e[i]`` is a path expression.
- Tuple access ``e[0]`` is a path expression.
- Object field access ``e.field`` is a path expression.
- ``system.toOpenArray(e, ...)`` is a path expression.
- Pointer dereference ``e[]`` is a path expression.
- An address ``addr e``, ``unsafeAddr e`` is a path expression.
- A type conversion ``T(e)`` is a path expression.
- A cast expression ``cast[T](e)`` is a path expression.
- ``f(e, ...)`` is a path expression if ``f``'s return type is a view type.
Because the view can only have been borrowed from ``e``, we then know
that owner of ``f(e, ...)`` is ``e``.
If a view type is used as a return type, the location must borrow from a location
that is derived from the first parameter that is passed to the proc.
See https://nim-lang.org/docs/manual.html#procedures-var-return-type for See https://nim-lang.org/docs/manual.html#procedures-var-return-type for
details about how this is done for ``var T``. details about how this is done for ``var T``.
The algorithm behind this analysis is currently not documented. A mutable view can borrow from a mutable location, an immutable view can borrow
from both a mutable or an immutable location.
The *duration* of a borrow is the span of commands beginning from the assignment
to the view and ending with the last usage of the view.
For the duration of the borrow operation, no mutations to the borrowed locations
may be performed except via the potentially mutable view that borrowed from the
location. The borrowed location is said to be *sealed* during the borrow.
.. code-block:: nim
{.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
The scope of the view does not matter:
.. code-block:: nim
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
The analysis requires as much precision about mutations as is reasonably obtainable,
so it is more effective with the experimental `strict funcs <#strict-funcs>`_
feature. In other words ``--experimental:views`` works better
with ``--experimental:strictFuncs``.
The analysis is currently control flow insensitive:
.. code-block:: nim
proc invalid(s: var seq[Obj]) =
let v: lent Obj = s[0]
if false:
s.setLen 0
echo v.field
In this example, the compiler assumes that ``s.setLen 0`` invalidates the
borrow operation of ``v`` even though a human being can easily see that it
will never do that at runtime.
Reborrows
---------
A view ``v`` can borrow from multiple different locations. However, the borrow
is always the full span of ``v``'s lifetime and every location that is borrowed
from is sealed during ``v``'s lifetime.
Algorithm
---------
The following section is an outline of the algorithm that the current implementation
uses. The algorithm performs two traversals over the AST of the procedure or global
section of code that uses a view variable. No fixpoint iterations are performed, the
complexity of the analysis is O(N) where N is the number of nodes of the AST.
The first pass over the AST computes the lifetime of each local variable based on
a notion of an "abstract time", in the implementation it's a simple integer that is
incremented for every visited node.
In the second pass information about the underlying object "graphs" is computed.
Let ``v`` be a parameter or a local variable. Let ``G(v)`` be the graph
that ``v`` belongs to. A graph is defined by the set of variables that belong
to the graph. Initially for all ``v``: ``G(v) = {v}``. Every variable can only
be part of a single graph.
Assignments like ``a = b`` "connect" two variables, both variables end up in the
same graph ``{a, b} = G(a) = G(b)``. Unfortunately, the pattern to look for is
much more complex than that and can involve multiple assignment targets
and sources::
f(x, y) = g(a, b)
connects ``x`` and ``y`` to ``a`` and ``b``: ``G(x) = G(y) = G(a) = G(b) = {x, y, a, b}``.
A type based alias analysis rules out some of these combinations, for example
a ``string`` value cannot possibly be connected to a ``seq[int]``.
A pattern like ``v[] = value`` or ``v.field = value`` marks ``G(v)`` as mutated.
After the second pass a set of disjoint graphs was computed.
For strict functions it is then enforced that there is no graph that is both mutated
and has an element that is an immutable parameter (that is a parameter that is not
of type ``var T``).
For borrow checking a different set of checks is performed. Let ``v`` be the view
and ``b`` the location that is borrowed from.
- The lifetime of ``v`` must not exceed ``b``'s lifetime. Note: The lifetime of
a parameter is the complete proc body.
- If ``v`` is a mutable view and ``v`` is used to actually mutate the
borrowed location, then ``b`` has to be a mutable location.
Note: If it is not actually used for mutation, borrowing a mutable view from an
immutable location is allowed! This allows for many important idioms and will be
justified in an upcoming RFC.
- During ``v``'s lifetime, ``G(b)`` can only be modified by ``v`` (and only if
``v`` is a mutable view).
- If ``v`` is ``result`` then ``b`` has to be a location derived from the first
formal parameter or from a constant location.
- A view cannot be used for a read or a write access before it was assigned to.

View file

@ -1,17 +1,22 @@
discard """ discard """
errormsg: "cannot borrow" errormsg: "cannot borrow"
nimout: '''tcannot_borrow.nim(16, 7) Error: cannot borrow meh; what it borrows from is potentially mutated nimout: '''tcannot_borrow.nim(21, 7) Error: cannot borrow meh; what it borrows from is potentially mutated
tcannot_borrow.nim(17, 3) the mutation is here tcannot_borrow.nim(22, 3) the mutation is here'''
tcannot_borrow.nim(16, 7) is the statement that connected the mutation to the parameter''' line: 21
line: 16
""" """
{.experimental: "views".} {.experimental: "views".}
type type
Foo = object Foo = object
field: string field: string
proc valid(s: var seq[Foo]) =
let v: lent Foo = s[0] # begin of borrow
echo v.field # end of borrow
s.setLen 0 # valid because 'v' isn't used afterwards
proc dangerous(s: var seq[Foo]) = proc dangerous(s: var seq[Foo]) =
let meh: lent Foo = s[0] let meh: lent Foo = s[0]
s.setLen 0 s.setLen 0

View file

@ -0,0 +1,9 @@
discard """
errormsg: "'result' must borrow from the first parameter"
line: 9
"""
{.experimental: "views".}
proc p(a, b: openArray[char]): openArray[char] =
result = b

View file

@ -23,7 +23,6 @@ proc split*(s: string, seps: set[char] = Whitespace,
while last < len(s) and s[last] notin seps: while last < len(s) and s[last] notin seps:
inc(last) inc(last)
if splits == 0: last = len(s) if splits == 0: last = len(s)
{.noSideEffect.}:
result.add toOpenArray(s, first, last-1) result.add toOpenArray(s, first, last-1)
result.add toOpenArray(s, first, last-1) result.add toOpenArray(s, first, last-1)
if splits == 0: break if splits == 0: break

View file

@ -4,6 +4,7 @@ discard """
33 33
3 3
2 2
3
3''' 3'''
targets: "c cpp" targets: "c cpp"
""" """
@ -22,6 +23,6 @@ proc main(s: seq[int]) =
take(x.toOpenArray(0, 1)) take(x.toOpenArray(0, 1))
let y = x let y = x
take y take y
take x
main(@[11, 22, 33]) main(@[11, 22, 33])