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

@ -8,7 +8,8 @@
#
## 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
## algorithm.
## The used data structure is "union find" with path compression.
@ -23,12 +24,27 @@
## borrowed from locations that are connected to a graph
## that is mutated during the liveness of the cursor.
## (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 wordrecg import wNoSideEffect
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
SubgraphFlag = enum
@ -56,21 +72,24 @@ type
flags: set[VarFlag]
sym: PSym
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
param: PSym
mutatedHere, connectedVia: TLineInfo
flags: set[SubgraphFlag]
maxMutation, minConnection: int
mutations: seq[int]
maxMutation, minConnection: AbstractTime
mutations: seq[AbstractTime]
Partitions* = object
abstractTime: int
abstractTime: AbstractTime
s: seq[VarIndex]
graphs: seq[MutationInfo]
unanalysableMutation, performCursorInference: bool
inAsgnSource, inConstructor, inNoSideEffectSection: int
owner: PSym
config: ConfigRef
proc mutationAfterConnection(g: MutationInfo): bool {.inline.} =
#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) =
if n.kind == nkSym:
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) =
assert n.kind == nkSym
@ -118,7 +137,7 @@ proc registerParam(c: var Partitions; n: PNode) =
aliveStart: c.abstractTime, aliveEnd: c.abstractTime)
c.graphs.add MutationInfo(param: n.sym, mutatedHere: unknownLineInfo,
connectedVia: unknownLineInfo, flags: {connectsConstParam},
maxMutation: -1, minConnection: high(int),
maxMutation: MinTime, minConnection: MaxTime,
mutations: @[])
else:
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.graphs.add MutationInfo(param: if isConstParam(s): s else: nil, mutatedHere: info,
connectedVia: unknownLineInfo, flags: {isMutated},
maxMutation: v.abstractTime, minConnection: high(int),
maxMutation: v.abstractTime, minConnection: MaxTime,
mutations: @[v.abstractTime])
of isRootOf:
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:
var rbFlags: set[SubgraphFlag] = {}
var mutatedHere = unknownLineInfo
var mut = 0
var mut = AbstractTime 0
var con = v.abstractTime
var gb: ptr MutationInfo = nil
if v.s[rb].con.kind == isRootOf:
@ -223,6 +242,80 @@ proc connect(v: var Partitions; a, b: PSym; info: TLineInfo) =
else:
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) =
case n.kind
of nkSym:
@ -385,13 +478,57 @@ proc pretendOwnsData(c: var Partitions, s: PSym) =
const
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) =
if not c.performCursorInference:
trackBorrow(c, dest, src)
var targets, sources: seq[PSym]
allRoots(dest, targets)
allRoots(src, sources)
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:
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: 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:
traverse(c, n[0])
when false:
@ -614,8 +758,8 @@ proc computeLiveRanges(c: var Partitions; n: PNode) =
else:
for child in n: computeLiveRanges(c, child)
proc computeGraphPartitions*(s: PSym; n: PNode; cursorInference = false): Partitions =
result = Partitions(performCursorInference: cursorInference)
proc computeGraphPartitions*(s: PSym; n: PNode; config: ConfigRef; cursorInference = false): Partitions =
result = Partitions(performCursorInference: cursorInference, owner: s, config: config)
if s.kind notin {skModule, skMacro}:
let params = s.typ.n
for i in 1..<params.len:
@ -624,8 +768,8 @@ proc computeGraphPartitions*(s: PSym; n: PNode; cursorInference = false): Partit
registerResult(result, s.ast[resultPos])
computeLiveRanges(result, n)
# resart the timer for the second pass:
result.abstractTime = 0
# restart the timer for the second pass:
result.abstractTime = AbstractTime 0
traverse(result, n)
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) =
for i in 0 ..< par.s.len:
let s = par.s[i].sym
if s.kind != skParam and isViewType(s.typ):
let v = par.s[i].sym
if v.kind != skParam and classifyViewType(v.typ) != noView:
let rid = root(par, i)
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])
if rid >= 0:
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) =
var par = computeGraphPartitions(s, n, true)
var par = computeGraphPartitions(s, n, config, true)
for i in 0 ..< par.s.len:
let v = addr(par.s[i])
if v.flags * {ownsData, preventCursor} == {} and v.sym.kind notin {skParam, skResult} and