ARC: cycle detector (#12823)

* first implementation of the =trace and =dispose hooks for the cycle collector
* a cycle collector for ARC: progress
* manual: the .acyclic pragma is a thing once again
* gcbench: adaptations for --gc:arc
* enable valgrind tests for the strutils tests
* testament: better valgrind support
* ARC refactoring: growable jumpstacks
* ARC cycle detector: non-recursive algorithm
* moved and renamed core/ files back to system/
* refactoring: --gc:arc vs --gc:orc since 'orc' is even more experimental and we want to ship --gc:arc soonish
This commit is contained in:
Andreas Rumpf 2019-12-17 17:37:50 +01:00 • committed by GitHub
commit 83a736a34a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 939 additions and 229 deletions

View file

@ -41,7 +41,8 @@ proc at(a, i: PNode, elemType: PType): PNode =
proc fillBodyTup(c: var TLiftCtx; t: PType; body, x, y: PNode) =
for i in 0..<t.len:
let lit = lowerings.newIntLit(c.g, x.info, i)
fillBody(c, t[i], body, x.at(lit, t[i]), y.at(lit, t[i]))
let b = if c.kind == attachedTrace: y else: y.at(lit, t[i])
fillBody(c, t[i], body, x.at(lit, t[i]), b)
proc dotField(x: PNode, f: PSym): PNode =
result = newNodeI(nkDotExpr, x.info, 2)
@ -58,18 +59,19 @@ proc newAsgnStmt(le, ri: PNode): PNode =
result[1] = ri
proc defaultOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
if c.kind != attachedDestructor:
if c.kind in {attachedAsgn, attachedDeepCopy, attachedSink}:
body.add newAsgnStmt(x, y)
proc fillBodyObj(c: var TLiftCtx; n, body, x, y: PNode) =
case n.kind
of nkSym:
let f = n.sym
let b = if c.kind == attachedTrace: y else: y.dotField(f)
if sfCursor in f.flags and f.typ.skipTypes(abstractInst).kind in {tyRef, tyProc} and
c.g.config.selectedGC in {gcDestructors, gcHooks}:
defaultOp(c, f.typ, body, x.dotField(f), y.dotField(f))
c.g.config.selectedGC in {gcArc, gcOrc, gcHooks}:
defaultOp(c, f.typ, body, x.dotField(f), b)
else:
fillBody(c, f.typ, body, x.dotField(f), y.dotField(f))
fillBody(c, f.typ, body, x.dotField(f), b)
of nkNilLit: discard
of nkRecCase:
if c.kind in {attachedSink, attachedAsgn, attachedDeepCopy}:
@ -117,13 +119,17 @@ proc genAddr(g: ModuleGraph; x: PNode): PNode =
result = newNodeIT(nkHiddenAddr, x.info, makeVarType(x.typ.owner, x.typ))
result.add x
proc newAsgnCall(g: ModuleGraph; op: PSym; x, y: PNode): PNode =
proc newHookCall(g: ModuleGraph; op: PSym; x, y: PNode): PNode =
#if sfError in op.flags:
# localError(c.config, x.info, "usage of '$1' is a user-defined error" % op.name.s)
result = newNodeI(nkCall, x.info)
result.add newSymNode(op)
result.add genAddr(g, x)
result.add y
if op.typ.sons[1].kind == tyVar:
result.add genAddr(g, x)
else:
result.add x
if y != nil:
result.add y
proc newOpCall(op: PSym; x: PNode): PNode =
result = newNodeIT(nkCall, x.info, op.typ[0])
@ -142,6 +148,10 @@ proc useNoGc(c: TLiftCtx; t: PType): bool {.inline.} =
result = optSeqDestructors in c.g.config.globalOptions and
({tfHasGCedMem, tfHasOwned} * t.flags != {} or t.isGCedMem)
proc requiresDestructor(c: TLiftCtx; t: PType): bool {.inline.} =
result = optSeqDestructors in c.g.config.globalOptions and
containsGarbageCollectedRef(t)
proc instantiateGeneric(c: var TLiftCtx; op: PSym; t, typeInst: PType): PSym =
if c.c != nil and typeInst != nil:
result = c.c.instTypeBoundOp(c.c, op, typeInst, c.info, attachedAsgn, 1)
@ -160,7 +170,7 @@ proc considerAsgnOrSink(c: var TLiftCtx; t: PType; body, x, y: PNode;
#else:
# markUsed(c.g.config, c.info, op, c.g.usageSym)
onUse(c.info, op)
body.add newAsgnCall(c.g, op, x, y)
body.add newHookCall(c.g, op, x, y)
result = true
elif tfHasAsgn in t.flags:
var op: PSym
@ -189,7 +199,7 @@ proc considerAsgnOrSink(c: var TLiftCtx; t: PType; body, x, y: PNode;
#debug(t)
#return false
assert op.ast[genericParamsPos].kind == nkEmpty
body.add newAsgnCall(c.g, op, x, y)
body.add newHookCall(c.g, op, x, y)
result = true
proc addDestructorCall(c: var TLiftCtx; orig: PType; body, x: PNode) =
@ -202,7 +212,7 @@ proc addDestructorCall(c: var TLiftCtx; orig: PType; body, x: PNode) =
op = instantiateGeneric(c, op, t, t.typeInst)
t.attachedOps[attachedDestructor] = op
if op == nil and useNoGc(c, t):
if op == nil and (useNoGc(c, t) or requiresDestructor(c, t)):
op = produceSym(c.g, c.c, t, attachedDestructor, c.info)
doAssert op != nil
doAssert op == t.destructor
@ -231,10 +241,10 @@ proc considerUserDefinedOp(c: var TLiftCtx; t: PType; body, x, y: PNode): bool =
body.add destructorCall(c.g, op, x)
result = true
#result = addDestructorCall(c, t, body, x)
of attachedAsgn:
result = considerAsgnOrSink(c, t, body, x, y, t.assignment)
of attachedSink:
result = considerAsgnOrSink(c, t, body, x, y, t.asink)
of attachedAsgn, attachedSink, attachedTrace:
result = considerAsgnOrSink(c, t, body, x, y, t.attachedOps[c.kind])
of attachedDispose:
result = considerAsgnOrSink(c, t, body, x, nil, t.attachedOps[c.kind])
of attachedDeepCopy:
let op = t.attachedOps[attachedDeepCopy]
if op != nil:
@ -305,8 +315,8 @@ proc forallElements(c: var TLiftCtx; t: PType; body, x, y: PNode) =
let i = declareCounter(c, body, toInt64(firstOrd(c.g.config, t)))
let whileLoop = genWhileLoop(c, i, x)
let elemType = t.lastSon
fillBody(c, elemType, whileLoop[1], x.at(i, elemType),
y.at(i, elemType))
let b = if c.kind == attachedTrace: y else: y.at(i, elemType)
fillBody(c, elemType, whileLoop[1], x.at(i, elemType), b)
addIncStmt(c, whileLoop[1], i)
body.add whileLoop
@ -330,6 +340,11 @@ proc fillSeqOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
# destroy all elements:
forallElements(c, t, body, x, y)
body.add genBuiltin(c.g, mDestroy, "destroy", x)
of attachedTrace:
# follow all elements:
forallElements(c, t, body, x, y)
of attachedDispose:
body.add genBuiltin(c.g, mDestroy, "destroy", x)
proc useSeqOrStrOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
createTypeBoundOps(c.g, c.c, t, body.info)
@ -344,7 +359,7 @@ proc useSeqOrStrOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
case c.kind
of attachedAsgn, attachedDeepCopy:
doAssert t.assignment != nil
body.add newAsgnCall(c.g, t.assignment, x, y)
body.add newHookCall(c.g, t.assignment, x, y)
of attachedSink:
# we always inline the move for better performance:
let moveCall = genBuiltin(c.g, mMove, "move", x)
@ -355,10 +370,14 @@ proc useSeqOrStrOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
# alternatively we could do this:
when false:
doAssert t.asink != nil
body.add newAsgnCall(c.g, t.asink, x, y)
body.add newHookCall(c.g, t.asink, x, y)
of attachedDestructor:
doAssert t.destructor != nil
body.add destructorCall(c.g, t.destructor, x)
of attachedTrace:
body.add newHookCall(c.g, t.attachedOps[c.kind], x, y)
of attachedDispose:
body.add newHookCall(c.g, t.attachedOps[c.kind], x, nil)
proc fillStrOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
case c.kind
@ -370,8 +389,10 @@ proc fillStrOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
doAssert t.destructor != nil
moveCall.add destructorCall(c.g, t.destructor, x)
body.add moveCall
of attachedDestructor:
of attachedDestructor, attachedDispose:
body.add genBuiltin(c.g, mDestroy, "destroy", x)
of attachedTrace:
discard "strings are atomic and have no inner elements that are to trace"
proc atomicRefOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
var actions = newNodeI(nkStmtList, c.info)
@ -384,7 +405,18 @@ proc atomicRefOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
addDestructorCall(c, elemType, newNodeI(nkStmtList, c.info), genDeref(x, nkDerefExpr))
actions.add callCodegenProc(c.g, "nimDestroyAndDispose", c.info, x)
let cond = callCodegenProc(c.g, "nimDecRefIsLast", c.info, x)
let isCyclic = c.g.config.selectedGC == gcOrc and types.canFormAcycle(t)
var cond: PNode
if isCyclic:
if isFinal(elemType):
let typInfo = genBuiltin(c.g, mGetTypeInfo, "getTypeInfo", newNodeIT(nkType, x.info, elemType))
typInfo.typ = getSysType(c.g, c.info, tyPointer)
cond = callCodegenProc(c.g, "nimDecRefIsLastCyclicStatic", c.info, x, typInfo)
else:
cond = callCodegenProc(c.g, "nimDecRefIsLastCyclicDyn", c.info, x)
else:
cond = callCodegenProc(c.g, "nimDecRefIsLast", c.info, x)
cond.typ = getSysType(c.g, x.info, tyBool)
case c.kind
@ -392,7 +424,8 @@ proc atomicRefOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
body.add genIf(c, cond, actions)
body.add newAsgnStmt(x, y)
of attachedAsgn:
body.add genIf(c, y, callCodegenProc(c.g, "nimIncRef", c.info, y))
body.add genIf(c, y, callCodegenProc(c.g,
if isCyclic: "nimIncRefCyclic" else: "nimIncRef", c.info, y))
body.add genIf(c, cond, actions)
body.add newAsgnStmt(x, y)
of attachedDestructor:
@ -401,6 +434,22 @@ proc atomicRefOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
actions.add newAsgnStmt(x, newNodeIT(nkNilLit, body.info, t))
body.add genIf(c, cond, actions)
of attachedDeepCopy: assert(false, "cannot happen")
of attachedTrace:
if isFinal(elemType):
let typInfo = genBuiltin(c.g, mGetTypeInfo, "getTypeInfo", newNodeIT(nkType, x.info, elemType))
typInfo.typ = getSysType(c.g, c.info, tyPointer)
body.add callCodegenProc(c.g, "nimTraceRef", c.info, x, typInfo, y)
else:
# If the ref is polymorphic we have to account for this
body.add callCodegenProc(c.g, "nimTraceRefDyn", c.info, x, y)
of attachedDispose:
# this is crucial! dispose is like =destroy but we don't follow refs
# as that is dealt within the cycle collector.
when false:
let cond = copyTree(x)
cond.typ = getSysType(c.g, x.info, tyBool)
actions.add callCodegenProc(c.g, "nimRawDispose", c.info, x)
body.add genIf(c, cond, actions)
proc atomicClosureOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
## Closures are really like refs except they always use a virtual destructor
@ -411,7 +460,10 @@ proc atomicClosureOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
var actions = newNodeI(nkStmtList, c.info)
actions.add callCodegenProc(c.g, "nimDestroyAndDispose", c.info, xenv)
let cond = callCodegenProc(c.g, "nimDecRefIsLast", c.info, xenv)
let decRefProc =
if c.g.config.selectedGC == gcOrc: "nimDecRefIsLastCyclicDyn"
else: "nimDecRefIsLast"
let cond = callCodegenProc(c.g, decRefProc, c.info, xenv)
cond.typ = getSysType(c.g, x.info, tyBool)
case c.kind
@ -421,7 +473,10 @@ proc atomicClosureOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
of attachedAsgn:
let yenv = genBuiltin(c.g, mAccessEnv, "accessEnv", y)
yenv.typ = getSysType(c.g, c.info, tyPointer)
body.add genIf(c, yenv, callCodegenProc(c.g, "nimIncRef", c.info, yenv))
let incRefProc =
if c.g.config.selectedGC == gcOrc: "nimIncRefCyclic"
else: "nimIncRef"
body.add genIf(c, yenv, callCodegenProc(c.g, incRefProc, c.info, yenv))
body.add genIf(c, cond, actions)
body.add newAsgnStmt(x, y)
of attachedDestructor:
@ -430,6 +485,16 @@ proc atomicClosureOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
actions.add newAsgnStmt(xenv, newNodeIT(nkNilLit, body.info, xenv.typ))
body.add genIf(c, cond, actions)
of attachedDeepCopy: assert(false, "cannot happen")
of attachedTrace:
body.add callCodegenProc(c.g, "nimTraceRefDyn", c.info, xenv, y)
of attachedDispose:
# this is crucial! dispose is like =destroy but we don't follow refs
# as that is dealt within the cycle collector.
when false:
let cond = copyTree(xenv)
cond.typ = getSysType(c.g, xenv.info, tyBool)
actions.add callCodegenProc(c.g, "nimRawDispose", c.info, xenv)
body.add genIf(c, cond, actions)
proc weakrefOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
case c.kind
@ -451,6 +516,7 @@ proc weakrefOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
else:
body.sons.insert(des, 0)
of attachedDeepCopy: assert(false, "cannot happen")
of attachedTrace, attachedDispose: discard
proc ownedRefOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
var actions = newNodeI(nkStmtList, c.info)
@ -473,6 +539,7 @@ proc ownedRefOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
of attachedDestructor:
body.add genIf(c, x, actions)
of attachedDeepCopy: assert(false, "cannot happen")
of attachedTrace, attachedDispose: discard
proc closureOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
if c.kind == attachedDeepCopy:
@ -484,7 +551,7 @@ proc closureOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
call[1] = y
body.add newAsgnStmt(x, call)
elif (optOwnedRefs in c.g.config.globalOptions and
optRefCheck in c.g.config.options) or c.g.config.selectedGC == gcDestructors:
optRefCheck in c.g.config.options) or c.g.config.selectedGC in {gcArc, gcOrc}:
let xx = genBuiltin(c.g, mAccessEnv, "accessEnv", x)
xx.typ = getSysType(c.g, c.info, tyPointer)
case c.kind
@ -506,6 +573,7 @@ proc closureOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
else:
body.sons.insert(des, 0)
of attachedDeepCopy: assert(false, "cannot happen")
of attachedTrace, attachedDispose: discard
proc ownedClosureOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
let xx = genBuiltin(c.g, mAccessEnv, "accessEnv", x)
@ -520,6 +588,7 @@ proc ownedClosureOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
of attachedDestructor:
body.add genIf(c, xx, actions)
of attachedDeepCopy: assert(false, "cannot happen")
of attachedTrace, attachedDispose: discard
proc fillBody(c: var TLiftCtx; t: PType; body, x, y: PNode) =
case t.kind
@ -528,7 +597,7 @@ proc fillBody(c: var TLiftCtx; t: PType; body, x, y: PNode) =
tyPtr, tyOpt, tyUncheckedArray, tyVar, tyLent:
defaultOp(c, t, body, x, y)
of tyRef:
if c.g.config.selectedGC == gcDestructors:
if c.g.config.selectedGC in {gcArc, gcOrc}:
atomicRefOp(c, t, body, x, y)
elif (optOwnedRefs in c.g.config.globalOptions and
optRefCheck in c.g.config.options):
@ -537,7 +606,7 @@ proc fillBody(c: var TLiftCtx; t: PType; body, x, y: PNode) =
defaultOp(c, t, body, x, y)
of tyProc:
if t.callConv == ccClosure:
if c.g.config.selectedGC == gcDestructors:
if c.g.config.selectedGC in {gcArc, gcOrc}:
atomicClosureOp(c, t, body, x, y)
else:
closureOp(c, t, body, x, y)
@ -569,7 +638,7 @@ proc fillBody(c: var TLiftCtx; t: PType; body, x, y: PNode) =
# 'selectedGC' here to determine if we have the new runtime.
discard considerUserDefinedOp(c, t, body, x, y)
elif tfHasAsgn in t.flags:
if c.kind != attachedDestructor:
if c.kind in {attachedAsgn, attachedSink, attachedDeepCopy}:
body.add newSeqCall(c.g, x, y)
forallElements(c, t, body, x, y)
else:
@ -632,30 +701,40 @@ proc produceSym(g: ModuleGraph; c: PContext; typ: PType; kind: TTypeAttachedOp;
a.asgnForType = typ
let dest = newSym(skParam, getIdent(g.cache, "dest"), result, info)
let src = newSym(skParam, getIdent(g.cache, "src"), result, info)
let src = newSym(skParam, getIdent(g.cache, if kind == attachedTrace: "env" else: "src"), result, info)
var d: PNode
#if kind notin {attachedTrace, attachedDispose}:
dest.typ = makeVarType(typ.owner, typ)
src.typ = typ
d = newDeref(newSymNode(dest))
#else:
# dest.typ = typ
# d = newSymNode(dest)
if kind == attachedTrace:
src.typ = getSysType(g, info, tyPointer)
else:
src.typ = typ
result.typ = newProcType(info, typ.owner)
result.typ.addParam dest
if kind != attachedDestructor:
if kind notin {attachedDestructor, attachedDispose}:
result.typ.addParam src
# register this operation already:
typ.attachedOps[kind] = result
var tk: TTypeKind
if g.config.selectedGC in {gcDestructors, gcHooks}:
if g.config.selectedGC in {gcArc, gcOrc, gcHooks}:
tk = skipTypes(typ, {tyOrdinal, tyRange, tyInferred, tyGenericInst, tyStatic, tyAlias, tySink}).kind
else:
tk = tyNone # no special casing for strings and seqs
case tk
of tySequence:
fillSeqOp(a, typ, body, newSymNode(dest).newDeref, newSymNode(src))
fillSeqOp(a, typ, body, d, newSymNode(src))
of tyString:
fillStrOp(a, typ, body, newSymNode(dest).newDeref, newSymNode(src))
fillStrOp(a, typ, body, d, newSymNode(src))
else:
fillBody(a, typ, body, newSymNode(dest).newDeref, newSymNode(src))
fillBody(a, typ, body, d, newSymNode(src))
var n = newNodeI(nkProcDef, info, bodyPos+1)
for i in 0..<n.len: n[i] = newNodeI(nkEmpty, info)
@ -704,7 +783,7 @@ proc isTrival(s: PSym): bool {.inline.} =
proc isEmptyContainer(g: ModuleGraph, t: PType): bool =
(t.kind == tyArray and lengthOrd(g.config, t[0]) == 0) or
(t.kind == tyArray and lengthOrd(g.config, t[0]) == 0) or
(t.kind == tySequence and t[0].kind == tyError)
@ -731,8 +810,13 @@ proc createTypeBoundOps(g: ModuleGraph; c: PContext; orig: PType; info: TLineInf
# 4. We have a custom destructor.
# 5. We have a (custom) generic destructor.
# we do not generate '=trace' nor '=dispose' procs if we
# have the cycle detection disabled, saves code size.
let lastAttached = if g.config.selectedGC == gcOrc: attachedDispose
else: attachedSink
# we generate the destructor first so that other operators can depend on it:
for k in attachedDestructor..attachedSink:
for k in attachedDestructor..lastAttached:
if canon.attachedOps[k] == nil:
discard produceSym(g, c, canon, k, info)
else: