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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue