# # # The Nim Compiler # (c) Copyright 2017 Andreas Rumpf # # See the file "copying.txt", included in this # distribution, for details about the copyright. # ## Injects destructor calls into Nim code as well as ## an optimizer that optimizes copies to moves. This is implemented as an ## AST to AST transformation so that every backend benefits from it. ## 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, strutils, options, dfa, lowerings, tables, modulegraphs, msgs, lineinfos, parampatterns, sighashes, liftdestructors from trees import exprStructuralEquivalent, getRoot type Scope = object # well we do scope-based memory management. \ # a scope is comparable to an nkStmtListExpr like # (try: statements; dest = y(); finally: destructors(); dest) vars: seq[PSym] wasMoved: seq[PNode] final: seq[PNode] # finally section needsTry: bool parent: ptr Scope escapingSyms: IntSet # a construct like (block: let x = f(); x) # means that 'x' escapes. We then destroy it # in the parent's scope (and also allocate it there). escapingExpr: PNode type Con = object owner: PSym g: ControlFlowGraph jumpTargets: IntSet graph: ModuleGraph emptyNode: PNode otherRead: PNode inLoop, inSpawn: int uninit: IntSet # set of uninit'ed vars uninitComputed: bool ProcessMode = enum normal consumed sinkArg const toDebug {.strdefine.} = "" template dbg(body) = when toDebug.len > 0: if c.owner.name.s == toDebug or toDebug == "always": body proc getTemp(c: var Con; s: var Scope; typ: PType; info: TLineInfo): PNode = let sym = newSym(skTemp, getIdent(c.graph.cache, ":tmpD"), c.owner, info) sym.typ = typ s.vars.add(sym) result = newSymNode(sym) proc nestedScope(parent: var Scope): Scope = Scope(vars: @[], wasMoved: @[], final: @[], needsTry: false, parent: addr(parent)) proc rememberParent(parent: var Scope; inner: Scope) {.inline.} = parent.needsTry = parent.needsTry or inner.needsTry proc optimize(s: var Scope) = # 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(x) Now assume 'use' raises, then we shouldn't do the 'wasMoved(s)' ]# proc findCorrespondingDestroy(final: seq[PNode]; moved: PNode): int = # remember that it's destroy(addr(x)) for i in 0 ..< final.len: if final[i] != nil and exprStructuralEquivalent(final[i][1].skipAddr, moved, strictSymEquality = true): return i return -1 var removed = 0 for i in 0 ..< s.wasMoved.len: let j = findCorrespondingDestroy(s.final, s.wasMoved[i][1]) if j >= 0: s.wasMoved[i] = nil s.final[j] = nil inc removed if removed > 0: template filterNil(field) = var m = newSeq[PNode](s.field.len - removed) var mi = 0 for i in 0 ..< s.field.len: if s.field[i] != nil: m[mi] = s.field[i] inc mi assert mi == m.len s.field = m filterNil(wasMoved) filterNil(final) type ToTreeFlag = enum onlyCareAboutVars, producesValue proc toTree(c: var Con; s: var Scope; ret: PNode; flags: set[ToTreeFlag]): PNode = proc isComplexStmtListExpr(n: PNode): bool = n.kind == nkStmtListExpr and (n.len == 0 or n[^1].kind != nkSym) #if not s.needsTry: optimize(s) assert ret != nil if s.vars.len == 0 and s.final.len == 0 and s.wasMoved.len == 0: # trivial, nothing was done: result = ret else: let isExpr = producesValue in flags and not isEmptyType(ret.typ) var r = PNode(nil) if isExpr: result = newNodeIT(nkStmtListExpr, ret.info, ret.typ) if ret.kind in nkCallKinds or isComplexStmtListExpr(ret): r = c.getTemp(s, ret.typ, ret.info) else: result = newNodeI(nkStmtList, ret.info) if s.vars.len > 0: let varSection = newNodeI(nkVarSection, ret.info) for tmp in s.vars: varSection.add newTree(nkIdentDefs, newSymNode(tmp), newNodeI(nkEmpty, ret.info), newNodeI(nkEmpty, ret.info)) result.add varSection if onlyCareAboutVars in flags: result.add ret s.vars.setLen 0 elif s.needsTry: var finSection = newNodeI(nkStmtList, ret.info) for m in s.wasMoved: finSection.add m for i in countdown(s.final.high, 0): finSection.add s.final[i] result.add newTryFinally(ret, finSection) else: if r != nil: if ret.kind == nkStmtListExpr: # simplify it a bit further by merging the nkStmtListExprs let last = ret.len - 1 for i in 0 ..< last: result.add ret[i] result.add newTree(nkFastAsgn, r, ret[last]) else: result.add newTree(nkFastAsgn, r, ret) for m in s.wasMoved: result.add m for i in countdown(s.final.high, 0): result.add s.final[i] result.add r elif ret.kind == nkStmtListExpr: # simplify it a bit further by merging the nkStmtListExprs let last = ret.len - 1 for i in 0 ..< last: result.add ret[i] for m in s.wasMoved: result.add m for i in countdown(s.final.high, 0): result.add s.final[i] result.add ret[last] else: result.add ret for m in s.wasMoved: result.add m for i in countdown(s.final.high, 0): result.add s.final[i] proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode): PNode proc moveOrCopy(dest, ri: PNode; c: var Con; s: var Scope; isDecl = false): PNode proc isLastRead(location: PNode; cfg: ControlFlowGraph; otherRead: var PNode; pc, until: int): int = var pc = pc while pc < cfg.len and pc < until: case cfg[pc].kind of def: if instrTargets(cfg[pc].n, location) == Full: # the path leads to a redefinition of 's' --> abandon it. return high(int) elif instrTargets(cfg[pc].n, location) == Partial: # only partially writes to 's' --> can't sink 's', so this def reads 's' otherRead = cfg[pc].n return -1 inc pc of use: if instrTargets(cfg[pc].n, location) != None: otherRead = cfg[pc].n return -1 inc pc of goto: pc = pc + cfg[pc].dest of fork: # every branch must lead to the last read of the location: var variantA = pc + 1 var variantB = pc + cfg[pc].dest while variantA != variantB: if min(variantA, variantB) < 0: return -1 if max(variantA, variantB) >= cfg.len or min(variantA, variantB) >= until: break if variantA < variantB: variantA = isLastRead(location, cfg, otherRead, variantA, min(variantB, until)) else: variantB = isLastRead(location, cfg, otherRead, variantB, min(variantA, until)) pc = min(variantA, variantB) return pc proc isLastRead(n: PNode; c: var Con): bool = # first we need to search for the instruction that belongs to 'n': var instr = -1 let m = dfa.skipConvDfa(n) for i in 0..= c.g.len: return true c.otherRead = nil result = isLastRead(n, c.g, c.otherRead, instr+1, int.high) >= 0 dbg: echo "ugh ", c.otherRead.isNil, " ", result proc isFirstWrite(location: PNode; cfg: ControlFlowGraph; pc, until: int): int = var pc = pc while pc < until: case cfg[pc].kind of def: if instrTargets(cfg[pc].n, location) != None: # a definition of 's' before ours makes ours not the first write return -1 inc pc of use: if instrTargets(cfg[pc].n, location) != None: return -1 inc pc of goto: pc = pc + cfg[pc].dest of fork: # every branch must not contain a def/use of our location: var variantA = pc + 1 var variantB = pc + cfg[pc].dest while variantA != variantB: if min(variantA, variantB) < 0: return -1 if max(variantA, variantB) > until: break if variantA < variantB: variantA = isFirstWrite(location, cfg, variantA, min(variantB, until)) else: variantB = isFirstWrite(location, cfg, variantB, min(variantA, until)) pc = min(variantA, variantB) return pc proc isFirstWrite(n: PNode; c: var Con): bool = # first we need to search for the instruction that belongs to 'n': var instr = -1 let m = dfa.skipConvDfa(n) for i in countdown(c.g.len-1, 0): # We search backwards here to treat loops correctly if c.g[i].kind == def and c.g[i].n == m: if instr < 0: instr = i break if instr < 0: return false # we go through all paths going to 'instr' and need to # ensure that we don't find another 'def/use X' instruction. if instr == 0: return true result = isFirstWrite(n, c.g, 0, instr) >= 0 proc initialized(code: ControlFlowGraph; pc: int, init, uninit: var IntSet; until: int): int = ## Computes the set of definitely initialized variables across all code paths ## as an IntSet of IDs. var pc = pc while pc < code.len: case code[pc].kind of goto: pc = pc + code[pc].dest of fork: var initA = initIntSet() var initB = initIntSet() var variantA = pc + 1 var variantB = pc + code[pc].dest while variantA != variantB: if max(variantA, variantB) > until: break if variantA < variantB: variantA = initialized(code, variantA, initA, uninit, min(variantB, until)) else: variantB = initialized(code, variantB, initB, uninit, min(variantA, until)) pc = min(variantA, variantB) # we add vars if they are in both branches: for v in initA: if v in initB: init.incl v of use: let v = code[pc].n.sym if v.kind != skParam and v.id notin init: # attempt to read an uninit'ed variable uninit.incl v.id inc pc of def: let v = code[pc].n.sym init.incl v.id inc pc return pc template isUnpackedTuple(n: PNode): bool = ## we move out all elements of unpacked tuples, ## hence unpacked tuples themselves don't need to be destroyed (n.kind == nkSym and n.sym.kind == skTemp and n.sym.typ.kind == tyTuple) proc checkForErrorPragma(c: Con; t: PType; ri: PNode; opname: string) = var m = "'" & opname & "' is not available for type <" & typeToString(t) & ">" if opname == "=" and ri != nil: m.add "; requires a copy because it's not the last read of '" m.add renderTree(ri) m.add '\'' if c.otherRead != nil: m.add "; another read is done here: " m.add c.graph.config $ c.otherRead.info elif ri.kind == nkSym and ri.sym.kind == skParam and not isSinkType(ri.sym.typ): m.add "; try to make " m.add renderTree(ri) m.add " a 'sink' parameter" m.add "; routine: " m.add c.owner.name.s localError(c.graph.config, ri.info, errGenerated, m) proc makePtrType(c: Con, baseType: PType): PType = result = newType(tyPtr, c.owner) addSonSkipIntLit(result, baseType) proc genOp(c: Con; op: PSym; dest: PNode): PNode = let addrExp = newNodeIT(nkHiddenAddr, dest.info, makePtrType(c, dest.typ)) addrExp.add(dest) result = newTree(nkCall, newSymNode(op), addrExp) proc genOp(c: Con; t: PType; kind: TTypeAttachedOp; dest, ri: PNode): PNode = var op = t.attachedOps[kind] if op == nil or op.ast[genericParamsPos].kind != nkEmpty: # give up and find the canonical type instead: let h = sighashes.hashType(t, {CoType, CoConsiderOwned, CoDistinct}) let canon = c.graph.canonTypes.getOrDefault(h) if canon != nil: op = canon.attachedOps[kind] if op == nil: #echo dest.typ.id globalError(c.graph.config, dest.info, "internal error: '" & AttachedOpToStr[kind] & "' operator not found for type " & typeToString(t)) elif op.ast[genericParamsPos].kind != nkEmpty: globalError(c.graph.config, dest.info, "internal error: '" & AttachedOpToStr[kind] & "' operator is generic") dbg: if kind == attachedDestructor: echo "destructor is ", op.id, " ", op.ast if sfError in op.flags: checkForErrorPragma(c, t, ri, AttachedOpToStr[kind]) c.genOp(op, dest) proc genDestroy(c: Con; dest: PNode): PNode = let t = dest.typ.skipTypes({tyGenericInst, tyAlias, tySink}) result = c.genOp(t, attachedDestructor, dest, nil) proc canBeMoved(c: Con; t: PType): bool {.inline.} = let t = t.skipTypes({tyGenericInst, tyAlias, tySink}) if optOwnedRefs in c.graph.config.globalOptions: result = t.kind != tyRef and t.attachedOps[attachedSink] != nil else: result = t.attachedOps[attachedSink] != nil proc isNoInit(dest: PNode): bool {.inline.} = result = dest.kind == nkSym and sfNoInit in dest.sym.flags proc genSink(c: var Con; s: var Scope; dest, ri: PNode, isDecl = false): PNode = if isUnpackedTuple(dest) or isDecl or (isAnalysableFieldAccess(dest, c.owner) and isFirstWrite(dest, c)) or isNoInit(dest): # optimize sink call into a bitwise memcopy result = newTree(nkFastAsgn, dest, ri) else: let t = dest.typ.skipTypes({tyGenericInst, tyAlias, tySink}) if t.attachedOps[attachedSink] != nil: result = c.genOp(t, attachedSink, dest, ri) result.add ri else: # the default is to use combination of `=destroy(dest)` and # and copyMem(dest, source). This is efficient. result = newTree(nkStmtList, c.genDestroy(dest), newTree(nkFastAsgn, dest, ri)) proc genCopyNoCheck(c: Con; dest, ri: PNode): PNode = let t = dest.typ.skipTypes({tyGenericInst, tyAlias, tySink}) result = c.genOp(t, attachedAsgn, dest, ri) proc genCopy(c: var Con; dest, ri: PNode): PNode = let t = dest.typ if tfHasOwned in t.flags and ri.kind != nkNilLit: # try to improve the error message here: if c.otherRead == nil: discard isLastRead(ri, c) c.checkForErrorPragma(t, ri, "=") result = c.genCopyNoCheck(dest, ri) proc addTopVar(c: var Con; s: var Scope; v: PNode): ptr Scope = result = addr(s) while v.sym.id in result.escapingSyms and result.parent != nil: result = result.parent result[].vars.add v.sym proc genDiscriminantAsgn(c: var Con; s: var Scope; n: PNode): PNode = # discriminator is ordinal value that doesn't need sink destroy # but fields within active case branch might need destruction # tmp to support self assignments let tmp = c.getTemp(s, n[1].typ, n.info) result = newTree(nkStmtList) result.add newTree(nkFastAsgn, tmp, p(n[1], c, s, consumed)) result.add p(n[0], c, s, normal) let le = p(n[0], c, s, normal) let leDotExpr = if le.kind == nkCheckedFieldExpr: le[0] else: le let objType = leDotExpr[0].typ if hasDestructor(objType): if objType.attachedOps[attachedDestructor] != nil and sfOverriden in objType.attachedOps[attachedDestructor].flags: localError(c.graph.config, n.info, errGenerated, """Assignment to discriminant for objects with user defined destructor is not supported, object must have default destructor. It is best to factor out piece of object that needs custom destructor into separate object or not use discriminator assignment""") result.add newTree(nkFastAsgn, le, tmp) return # generate: if le != tmp: `=destroy`(le) let branchDestructor = produceDestructorForDiscriminator(c.graph, objType, leDotExpr[1].sym, n.info) let cond = newNodeIT(nkInfix, n.info, getSysType(c.graph, unknownLineInfo, tyBool)) cond.add newSymNode(getMagicEqSymForType(c.graph, le.typ, n.info)) cond.add le cond.add tmp let notExpr = newNodeIT(nkPrefix, n.info, getSysType(c.graph, unknownLineInfo, tyBool)) notExpr.add newSymNode(createMagic(c.graph, "not", mNot)) notExpr.add cond result.add newTree(nkIfStmt, newTree(nkElifBranch, notExpr, c.genOp(branchDestructor, le))) result.add newTree(nkFastAsgn, le, tmp) proc genWasMoved(c: var Con, n: PNode): PNode = result = newNodeI(nkCall, n.info) result.add(newSymNode(createMagic(c.graph, "wasMoved", mWasMoved))) result.add copyTree(n) #mWasMoved does not take the address #if n.kind != nkSym: # message(c.graph.config, n.info, warnUser, "wasMoved(" & $n & ")") proc genDefaultCall(t: PType; c: Con; info: TLineInfo): PNode = result = newNodeI(nkCall, info) result.add(newSymNode(createMagic(c.graph, "default", mDefault))) result.typ = t proc destructiveMoveVar(n: PNode; c: var Con; s: var Scope): PNode = # generate: (let tmp = v; reset(v); tmp) if not hasDestructor(n.typ): result = copyTree(n) else: result = newNodeIT(nkStmtListExpr, n.info, n.typ) var temp = newSym(skLet, getIdent(c.graph.cache, "blitTmp"), c.owner, n.info) temp.typ = n.typ var v = newNodeI(nkLetSection, n.info) let tempAsNode = newSymNode(temp) var vpart = newNodeI(nkIdentDefs, tempAsNode.info, 3) vpart[0] = tempAsNode vpart[1] = c.emptyNode vpart[2] = n v.add(vpart) result.add v let wasMovedCall = c.genWasMoved(skipConv(n)) result.add wasMovedCall result.add tempAsNode proc isCapturedVar(n: PNode): bool = let root = getRoot(n) if root != nil: result = root.name.s[0] == ':' proc passCopyToSink(n: PNode; c: var Con; s: var Scope): PNode = result = newNodeIT(nkStmtListExpr, n.info, n.typ) let tmp = c.getTemp(s, n.typ, n.info) if hasDestructor(n.typ): result.add c.genWasMoved(tmp) var m = c.genCopy(tmp, n) m.add p(n, c, s, normal) result.add m if isLValue(n) and not isCapturedVar(n) and n.typ.skipTypes(abstractInst).kind != tyRef and c.inSpawn == 0: message(c.graph.config, n.info, hintPerformance, ("passing '$1' to a sink parameter introduces an implicit copy; " & "if possible, rearrange your program's control flow to prevent it") % $n) else: if c.graph.config.selectedGC in {gcArc, gcOrc}: assert(not containsGarbageCollectedRef(n.typ)) result.add newTree(nkAsgn, tmp, p(n, c, s, normal)) # Since we know somebody will take over the produced copy, there is # no need to destroy it. result.add tmp proc isDangerousSeq(t: PType): bool {.inline.} = let t = t.skipTypes(abstractInst) result = t.kind == tySequence and tfHasOwned notin t[0].flags proc containsConstSeq(n: PNode): bool = if n.kind == nkBracket and n.len > 0 and n.typ != nil and isDangerousSeq(n.typ): return true result = false case n.kind of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv: result = containsConstSeq(n[1]) of nkObjConstr, nkClosure: for i in 1.. 0: markEscapingVarsRec(n[^1], s) else: discard "no arbitrary tree traversal here" proc markEscapingVars(n: PNode; s: var Scope) = markEscapingVarsRec(n, s) var it = n while it.kind in {nkStmtList, nkStmtListExpr} and it.len > 0: it = it[^1] s.escapingExpr = it proc pVarTopLevel(v: PNode; c: var Con; s: var Scope; ri, res: PNode) = # move the variable declaration to the top of the frame: let owningScope = c.addTopVar(s, v) if isUnpackedTuple(v): if c.inLoop > 0: # unpacked tuple needs reset at every loop iteration res.add newTree(nkFastAsgn, v, genDefaultCall(v.typ, c, v.info)) elif sfThread notin v.sym.flags: # do not destroy thread vars for now at all for consistency. if sfGlobal in v.sym.flags and s.parent == nil: c.graph.globalDestructors.add c.genDestroy(v) #No need to genWasMoved here else: owningScope[].final.add c.genDestroy(v) if ri.kind == nkEmpty and c.inLoop > 0: res.add moveOrCopy(v, genDefaultCall(v.typ, c, v.info), c, s, isDecl = true) elif ri.kind != nkEmpty: res.add moveOrCopy(v, ri, c, s, isDecl = true) template handleNestedTempl(n, processCall: untyped; alwaysStmt: bool) = template maybeVoid(child, s): untyped = if isEmptyType(child.typ): p(child, c, s, normal) else: processCall(child, s) let treeFlags = if not isEmptyType(n.typ) and not alwaysStmt: {producesValue} else: {} case n.kind of nkStmtList, nkStmtListExpr: # a statement list does not open a new scope if n.len == 0: return n result = copyNode(n) if alwaysStmt: result.typ = nil for i in 0.. 0: c.inSpawn.dec let parameters = n[0].typ let L = if parameters != nil: parameters.len else: 0 when false: var isDangerous = false if n[0].kind == nkSym and n[0].sym.magic in {mOr, mAnd}: inc c.inDangerousBranch isDangerous = true result = shallowCopy(n) for i in 1.. 0): result[i] = p(n[i], c, s, sinkArg) else: result[i] = p(n[i], c, s, normal) when false: if isDangerous: dec c.inDangerousBranch if n[0].kind == nkSym and n[0].sym.magic in {mNew, mNewFinalize}: result[0] = copyTree(n[0]) if c.graph.config.selectedGC in {gcHooks, gcArc, gcOrc}: let destroyOld = c.genDestroy(result[1]) result = newTree(nkStmtList, destroyOld, result) else: result[0] = p(n[0], c, s, normal) if canRaise(n[0]): s.needsTry = true if mode == normal: result = ensureDestruction(result, n, c, s) of nkDiscardStmt: # Small optimization result = shallowCopy(n) if n[0].kind != nkEmpty: result[0] = p(n[0], c, s, normal) else: result[0] = copyNode(n[0]) of nkVarSection, nkLetSection: # transform; var x = y to var x; x op y where op is a move or copy result = newNodeI(nkStmtList, n.info) for it in n: var ri = it[^1] 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]): for j in 0.. 0: ri = genDefaultCall(v.typ, c, v.info) if ri.kind != nkEmpty: result.add moveOrCopy(v, ri, c, s, isDecl = true) else: # keep the var but transform 'ri': var v = copyNode(n) var itCopy = copyNode(it) for j in 0.. 0 and isDangerousSeq(ri.typ): result = c.genCopy(dest, ri) result.add p(ri, c, s, consumed) else: result = c.genSink(s, dest, p(ri, c, s, consumed), isDecl) of nkObjConstr, nkTupleConstr, nkClosure, nkCharLit..nkNilLit: result = c.genSink(s, dest, p(ri, c, s, consumed), isDecl) of nkSym: if isSinkParam(ri.sym) and isLastRead(ri, c): # Rule 3: `=sink`(x, z); wasMoved(z) let snk = c.genSink(s, 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): # Rule 3: `=sink`(x, z); wasMoved(z) let snk = c.genSink(s, dest, ri, isDecl) result = newTree(nkStmtList, snk, c.genWasMoved(ri)) else: result = c.genCopy(dest, ri) result.add p(ri, c, s, consumed) of nkHiddenSubConv, nkHiddenStdConv, nkConv, nkObjDownConv, nkObjUpConv: result = c.genSink(s, dest, p(ri, c, s, sinkArg), isDecl) of nkStmtListExpr, nkBlockExpr, nkIfExpr, nkCaseStmt: template process(child, s): untyped = moveOrCopy(dest, child, c, s, isDecl) handleNestedTempl(ri, process, true) of nkRaiseStmt: result = pRaiseStmt(ri, c, s) else: if isAnalysableFieldAccess(ri, c.owner) and isLastRead(ri, c) and canBeMoved(c, dest.typ): # Rule 3: `=sink`(x, z); wasMoved(z) let snk = c.genSink(s, dest, ri, isDecl) result = newTree(nkStmtList, snk, c.genWasMoved(ri)) else: result = c.genCopy(dest, ri) result.add p(ri, c, s, consumed) proc computeUninit(c: var Con) = if not c.uninitComputed: c.uninitComputed = true c.uninit = initIntSet() var init = initIntSet() discard initialized(c.g, pc = 0, init, c.uninit, int.high) proc injectDefaultCalls(n: PNode, c: var Con) = case n.kind of nkVarSection, nkLetSection: for it in n: if it.kind == nkIdentDefs and it[^1].kind == nkEmpty: computeUninit(c) for j in 0..---------transformed-to--------->" echo renderTree(result, {renderIds})