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:
parent
f8866598e7
commit
4058801607
11 changed files with 446 additions and 66 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}))
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
9
tests/views/tmust_borrow_from_first_parameter.nim
Normal file
9
tests/views/tmust_borrow_from_first_parameter.nim
Normal 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
|
||||||
|
|
@ -23,9 +23,8 @@ 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
|
||||||
dec(splits)
|
dec(splits)
|
||||||
inc(last)
|
inc(last)
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue