An optimizer for ARC (#14962)

* WIP: an optimizer for ARC
* do not optimize away destructors in 'finally' if unstructured control flow is involved
* optimized the optimizer
* minor code cleanup
* first steps to .cursor inference
* cursor inference: big steps to a working solution
* baby steps
* better .cursor inference
* new feature: expandArc for easy inspection of the AST after ARC transformations
* added topt_cursor test
* adapt tests
* cleanups, make tests green
* optimize common traversal patterns
* moved test case
* fixes .cursor inference so that npeg compiles once again
* cursor inference: more bugfixes

Co-authored-by: Clyybber <darkmine956@gmail.com>
This commit is contained in:
Andreas Rumpf
2020-07-15 23:00:06 +02:00
committed by GitHub
parent 813dd1b670
commit c5358b0d4b
19 changed files with 895 additions and 62 deletions

View File

@@ -866,6 +866,9 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
of "expandmacro":
expectArg(conf, switch, arg, pass, info)
conf.macrosToExpand[arg] = "T"
of "expandarc":
expectArg(conf, switch, arg, pass, info)
conf.arcToExpand[arg] = "T"
of "oldgensym":
processOnOffSwitchG(conf, {optNimV019}, arg, pass, info)
of "useversion":

View File

@@ -0,0 +1,295 @@
#
#
# The Nim Compiler
# (c) Copyright 2020 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Cursor inference:
## The basic idea was like this: Elide 'destroy(x)' calls if only
## special literals are assigned to 'x' and 'x' is not mutated or
## passed by 'var T' to something else. Special literals are string literals or
## arrays / tuples of string literals etc.
##
## However, there is a much more general rule here: Compute which variables
## can be annotated with `.cursor`. To see how and when we can do that,
## think about this question: In `dest = src` when do we really have to
## *materialize* the full copy? - Only if `dest` or `src` are mutated
## afterwards. `dest` is the potential cursor variable, so that is
## simple to analyse. And if `src` is a location derived from a
## formal parameter, we also know it is not mutated! In other words, we
## do a compile-time copy-on-write analysis.
import
ast, types, renderer, idents, intsets, options, msgs
type
Cursor = object
s: PSym
deps: IntSet
Con = object
cursors: seq[Cursor]
mayOwnData: IntSet
mutations: IntSet
reassigns: IntSet
config: ConfigRef
proc locationRoot(e: PNode; followDotExpr = true): PSym =
var n = e
while true:
case n.kind
of nkSym:
if n.sym.kind in {skVar, skResult, skTemp, skLet, skForVar, skParam}:
return n.sym
else:
return nil
of nkDotExpr, nkDerefExpr, nkBracketExpr, nkHiddenDeref,
nkCheckedFieldExpr, nkAddr, nkHiddenAddr:
if followDotExpr:
n = n[0]
else:
return nil
of nkObjUpConv, nkObjDownConv:
n = n[0]
of nkHiddenStdConv, nkHiddenSubConv, nkConv, nkCast:
n = n[1]
of nkStmtList, nkStmtListExpr:
if n.len > 0:
n = n[^1]
else:
return nil
else:
return nil
proc addDep(c: var Con; dest: var Cursor; dependsOn: PSym) =
if dest.s != dependsOn:
dest.deps.incl dependsOn.id
proc cursorId(c: Con; x: PSym): int =
for i in 0..<c.cursors.len:
if c.cursors[i].s == x: return i
return -1
proc getCursors(c: Con): IntSet =
#[
Question: if x depends on y and y depends on z then also y depends on z.
Or does it?
var y = x # y gets the copy already
var harmless = x
y.s = "mutate"
]#
result = initIntSet()
for cur in c.cursors:
if not c.mayOwnData.contains(cur.s.id) and
cur.s.typ.skipTypes({tyGenericInst, tyAlias}).kind != tyOwned:
block doAdd:
for d in cur.deps:
# you cannot borrow from a location that is mutated or (owns data and is re-assigned)
if c.mutations.contains(d) or (c.mayOwnData.contains(d) and c.reassigns.contains(d)):
#echo "bah, not a cursor ", cur.s, " bad dependency ", d
break doAdd
when true:
result.incl cur.s.id
when true:
echo "computed as a cursor ", cur.s, " ", cur.deps, " ", c.config $ cur.s.info
proc analyseAsgn(c: var Con; dest: var Cursor; n: PNode) =
case n.kind
of nkEmpty, nkCharLit..pred(nkNilLit):
# primitive literals including the empty are harmless:
discard
of nkNilLit:
when false:
# XXX think about this case. Is 'nil' an owned location?
if hasDestructor(n.typ):
# 'myref = nil' is destructive and so 'myref' should not be a cursor:
c.mayOwnData.incl dest.s.id
of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv, nkCast, nkConv:
analyseAsgn(c, dest, n[1])
of nkIfStmt, nkIfExpr:
for i in 0..<n.len:
analyseAsgn(c, dest, n[i].lastSon)
of nkCaseStmt:
for i in 1..<n.len:
analyseAsgn(c, dest, n[i].lastSon)
of nkStmtList, nkStmtListExpr:
if n.len > 0:
analyseAsgn(c, dest, n[^1])
of nkClosure:
for i in 1..<n.len:
analyseAsgn(c, dest, n[i])
# you must destroy a closure:
c.mayOwnData.incl dest.s.id
of nkObjConstr:
for i in 1..<n.len:
analyseAsgn(c, dest, n[i])
if hasDestructor(n.typ):
# you must destroy a ref object:
c.mayOwnData.incl dest.s.id
of nkCurly, nkBracket, nkPar, nkTupleConstr:
for son in n:
analyseAsgn(c, dest, son)
if n.typ.skipTypes(abstractInst).kind == tySequence:
# you must destroy a sequence:
c.mayOwnData.incl dest.s.id
of nkSym:
if n.sym.kind in {skVar, skResult, skTemp, skLet, skForVar, skParam}:
if n.sym.flags * {sfThread, sfGlobal} != {}:
# aliasing a global is inherently dangerous:
c.mayOwnData.incl dest.s.id
else:
# otherwise it's just a dependency, nothing to worry about:
c.addDep(dest, n.sym)
of nkDotExpr, nkBracketExpr, nkHiddenDeref, nkDerefExpr,
nkObjUpConv, nkObjDownConv, nkCheckedFieldExpr, nkAddr, nkHiddenAddr:
analyseAsgn(c, dest, n[0])
of nkCallKinds:
if hasDestructor(n.typ):
# calls do construct, what we construct must be destroyed,
# so dest cannot be a cursor:
c.mayOwnData.incl dest.s.id
elif n.typ.kind in {tyLent, tyVar}:
# we know the result is derived from the first argument:
let r = locationRoot(n[1])
if r != nil:
c.addDep(dest, r)
else:
let magic = if n[0].kind == nkSym: n[0].sym.magic else: mNone
# this list is subtle, we try to answer the question if after 'dest = f(src)'
# there is a connection betwen 'src' and 'dest' so that mutations to 'src'
# also reflect 'dest':
if magic in {mNone, mMove, mSlice, mAppendStrCh, mAppendStrStr, mAppendSeqElem, mArrToSeq}:
for i in 1..<n.len:
# we always have to assume a 'select(...)' like mechanism.
# But at least we do filter out simple POD types from the
# list of dependencies via the 'hasDestructor' check for
# the root's symbol.
if hasDestructor(n[i].typ.skipTypes({tyVar, tySink, tyLent, tyGenericInst, tyAlias})):
analyseAsgn(c, dest, n[i])
else:
# something we cannot handle:
c.mayOwnData.incl dest.s.id
proc rhsIsSink(c: var Con, n: PNode) =
if n.kind == nkSym and n.typ.skipTypes(abstractInst-{tyOwned}).kind == tyRef:
discard "do no pessimize simple refs further, injectdestructors.nim will prevent moving from it"
else:
let r = locationRoot(n, followDotExpr = false)
if r != nil and r.kind != skParam:
# let x = cursor? --> treat it like a sink parameter
c.mayOwnData.incl r.id
c.mutations.incl r.id
proc analyse(c: var Con; n: PNode) =
case n.kind
of nkCallKinds:
let parameters = n[0].typ
let L = if parameters != nil: parameters.len else: 0
analyse(c, n[0])
for i in 1..<n.len:
let it = n[i]
let r = locationRoot(it)
if r != nil and i < L:
let paramType = parameters[i].skipTypes({tyGenericInst, tyAlias})
if paramType.kind in {tyVar, tySink, tyOwned}:
# pass by var? mayOwnData the root
# Else we seek to take ownership of 'r'. This is only valid when 'r'
# actually owns its data. Thus 'r' cannot be a cursor:
c.mayOwnData.incl r.id
# and we assume it might get wasMoved(...) too:
c.mutations.incl r.id
analyse(c, it)
of nkAsgn, nkFastAsgn:
analyse(c, n[0])
analyse(c, n[1])
if n[0].kind == nkSym:
if hasDestructor(n[0].typ):
# re-assignment to the full object is fundamentally different:
let idx = cursorId(c, n[0].sym)
if idx >= 0:
analyseAsgn(c, c.cursors[idx], n[1])
c.reassigns.incl n[0].sym.id
else:
# assignments like 'x.field = value' mean that 'x' itself cannot
# be a cursor:
let r = locationRoot(n[0])
if r != nil and r.typ.skipTypes(abstractInst).kind notin {tyPtr, tyRef}:
# however, an assignment like 'it.field = x' does not influence r's
# cursorness property:
c.mayOwnData.incl r.id
c.mutations.incl r.id
if hasDestructor(n[1].typ):
rhsIsSink(c, n[1])
of nkAddr, nkHiddenAddr:
analyse(c, n[0])
let r = locationRoot(n[0])
if r != nil:
c.mayOwnData.incl r.id
c.mutations.incl r.id
of nkTupleConstr, nkBracket, nkObjConstr:
for i in ord(n.kind == nkObjConstr)..<n.len:
if n[i].kind == nkSym:
# we assume constructions with cursors are better without
# the cursors because it's likely we can move then, see
# test arc/topt_no_cursor.nim
let r = n[i].sym
c.mayOwnData.incl r.id
c.mutations.incl r.id
of nkVarSection, nkLetSection:
for it in n:
let value = it[^1]
analyse(c, value)
if hasDestructor(it[0].typ):
for i in 0..<it.len-2:
let v = it[i]
if v.kind == nkSym and v.sym.flags * {sfThread, sfGlobal} == {}:
# assume it's a cursor:
c.cursors.add Cursor(s: it[i].sym)
if it.kind == nkVarTuple and value.kind in {nkPar, nkTupleConstr} and
it.len-2 == value.len:
# this might mayOwnData it again:
analyseAsgn(c, c.cursors[^1], value[i])
rhsIsSink(c, value[i])
else:
# this might mayOwnData it again:
analyseAsgn(c, c.cursors[^1], value)
rhsIsSink(c, value)
of nkNone..nkNilLit, nkTypeSection, nkProcDef, nkConverterDef,
nkMethodDef, nkIteratorDef, nkMacroDef, nkTemplateDef, nkLambda, nkDo,
nkFuncDef, nkConstSection, nkConstDef, nkIncludeStmt, nkImportStmt,
nkExportStmt, nkPragma, nkCommentStmt, nkBreakState, nkTypeOfExpr:
discard
else:
for child in n: analyse(c, child)
proc computeCursors*(n: PNode; config: ConfigRef): IntSet =
var c = Con(config: config)
analyse(c, n)
result = getCursors c

View File

@@ -13,18 +13,11 @@
## See doc/destructors.rst for a spec of the implemented rewrite rules
## XXX Optimization to implement: if a local variable is only assigned
## string literals as in ``let x = conf: "foo" else: "bar"`` do not
## produce a destructor call for ``x``. The address of ``x`` must also
## not have been taken. ``x = "abc"; x.add(...)``
# Todo:
# - eliminate 'wasMoved(x); destroy(x)' pairs as a post processing step.
import
intsets, ast, astalgo, msgs, renderer, magicsys, types, idents,
intsets, strtabs, ast, astalgo, msgs, renderer, magicsys, types, idents,
strutils, options, dfa, lowerings, tables, modulegraphs, msgs,
lineinfos, parampatterns, sighashes, liftdestructors
lineinfos, parampatterns, sighashes, liftdestructors, optimizer,
cursor_inference
from trees import exprStructuralEquivalent, getRoot
@@ -42,6 +35,7 @@ type
Con = object
owner: PSym
g: ControlFlowGraph
cursors: IntSet
graph: ModuleGraph
otherRead: PNode
inLoop, inSpawn: int
@@ -155,6 +149,17 @@ proc isLastRead(location: PNode; cfg: ControlFlowGraph; otherRead: var PNode; pc
pc = min(variantA, variantB)
return pc
proc isCursor(n: PNode; c: Con): bool =
case n.kind
of nkSym:
result = sfCursor in n.sym.flags or c.cursors.contains(n.sym.id)
of nkDotExpr:
result = sfCursor in n[1].sym.flags
of nkCheckedFieldExpr:
result = isCursor(n[0], c)
else:
result = false
proc isLastRead(n: PNode; c: var Con): bool =
# first we need to search for the instruction that belongs to 'n':
var instr = -1
@@ -486,17 +491,6 @@ proc ensureDestruction(arg, orig: PNode; c: var Con; s: var Scope): PNode =
else:
result = arg
proc isCursor(n: PNode): bool =
case n.kind
of nkSym:
result = sfCursor in n.sym.flags
of nkDotExpr:
result = sfCursor in n[1].sym.flags
of nkCheckedFieldExpr:
result = isCursor(n[0])
else:
result = false
proc cycleCheck(n: PNode; c: var Con) =
if c.graph.config.selectedGC != gcArc: return
var value = n[1]
@@ -727,7 +721,8 @@ proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode): PNode =
elif n.kind in {nkBracket, nkObjConstr, nkTupleConstr, nkClosure, nkNilLit} +
nkCallKinds + nkLiterals:
result = p(n, c, s, consumed)
elif ((n.kind == nkSym and isSinkParam(n.sym)) or isAnalysableFieldAccess(n, c.owner)) and isLastRead(n, c):
elif ((n.kind == nkSym and isSinkParam(n.sym)) or isAnalysableFieldAccess(n, c.owner)) and
isLastRead(n, c) and not (n.kind == nkSym and isCursor(n, c)):
# Sinked params can be consumed only once. We need to reset the memory
# to disable the destructor which we have not elided
result = destructiveMoveVar(n, c, s)
@@ -830,7 +825,7 @@ proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode): PNode =
if it.kind == nkVarTuple and hasDestructor(ri.typ):
let x = lowerTupleUnpacking(c.graph, it, c.owner)
result.add p(x, c, s, consumed)
elif it.kind == nkIdentDefs and hasDestructor(it[0].typ) and not isCursor(it[0]):
elif it.kind == nkIdentDefs and hasDestructor(it[0].typ) and not isCursor(it[0], c):
for j in 0..<it.len-2:
let v = it[j]
if v.kind == nkSym:
@@ -851,7 +846,7 @@ proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode): PNode =
result.add v
of nkAsgn, nkFastAsgn:
if hasDestructor(n[0].typ) and n[1].kind notin {nkProcDef, nkDo, nkLambda} and
not isCursor(n[0]):
not isCursor(n[0], c):
# rule (self-assignment-removal):
if n[1].kind == nkSym and n[0].kind == nkSym and n[0].sym == n[1].sym:
result = newNodeI(nkEmpty, n.info)
@@ -988,7 +983,7 @@ proc moveOrCopy(dest, ri: PNode; c: var Con; s: var Scope, isDecl = false): PNod
let snk = c.genSink(dest, ri, isDecl)
result = newTree(nkStmtList, snk, c.genWasMoved(ri))
elif ri.sym.kind != skParam and ri.sym.owner == c.owner and
isLastRead(ri, c) and canBeMoved(c, dest.typ):
isLastRead(ri, c) and canBeMoved(c, dest.typ) and not isCursor(ri, c):
# Rule 3: `=sink`(x, z); wasMoved(z)
let snk = c.genSink(dest, ri, isDecl)
result = newTree(nkStmtList, snk, c.genWasMoved(ri))
@@ -1048,6 +1043,8 @@ proc injectDestructorCalls*(g: ModuleGraph; owner: PSym; n: PNode): PNode =
echoCfg(c.g)
echo n
c.cursors = computeCursors(n, g.config)
var scope: Scope
let body = p(n, c, scope, normal)
@@ -1059,7 +1056,12 @@ proc injectDestructorCalls*(g: ModuleGraph; owner: PSym; n: PNode): PNode =
scope.final.add c.genDestroy(params[i])
#if optNimV2 in c.graph.config.globalOptions:
# injectDefaultCalls(n, c)
result = processScope(c, scope, body)
result = optimize processScope(c, scope, body)
dbg:
echo ">---------transformed-to--------->"
echo renderTree(result, {renderIds})
if g.config.arcToExpand.hasKey(owner.name.s):
echo "--expandArc: ", owner.name.s
echo renderTree(result, {renderIr})
echo "-- end of expandArc ------------------------"

285
compiler/optimizer.nim Normal file
View File

@@ -0,0 +1,285 @@
#
#
# The Nim Compiler
# (c) Copyright 2020 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Optimizer:
## - elide 'wasMoved(x); destroy(x)' pairs
## - recognize "all paths lead to 'wasMoved(x)'"
import
ast, renderer, idents, intsets
from trees import exprStructuralEquivalent
const
nfMarkForDeletion = nfNone # faster than a lookup table
type
BasicBlock = object
wasMovedLocs: seq[PNode]
kind: TNodeKind
hasReturn, hasBreak: bool
label: PSym # can be nil
parent: ptr BasicBlock
Con = object
somethingTodo: bool
inFinally: int
proc nestedBlock(parent: var BasicBlock; kind: TNodeKind): BasicBlock =
BasicBlock(wasMovedLocs: @[], kind: kind, hasReturn: false, hasBreak: false,
label: nil, parent: addr(parent))
proc breakStmt(b: var BasicBlock; n: PNode) =
var it = addr(b)
while it != nil:
it.wasMovedLocs.setLen 0
it.hasBreak = true
if n.kind == nkSym:
if it.label == n.sym: break
else:
# unnamed break leaves the block is nkWhileStmt or the like:
if it.kind in {nkWhileStmt, nkBlockStmt, nkBlockExpr}: break
it = it.parent
proc returnStmt(b: var BasicBlock) =
b.hasReturn = true
var it = addr(b)
while it != nil:
it.wasMovedLocs.setLen 0
it = it.parent
proc mergeBasicBlockInfo(parent: var BasicBlock; this: BasicBlock) {.inline.} =
if this.hasReturn:
parent.wasMovedLocs.setLen 0
parent.hasReturn = true
proc wasMovedTarget(matches: var IntSet; branch: seq[PNode]; moveTarget: PNode): bool =
result = false
for i in 0..<branch.len:
if exprStructuralEquivalent(branch[i][1].skipAddr, moveTarget,
strictSymEquality = true):
result = true
matches.incl i
proc intersect(summary: var seq[PNode]; branch: seq[PNode]) =
# keep all 'wasMoved(x)' calls in summary that are also in 'branch':
var i = 0
var matches = initIntSet()
while i < summary.len:
if wasMovedTarget(matches, branch, summary[i][1].skipAddr):
inc i
else:
summary.del i
for m in matches:
summary.add branch[m]
proc invalidateWasMoved(c: var BasicBlock; x: PNode) =
var i = 0
while i < c.wasMovedLocs.len:
if exprStructuralEquivalent(c.wasMovedLocs[i][1].skipAddr, x,
strictSymEquality = true):
c.wasMovedLocs.del i
else:
inc i
proc wasMovedDestroyPair(c: var Con; b: var BasicBlock; d: PNode) =
var i = 0
while i < b.wasMovedLocs.len:
if exprStructuralEquivalent(b.wasMovedLocs[i][1].skipAddr, d[1].skipAddr,
strictSymEquality = true):
b.wasMovedLocs[i].flags.incl nfMarkForDeletion
c.somethingTodo = true
d.flags.incl nfMarkForDeletion
b.wasMovedLocs.del i
else:
inc i
proc analyse(c: var Con; b: var BasicBlock; n: PNode) =
case n.kind
of nkCallKinds:
var special = false
var reverse = false
if n[0].kind == nkSym:
let s = n[0].sym
if s.magic == mWasMoved:
b.wasMovedLocs.add n
special = true
elif s.name.s == "=destroy":
if c.inFinally > 0 and (b.hasReturn or b.hasBreak):
discard "cannot optimize away the destructor"
else:
c.wasMovedDestroyPair b, n
special = true
elif s.name.s == "=sink":
reverse = true
if not special:
if not reverse:
for i in 0 ..< n.len:
analyse(c, b, n[i])
else:
#[ Test tmatrix.test3:
Prevent this from being elided. We should probably
find a better solution...
`=sink`(b, - (
let blitTmp = b;
wasMoved(b);
blitTmp + a)
`=destroy`(b)
]#
for i in countdown(n.len-1, 0):
analyse(c, b, n[i])
if canRaise(n[0]): returnStmt(b)
of nkSym:
# any usage of the location before destruction implies we
# cannot elide the 'wasMoved(x)':
b.invalidateWasMoved n
of nkNone..pred(nkSym), succ(nkSym)..nkNilLit, nkTypeSection, nkProcDef, nkConverterDef,
nkMethodDef, nkIteratorDef, nkMacroDef, nkTemplateDef, nkLambda, nkDo,
nkFuncDef, nkConstSection, nkConstDef, nkIncludeStmt, nkImportStmt,
nkExportStmt, nkPragma, nkCommentStmt, nkBreakState, nkTypeOfExpr:
discard "do not follow the construct"
of nkAsgn, nkFastAsgn:
# reverse order, see remark for `=sink`:
analyse(c, b, n[1])
analyse(c, b, n[0])
of nkIfStmt, nkIfExpr:
let isExhaustive = n[^1].kind in {nkElse, nkElseExpr}
var wasMovedSet: seq[PNode] = @[]
for i in 0 ..< n.len:
var branch = nestedBlock(b, n[i].kind)
analyse(c, branch, n[i])
mergeBasicBlockInfo(b, branch)
if isExhaustive:
if i == 0:
wasMovedSet = move(branch.wasMovedLocs)
else:
wasMovedSet.intersect(branch.wasMovedLocs)
for i in 0..<wasMovedSet.len:
b.wasMovedLocs.add wasMovedSet[i]
of nkCaseStmt:
let isExhaustive = skipTypes(n[0].typ,
abstractVarRange-{tyTypeDesc}).kind notin {tyFloat..tyFloat128, tyString} or
n[^1].kind == nkElse
analyse(c, b, n[0])
var wasMovedSet: seq[PNode] = @[]
for i in 1 ..< n.len:
var branch = nestedBlock(b, n[i].kind)
analyse(c, branch, n[i])
mergeBasicBlockInfo(b, branch)
if isExhaustive:
if i == 1:
wasMovedSet = move(branch.wasMovedLocs)
else:
wasMovedSet.intersect(branch.wasMovedLocs)
for i in 0..<wasMovedSet.len:
b.wasMovedLocs.add wasMovedSet[i]
of nkTryStmt:
for i in 0 ..< n.len:
var tryBody = nestedBlock(b, nkTryStmt)
analyse(c, tryBody, n[i])
mergeBasicBlockInfo(b, tryBody)
of nkWhileStmt:
analyse(c, b, n[0])
var loopBody = nestedBlock(b, nkWhileStmt)
analyse(c, loopBody, n[1])
mergeBasicBlockInfo(b, loopBody)
of nkBlockStmt, nkBlockExpr:
var blockBody = nestedBlock(b, n.kind)
if n[0].kind == nkSym:
blockBody.label = n[0].sym
analyse(c, blockBody, n[1])
mergeBasicBlockInfo(b, blockBody)
of nkBreakStmt:
breakStmt(b, n[0])
of nkReturnStmt, nkRaiseStmt:
for child in n: analyse(c, b, child)
returnStmt(b)
of nkFinally:
inc c.inFinally
for child in n: analyse(c, b, child)
dec c.inFinally
else:
for child in n: analyse(c, b, child)
proc opt(c: Con; n, parent: PNode; parentPos: int) =
template recurse() =
let x = shallowCopy(n)
for i in 0 ..< n.len:
opt(c, n[i], x, i)
parent[parentPos] = x
case n.kind
of nkCallKinds:
if nfMarkForDeletion in n.flags:
parent[parentPos] = newNodeI(nkEmpty, n.info)
else:
recurse()
of nkNone..nkNilLit, nkTypeSection, nkProcDef, nkConverterDef,
nkMethodDef, nkIteratorDef, nkMacroDef, nkTemplateDef, nkLambda, nkDo,
nkFuncDef, nkConstSection, nkConstDef, nkIncludeStmt, nkImportStmt,
nkExportStmt, nkPragma, nkCommentStmt, nkBreakState, nkTypeOfExpr:
parent[parentPos] = n
else:
recurse()
proc optimize*(n: PNode): PNode =
# optimize away simple 'wasMoved(x); destroy(x)' pairs.
#[ Unfortunately this optimization is only really safe when no exceptions
are possible, see for example:
proc main(inp: string; cond: bool) =
if cond:
try:
var s = ["hi", inp & "more"]
for i in 0..4:
use s
consume(s)
wasMoved(s)
finally:
destroy(s)
Now assume 'use' raises, then we shouldn't do the 'wasMoved(s)'
]#
var c: Con
var b: BasicBlock
analyse(c, b, n)
if c.somethingTodo:
result = shallowCopy(n)
for i in 0 ..< n.safeLen:
opt(c, n[i], result, i)
else:
result = n

View File

@@ -235,6 +235,7 @@ type
options*: TOptions # (+)
globalOptions*: TGlobalOptions # (+)
macrosToExpand*: StringTableRef
arcToExpand*: StringTableRef
m*: MsgConfig
evalTemplateCounter*: int
evalMacroCounter*: int
@@ -410,6 +411,7 @@ proc newConfigRef*(): ConfigRef =
options: DefaultOptions,
globalOptions: DefaultGlobalOptions,
macrosToExpand: newStringTable(modeStyleInsensitive),
arcToExpand: newStringTable(modeStyleInsensitive),
m: initMsgConfig(),
evalExpr: "",
cppDefines: initHashSet[string](),

View File

@@ -20,7 +20,8 @@ import
type
TRenderFlag* = enum
renderNone, renderNoBody, renderNoComments, renderDocComments,
renderNoPragmas, renderIds, renderNoProcDefs, renderSyms, renderRunnableExamples
renderNoPragmas, renderIds, renderNoProcDefs, renderSyms, renderRunnableExamples,
renderIr
TRenderFlags* = set[TRenderFlag]
TRenderTok* = object
kind*: TTokType
@@ -47,11 +48,20 @@ type
pendingNewlineCount: int
fid*: FileIndex
config*: ConfigRef
mangler: seq[PSym]
# We render the source code in a two phases: The first
# determines how long the subtree will likely be, the second
# phase appends to a buffer that will be the output.
proc disamb(g: var TSrcGen; s: PSym): int =
# we group by 's.name.s' to compute the stable name ID.
result = 0
for i in 0 ..< g.mangler.len:
if s == g.mangler[i]: return result
if s.name.s == g.mangler[i].name.s: inc result
g.mangler.add s
proc isKeyword*(i: PIdent): bool =
if (i.id >= ord(tokKeywordLow) - ord(tkSymbol)) and
(i.id <= ord(tokKeywordHigh) - ord(tkSymbol)):
@@ -850,7 +860,12 @@ proc gident(g: var TSrcGen, n: PNode) =
t = tkSymbol
else:
t = tkOpr
if n.kind == nkSym and (renderIds in g.flags or sfGenSym in n.sym.flags or n.sym.kind == skTemp):
if renderIr in g.flags and n.kind == nkSym:
let localId = disamb(g, n.sym)
if localId != 0 and n.sym.magic == mNone:
s.add '_'
s.addInt localId
elif n.kind == nkSym and (renderIds in g.flags or sfGenSym in n.sym.flags or n.sym.kind == skTemp):
s.add '_'
s.addInt n.sym.id
when defined(debugMagics):
@@ -1022,7 +1037,7 @@ proc gsub(g: var TSrcGen, n: PNode, c: TContext) =
else:
put(g, tkSymbol, "(wrong conv)")
of nkHiddenCallConv:
if renderIds in g.flags:
if {renderIds, renderIr} * g.flags != {}:
accentedName(g, n[0])
put(g, tkParLe, "(")
gcomma(g, n, 1)

View File

@@ -233,7 +233,7 @@ proc hasContinue(n: PNode): bool =
proc newLabel(c: PTransf, n: PNode): PSym =
result = newSym(skLabel, nil, getCurrOwner(c), n.info)
result.name = getIdent(c.graph.cache, genPrefix & $result.id)
result.name = getIdent(c.graph.cache, genPrefix)
proc transformBlock(c: PTransf, n: PNode): PNode =
var labl: PSym

View File

@@ -122,6 +122,8 @@ Advanced options:
use the provided namespace for the generated C++ code,
if no namespace is provided "Nim" will be used
--expandMacro:MACRO dump every generated AST from MACRO
--expandArc:PROCNAME show how PROCNAME looks like after diverse optimizations
before the final backend phase (mostly ARC/ORC specific)
--excludePath:PATH exclude a path from the list of search paths
--dynlibOverride:SYMBOL marks SYMBOL so that dynlib:SYMBOL
has no effect and can be statically linked instead;

View File

@@ -354,7 +354,8 @@ Destructor removal
``wasMoved(x);`` followed by a `=destroy(x)` operation cancel each other
out. An implementation is encouraged to exploit this in order to improve
efficiency and code sizes.
efficiency and code sizes. The current implementation does perform this
optimization.
Self assignments
@@ -509,6 +510,31 @@ to be safe, but for ``ptr`` the compiler has to remain silent about possible
problems.
Cursor inference / copy elision
===============================
The current implementation also performs `.cursor` inference. Cursor inference is
a form of copy elision.
To see how and when we can do that, think about this question: In `dest = src` when
do we really have to *materialize* the full copy? - Only if `dest` or `src` are mutated
afterwards. If `dest` is a local variable that is simple to analyse. And if `src` is a
location derived from a formal parameter, we also know it is not mutated! In other
words, we do a compile-time copy-on-write analysis.
This means that "borrowed" views can be written naturally and without explicit pointer
indirections:
.. code-block:: nim
proc main(tab: Table[string, string]) =
let v = tab["key"] # inferred as .cursor because 'tab' is not mutated.
# no copy into 'v', no destruction of 'v'.
use(v)
useItAgain(v)
Owned refs
==========

View File

@@ -498,7 +498,7 @@ proc re*(pattern: string): Regex =
initRegex(pattern, flags, study)
proc matchImpl(str: string, pattern: Regex, start, endpos: int, flags: int): Option[RegexMatch] =
var myResult = RegexMatch(pattern : pattern, str : str)
var myResult = RegexMatch(pattern: pattern, str: str)
# See PCRE man pages.
# 2x capture count to make room for start-end pairs
# 1x capture count as slack space for PCRE
@@ -528,13 +528,13 @@ proc matchImpl(str: string, pattern: Regex, start, endpos: int, flags: int): Opt
of pcre.ERROR_NULL:
raise newException(AccessViolationDefect, "Expected non-null parameters")
of pcre.ERROR_BADOPTION:
raise RegexInternalError(msg : "Unknown pattern flag. Either a bug or " &
raise RegexInternalError(msg: "Unknown pattern flag. Either a bug or " &
"outdated PCRE.")
of pcre.ERROR_BADUTF8, pcre.ERROR_SHORTUTF8, pcre.ERROR_BADUTF8_OFFSET:
raise InvalidUnicodeError(msg : "Invalid unicode byte sequence",
pos : myResult.pcreMatchBounds[0].a)
raise InvalidUnicodeError(msg: "Invalid unicode byte sequence",
pos: myResult.pcreMatchBounds[0].a)
else:
raise RegexInternalError(msg : "Unknown internal error: " & $execRet)
raise RegexInternalError(msg: "Unknown internal error: " & $execRet)
proc match*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[RegexMatch] =
## Like ` ``find(...)`` <#proc-find>`_, but anchored to the start of the

View File

@@ -1,15 +1,7 @@
discard """
cmd: '''nim c --newruntime $file'''
output: '''copied
copied
2
destroyed
destroyed
copied
copied
2
destroyed
destroyed'''
output: '''2
2'''
"""
type

35
tests/arc/topt_cursor.nim Normal file
View File

@@ -0,0 +1,35 @@
discard """
output: '''("string here", 80)'''
cmd: '''nim c --gc:arc --expandArc:main --hint:Performance:off $file'''
nimout: '''--expandArc: main
var
:tmpD
:tmpD_1
:tmpD_2
try:
var x = ("hi", 5)
x = if cond:
:tmpD = ("different", 54)
:tmpD else:
:tmpD_1 = ("string here", 80)
:tmpD_1
echo [
:tmpD_2 = `$`(x)
:tmpD_2]
finally:
`=destroy`(:tmpD_2)
-- end of expandArc ------------------------'''
"""
proc main(cond: bool) =
var x = ("hi", 5) # goal: computed as cursor
x = if cond:
("different", 54)
else:
("string here", 80)
echo x
main(false)

View File

@@ -0,0 +1,37 @@
discard """
output: '''(repo: "", package: "meo", ext: "")'''
cmd: '''nim c --gc:arc --expandArc:newTarget --hint:Performance:off $file'''
nimout: '''--expandArc: newTarget
var
splat
:tmp
:tmp_1
:tmp_2
splat = splitFile(path)
:tmp = splat.dir
wasMoved(splat.dir)
:tmp_1 = splat.name
wasMoved(splat.name)
:tmp_2 = splat.ext
wasMoved(splat.ext)
result = (
let blitTmp = :tmp
blitTmp,
let blitTmp_1 = :tmp_1
blitTmp_1,
let blitTmp_2 = :tmp_2
blitTmp_2)
`=destroy`(splat)
-- end of expandArc ------------------------'''
"""
import os
type Target = tuple[repo, package, ext: string]
proc newTarget*(path: string): Target =
let splat = path.splitFile
result = (repo: splat.dir, package: splat.name, ext: splat.ext)
echo newTarget("meo")

View File

@@ -0,0 +1,39 @@
discard """
output: ''''''
cmd: '''nim c --gc:arc --expandArc:traverse --hint:Performance:off $file'''
nimout: '''--expandArc: traverse
var it = root
block :tmp:
while (
not (it == nil)):
echo [it.s]
it = it.ri
var jt = root
block :tmp_1:
while (
not (jt == nil)):
let ri_1 = jt.ri
echo [jt.s]
jt = ri_1
-- end of expandArc ------------------------'''
"""
type
Node = ref object
le, ri: Node
s: string
proc traverse(root: Node) =
var it = root
while it != nil:
echo it.s
it = it.ri
var jt = root
while jt != nil:
let ri = jt.ri
echo jt.s
jt = ri
traverse(nil)

View File

@@ -0,0 +1,92 @@
discard """
output: ''''''
cmd: '''nim c --gc:arc --expandArc:main --expandArc:tfor --hint:Performance:off $file'''
nimout: '''--expandArc: main
var
a
b
x
x = f()
if cond:
add(a):
let blitTmp = x
blitTmp
else:
add(b):
let blitTmp_1 = x
blitTmp_1
`=destroy`(b)
`=destroy`(a)
-- end of expandArc ------------------------
--expandArc: tfor
var
a
b
x
try:
x = f()
block :tmp:
var i
var i_1 = 0
block :tmp_1:
while i_1 < 4:
var :tmpD
i = i_1
if i == 2:
return
add(a):
wasMoved(:tmpD)
`=`(:tmpD, x)
:tmpD
inc i_1, 1
if cond:
add(a):
let blitTmp = x
wasMoved(x)
blitTmp
else:
add(b):
let blitTmp_1 = x
wasMoved(x)
blitTmp_1
finally:
`=destroy`(x)
`=destroy_1`(b)
`=destroy_1`(a)
-- end of expandArc ------------------------'''
"""
proc f(): seq[int] =
@[1, 2, 3]
proc main(cond: bool) =
var a, b: seq[seq[int]]
var x = f()
if cond:
a.add x
else:
b.add x
# all paths move 'x' so no wasMoved(x); destroy(x) pair should be left in the
# AST.
main(false)
proc tfor(cond: bool) =
var a, b: seq[seq[int]]
var x = f()
for i in 0 ..< 4:
if i == 2: return
a.add x
if cond:
a.add x
else:
b.add x
tfor(false)

View File

@@ -138,16 +138,16 @@ proc search*[M, D: Dim; RT, LT](t: RTree[M, D, RT, LT]; b: Box[D, RT]): seq[LT]
# a R*TREE proc
proc chooseSubtree[M, D: Dim; RT, LT](t: RTree[M, D, RT, LT]; b: Box[D, RT]; level: int): H[M, D, RT, LT] =
assert level >= 0
var n = t.root
while n.level > level:
let nn = Node[M, D, RT, LT](n)
var it = t.root
while it.level > level:
let nn = Node[M, D, RT, LT](it)
var i0 = 0 # selected index
var minLoss = type(b[0].a).high
if n.level == 1: # childreen are leaves -- determine the minimum overlap costs
for i in 0 ..< n.numEntries:
if it.level == 1: # childreen are leaves -- determine the minimum overlap costs
for i in 0 ..< it.numEntries:
let nx = union(nn.a[i].b, b)
var loss = 0
for j in 0 ..< n.numEntries:
for j in 0 ..< it.numEntries:
if i == j: continue
loss += (overlap(nx, nn.a[j].b) - overlap(nn.a[i].b, nn.a[j].b)) # overlap (i, j) == (j, i), so maybe cache that?
var rep = loss < minLoss
@@ -163,7 +163,7 @@ proc chooseSubtree[M, D: Dim; RT, LT](t: RTree[M, D, RT, LT]; b: Box[D, RT]; lev
i0 = i
minLoss = loss
else:
for i in 0 ..< n.numEntries:
for i in 0 ..< it.numEntries:
let loss = enlargement(nn.a[i].b, b)
var rep = loss < minLoss
if loss == minLoss:
@@ -174,8 +174,8 @@ proc chooseSubtree[M, D: Dim; RT, LT](t: RTree[M, D, RT, LT]; b: Box[D, RT]; lev
if rep:
i0 = i
minLoss = loss
n = nn.a[i0].n
return n
it = nn.a[i0].n
return it
proc pickSeeds[M, D: Dim; RT, LT](t: RTree[M, D, RT, LT]; n: Node[M, D, RT, LT] | Leaf[M, D, RT, LT]; bx: Box[D, RT]): (int, int) =
var i0, j0: int

View File

@@ -1,5 +1,6 @@
discard """
output: '''assign
output: '''
assign
destroy
destroy
5
@@ -104,12 +105,12 @@ test()
#------------------------------------------------------------
# Issue #12883
type
type
TopObject = object
internal: UniquePtr[int]
proc deleteTop(p: ptr TopObject) =
if p != nil:
if p != nil:
`=destroy`(p[]) # !!! this operation used to leak the integer
deallocshared(p)
@@ -117,12 +118,12 @@ proc createTop(): ptr TopObject =
result = cast[ptr TopObject](allocShared0(sizeof(TopObject)))
result.internal = newUniquePtr(1)
proc test2() =
proc test2() =
let x = createTop()
echo $x.internal
deleteTop(x)
echo "---------------"
echo "---------------"
echo "app begin"
test2()
echo "app end"

View File

@@ -21,11 +21,13 @@ proc `=sink`(dest: var Foo, src: Foo) =
proc `=`(dest: var Foo, src: Foo) =
assign_counter.inc
proc createFoo(): Foo = Foo(boo: 0)
proc test(): auto =
var a, b: Foo
var a, b = createFoo()
return (a, b, Foo(boo: 5))
var (a, b, _) = test()
var (ag, bg, _) = test()
doAssert assign_counter == 0
doAssert sink_counter == 0

View File

@@ -17,15 +17,20 @@ proc `=sink`(self: var Foo; other: Foo) =
proc `=destroy`(self: var Foo) = discard
template preventCursorInference(x) =
let p = unsafeAddr(x)
proc test(): Foo =
result = Foo()
let temp = result
preventCursorInference temp
doAssert temp.i > 0
return result
proc testB(): Foo =
result = Foo()
let temp = result
preventCursorInference temp
doAssert temp.i > 0
discard test()