# # # The Nim Compiler # (c) Copyright 2015 Andreas Rumpf # # See the file "copying.txt", included in this # distribution, for details about the copyright. # import ast, astalgo, msgs, renderer, magicsys, types, idents, trees, wordrecg, options, guards, lineinfos, semfold, semdata, modulegraphs, varpartitions, typeallowed, nilcheck, errorhandling, semstrictfuncs, suggestsymdb, pushpoppragmas import std/[tables, intsets, strutils, sequtils] when defined(nimPreviewSlimSystem): import std/assertions when defined(useDfa): import dfa import liftdestructors include sinkparameter_inference #[ Second semantic checking pass over the AST. Necessary because the old way had some inherent problems. Performs: * effect+exception tracking * "usage before definition" checking * also now calls the "lift destructor logic" at strategic positions, this is about to be put into the spec: We treat assignment and sinks and destruction as identical. In the construct let/var x = expr() x's type is marked. In x = y the type of x is marked. For every sink parameter of type T T is marked. For every call f() the return type of f() is marked. ]# # ------------------------ exception and tag tracking ------------------------- discard """ exception tracking: a() # raises 'x', 'e' try: b() # raises 'e' except e: # must not undo 'e' here; hrm c() --> we need a stack of scopes for this analysis # XXX enhance the algorithm to care about 'dirty' expressions: lock a[i].L: inc i # mark 'i' dirty lock a[j].L: access a[i], a[j] # --> reject a[i] """ type CaughtExceptionsStack = object nodes: seq[seq[PType]] TEffects = object exc: PNode # stack of exceptions when defined(nimsuggest): caughtExceptions: CaughtExceptionsStack tags: PNode # list of tags forbids: PNode # list of tags bottom, inTryStmt, inExceptOrFinallyStmt, leftPartOfAsgn, inIfStmt, currentBlock: int owner: PSym ownerModule: PSym init: seq[int] # list of initialized variables scopes: Table[int, int] # maps var-id to its scope (see also `currentBlock`). guards: TModel # nested guards locked: seq[PNode] # locked locations gcUnsafe, isRecursive, isTopLevel, hasSideEffect, inEnforcedGcSafe: bool isInnerProc: bool inEnforcedNoSideEffects: bool currOptions: TOptions optionsStack: seq[(TOptions, TNoteKinds)] config: ConfigRef graph: ModuleGraph c: PContext escapingParams: IntSet PEffects = var TEffects const errXCannotBeAssignedTo = "'$1' cannot be assigned to" errLetNeedsInit = "'let' symbol requires an initialization" proc getObjDepth(t: PType): (int, ItemId) = var x = t result = (-1, default(ItemId)) var stack = newSeq[ItemId]() while x != nil: x = skipTypes(x, skipPtrs) if x.kind != tyObject: return (-3, default(ItemId)) stack.add x.itemId x = x.baseClass inc(result[0]) result[1] = stack[^2] proc collectObjectTree(graph: ModuleGraph, n: PNode) = for section in n: if section.kind == nkTypeDef and section[^1].kind in {nkObjectTy, nkRefTy, nkPtrTy} and section[^1].typ != nil: let typ = section[^1].typ.skipTypes(skipPtrs) if typ.kind == tyObject and typ.baseClass != nil: let (depthLevel, root) = getObjDepth(typ) if depthLevel != -3: if depthLevel == 1: graph.objectTree[root] = @[] else: if root notin graph.objectTree: graph.objectTree[root] = @[(depthLevel, typ)] else: graph.objectTree[root].add (depthLevel, typ) proc createTypeBoundOps(tracked: PEffects, typ: PType; info: TLineInfo) = if typ == nil or sfGeneratedOp in tracked.owner.flags: # don't create type bound ops for anything in a function with a `nodestroy` pragma # bug #21987 return when false: let realType = typ.skipTypes(abstractInst) if realType.kind == tyRef and optSeqDestructors in tracked.config.globalOptions: createTypeBoundOps(tracked.graph, tracked.c, realType.lastSon, info) createTypeBoundOps(tracked.graph, tracked.c, typ, info, tracked.c.idgen) if (tfHasAsgn in typ.flags) or optSeqDestructors in tracked.config.globalOptions: tracked.owner.flags.incl sfInjectDestructors proc isLocalSym(a: PEffects, s: PSym): bool = s.typ != nil and (s.kind in {skLet, skVar, skResult} or (s.kind == skParam and isOutParam(s.typ))) and sfGlobal notin s.flags and s.owner == a.owner proc lockLocations(a: PEffects; pragma: PNode) = if pragma.kind != nkExprColonExpr: localError(a.config, pragma.info, "locks pragma without argument") return for x in pragma[1]: a.locked.add x proc guardGlobal(a: PEffects; n: PNode; guard: PSym) = # check whether the corresponding lock is held: for L in a.locked: if L.kind == nkSym and L.sym == guard: return # we allow accesses nevertheless in top level statements for # easier initialization: #if a.isTopLevel: # message(a.config, n.info, warnUnguardedAccess, renderTree(n)) #else: if not a.isTopLevel: localError(a.config, n.info, "unguarded access: " & renderTree(n)) # 'guard*' are checks which are concerned with 'guard' annotations # (var x{.guard: y.}: int) proc guardDotAccess(a: PEffects; n: PNode) = let ri = n[1] if ri.kind != nkSym or ri.sym.kind != skField: return var g = ri.sym.guard if g.isNil or a.isTopLevel: return # fixup guard: if g.kind == skUnknown: var field: PSym = nil var ty = n[0].typ.skipTypes(abstractPtrs) if ty.kind == tyTuple and not ty.n.isNil: field = lookupInRecord(ty.n, g.name) else: while ty != nil and ty.kind == tyObject: field = lookupInRecord(ty.n, g.name) if field != nil: break ty = ty[0] if ty == nil: break ty = ty.skipTypes(skipPtrs) if field == nil: localError(a.config, n.info, "invalid guard field: " & g.name.s) return g = field #ri.sym.guard = field # XXX unfortunately this is not correct for generic instantiations! if g.kind == skField: let dot = newNodeI(nkDotExpr, n.info, 2) dot[0] = n[0] dot[1] = newSymNode(g) dot.typ = g.typ for L in a.locked: #if a.guards.sameSubexprs(dot, L): return if guards.sameTree(dot, L): return localError(a.config, n.info, "unguarded access: " & renderTree(n)) else: guardGlobal(a, n, g) proc makeVolatile(a: PEffects; s: PSym) {.inline.} = if a.inTryStmt > 0 and a.config.exc == excSetjmp: incl(s.flags, sfVolatile) proc varDecl(a: PEffects; n: PNode) {.inline.} = if n.kind == nkSym: a.scopes[n.sym.id] = a.currentBlock proc skipHiddenDeref(n: PNode): PNode {.inline.} = result = if n.kind == nkHiddenDeref: n[0] else: n proc initVar(a: PEffects, n: PNode; volatileCheck: bool) = let n = skipHiddenDeref(n) if n.kind != nkSym: return let s = n.sym if isLocalSym(a, s): if volatileCheck: makeVolatile(a, s) for x in a.init: if x == s.id: if strictDefs in a.c.features and s.kind == skLet: localError(a.config, n.info, errXCannotBeAssignedTo % renderTree(n, {renderNoComments} )) return a.init.add s.id if a.scopes.getOrDefault(s.id) == a.currentBlock: #[ Consider this case: var x: T while true: if cond: x = T() #1 else: x = T() #2 use x Even though both #1 and #2 are first writes we must use the `=copy` here so that the old value is destroyed because `x`'s destructor is run outside of the while loop. This is why we need the check here that the assignment is done in the same logical block as `x` was declared in. ]# n.flags.incl nfFirstWrite proc initVarViaNew(a: PEffects, n: PNode) = let n = skipHiddenDeref(n) if n.kind != nkSym: return let s = n.sym if {tfRequiresInit, tfNotNil} * s.typ.flags <= {tfNotNil}: # 'x' is not nil, but that doesn't mean its "not nil" children # are initialized: initVar(a, n, volatileCheck=true) elif isLocalSym(a, s): makeVolatile(a, s) proc warnAboutGcUnsafe(n: PNode; conf: ConfigRef) = #assert false message(conf, n.info, warnGcUnsafe, renderTree(n)) proc markGcUnsafe(a: PEffects; reason: PSym) = if not a.inEnforcedGcSafe: a.gcUnsafe = true if a.owner.kind in routineKinds: a.owner.gcUnsafetyReason = reason proc markGcUnsafe(a: PEffects; reason: PNode) = if not a.inEnforcedGcSafe: a.gcUnsafe = true if a.owner.kind in routineKinds: if reason.kind == nkSym: a.owner.gcUnsafetyReason = reason.sym else: a.owner.gcUnsafetyReason = newSym(skUnknown, a.owner.name, a.c.idgen, a.owner, reason.info, {}) proc markSideEffect(a: PEffects; reason: PNode | PSym; useLoc: TLineInfo) = if not a.inEnforcedNoSideEffects: a.hasSideEffect = true if a.owner.kind in routineKinds: var sym: PSym when reason is PNode: if reason.kind == nkSym: sym = reason.sym else: let kind = if reason.kind == nkHiddenDeref: skParam else: skUnknown sym = newSym(kind, a.owner.name, a.c.idgen, a.owner, reason.info, {}) else: sym = reason a.c.sideEffects.mgetOrPut(a.owner.id, @[]).add (useLoc, sym) when false: markGcUnsafe(a, reason) proc listGcUnsafety(s: PSym; onlyWarning: bool; cycleCheck: var IntSet; conf: ConfigRef) = let u = s.gcUnsafetyReason if u != nil and not cycleCheck.containsOrIncl(u.id): let msgKind = if onlyWarning: warnGcUnsafe2 else: errGenerated case u.kind of skLet, skVar: if u.typ.skipTypes(abstractInst).kind == tyProc: message(conf, s.info, msgKind, "'$#' is not GC-safe as it calls '$#'" % [s.name.s, u.name.s]) else: message(conf, s.info, msgKind, ("'$#' is not GC-safe as it accesses '$#'" & " which is a global using GC'ed memory") % [s.name.s, u.name.s]) of routineKinds: # recursive call *always* produces only a warning so the full error # message is printed: if u.kind == skMethod and {sfBase, sfThread} * u.flags == {sfBase}: message(conf, u.info, msgKind, "Base method '$#' requires explicit '{.gcsafe.}' to be GC-safe" % [u.name.s]) else: listGcUnsafety(u, true, cycleCheck, conf) message(conf, s.info, msgKind, "'$#' is not GC-safe as it calls '$#'" % [s.name.s, u.name.s]) of skParam, skForVar: message(conf, s.info, msgKind, "'$#' is not GC-safe as it performs an indirect call via '$#'" % [s.name.s, u.name.s]) else: message(conf, u.info, msgKind, "'$#' is not GC-safe as it performs an indirect call here" % s.name.s) proc listGcUnsafety(s: PSym; onlyWarning: bool; conf: ConfigRef) = var cycleCheck = initIntSet() listGcUnsafety(s, onlyWarning, cycleCheck, conf) proc listSideEffects(result: var string; s: PSym; cycleCheck: var IntSet; conf: ConfigRef; context: PContext; indentLevel: int) = template addHint(msg; lineInfo; sym; level = indentLevel) = result.addf("$# $# Hint: '$#' $#\n", repeat(">", level), conf $ lineInfo, sym, msg) if context.sideEffects.hasKey(s.id): for (useLineInfo, u) in context.sideEffects[s.id]: if u != nil and not cycleCheck.containsOrIncl(u.id): case u.kind of skLet, skVar: addHint("accesses global state '$#'" % u.name.s, useLineInfo, s.name.s) addHint("accessed by '$#'" % s.name.s, u.info, u.name.s, indentLevel + 1) of routineKinds: addHint("calls `.sideEffect` '$#'" % u.name.s, useLineInfo, s.name.s) addHint("called by '$#'" % s.name.s, u.info, u.name.s, indentLevel + 1) listSideEffects(result, u, cycleCheck, conf, context, indentLevel + 2) of skParam, skForVar: addHint("calls routine via hidden pointer indirection", useLineInfo, s.name.s) else: addHint("calls routine via pointer indirection", useLineInfo, s.name.s) proc listSideEffects(result: var string; s: PSym; conf: ConfigRef; context: PContext) = var cycleCheck = initIntSet() result.addf("'$#' can have side effects\n", s.name.s) listSideEffects(result, s, cycleCheck, conf, context, 1) proc useVarNoInitCheck(a: PEffects; n: PNode; s: PSym) = if {sfGlobal, sfThread} * s.flags != {} and s.kind in {skVar, skLet} and s.magic != mNimvm: if s.guard != nil: guardGlobal(a, n, s.guard) if {sfGlobal, sfThread} * s.flags == {sfGlobal} and (tfHasGCedMem in s.typ.flags or s.typ.isGCedMem): #if a.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n) markGcUnsafe(a, s) markSideEffect(a, s, n.info) if s.owner != a.owner and s.kind in {skVar, skLet, skForVar, skResult, skParam} and {sfGlobal, sfThread} * s.flags == {}: a.isInnerProc = true proc useVar(a: PEffects, n: PNode) = let s = n.sym if a.inExceptOrFinallyStmt > 0: incl s.flags, sfUsedInFinallyOrExcept if isLocalSym(a, s): if sfNoInit in s.flags: # If the variable is explicitly marked as .noinit. do not emit any error a.init.add s.id elif s.id notin a.init: if s.typ.requiresInit: message(a.config, n.info, warnProveInit, s.name.s) elif a.leftPartOfAsgn <= 0: if strictDefs in a.c.features: if s.kind == skLet: localError(a.config, n.info, errLetNeedsInit) else: message(a.config, n.info, warnUninit, s.name.s) # prevent superfluous warnings about the same variable: a.init.add s.id useVarNoInitCheck(a, n, s) type BreakState = enum bsNone bsBreakOrReturn bsNoReturn type TIntersection = seq[tuple[id, count: int]] # a simple count table proc addToIntersection(inter: var TIntersection, s: int, state: BreakState) = for j in 0.. 0: setLen(tracked.exc.sons, L) else: assert L == 0 proc catchesAll(tracked: PEffects) = if tracked.exc.len > 0: setLen(tracked.exc.sons, tracked.bottom) proc push(s: var CaughtExceptionsStack) = s.nodes.add(@[]) proc pop(s: var CaughtExceptionsStack) = s.nodes.del(high(s.nodes)) proc addCatch(s: var CaughtExceptionsStack, e: PType) = s.nodes[high(s.nodes)].add(e) proc addCatchAll(s: var CaughtExceptionsStack) = s.nodes[high(s.nodes)].add(nil) proc track(tracked: PEffects, n: PNode) proc trackTryStmt(tracked: PEffects, n: PNode) = let oldBottom = tracked.bottom tracked.bottom = tracked.exc.len let oldState = tracked.init.len var inter: TIntersection = @[] when defined(nimsuggest): tracked.caughtExceptions.push for i in 1.. 0: it = it.lastSon case it.kind of nkBreakStmt, nkReturnStmt: result = bsBreakOrReturn of nkRaiseStmt: result = bsNoReturn of nkCallKinds: if it[0].kind == nkSym and sfNoReturn in it[0].sym.flags: result = bsNoReturn else: result = bsNone else: result = bsNone proc addIdToIntersection(tracked: PEffects, inter: var TIntersection, resCounter: var int, hasBreaksBlock: BreakState, oldState: int, resSym: PSym, hasResult: bool) = if hasResult: var alreadySatisfy = false if hasBreaksBlock == bsNoReturn: alreadySatisfy = true inc resCounter for i in oldState..= toCover: tracked.init.add id # else we can't merge setLen(tracked.guards.s, oldFacts) dec tracked.inIfStmt proc trackIf(tracked: PEffects, n: PNode) = track(tracked, n[0][0]) inc tracked.inIfStmt let oldFacts = tracked.guards.s.len addFact(tracked.guards, n[0][0]) let oldState = tracked.init.len let hasResult = hasResultSym(tracked.owner) let resSym = if hasResult: tracked.owner.ast[resultPos].sym else: nil var resCounter = 0 var inter: TIntersection = @[] var toCover = 0 track(tracked, n[0][1]) let hasBreaksBlock = breaksBlock(n[0][1]) if hasBreaksBlock == bsNone: inc toCover addIdToIntersection(tracked, inter, resCounter, hasBreaksBlock, oldState, resSym, hasResult) for i in 1.. 1: addFact(tracked.guards, branch[0]) setLen(tracked.init, oldState) for i in 0..= toCover: tracked.init.add id # else we can't merge as it is not exhaustive setLen(tracked.guards.s, oldFacts) dec tracked.inIfStmt proc trackBlock(tracked: PEffects, n: PNode) = if n.kind in {nkStmtList, nkStmtListExpr}: var oldState = -1 for i in 0.. 'y' not defined after block! if oldState < 0: oldState = tracked.init.len track(tracked, n[i]) if oldState > 0: setLen(tracked.init, oldState) else: track(tracked, n) proc cstringCheck(tracked: PEffects; n: PNode) = if n[0].typ.kind == tyCstring and (let a = skipConv(n[1]); a.typ.kind == tyString and a.kind notin {nkStrLit..nkTripleStrLit}): message(tracked.config, n.info, warnUnsafeCode, renderTree(n)) proc patchResult(c: PEffects; n: PNode) = if n.kind == nkSym and n.sym.kind == skResult: let fn = c.owner if fn != nil and fn.kind in routineKinds and fn.ast != nil and resultPos < fn.ast.len: n.sym = fn.ast[resultPos].sym else: localError(c.config, n.info, "routine has no return type, but .requires contains 'result'") else: for i in 0.. " & $b) proc checkBounds(c: PEffects; arr, idx: PNode) = checkLe(c, lowBound(c.config, arr), idx) checkLe(c, idx, highBound(c.config, arr, c.guards.g.operators)) proc checkRange(c: PEffects; value: PNode; typ: PType) = let t = typ.skipTypes(abstractInst - {tyRange}) if t.kind == tyRange: let lowBound = copyTree(t.n[0]) lowBound.info = value.info let highBound = copyTree(t.n[1]) highBound.info = value.info checkLe(c, lowBound, value) checkLe(c, value, highBound) #[ proc passedToEffectsDelayedParam(tracked: PEffects; n: PNode) = let t = n.typ.skipTypes(abstractInst) if t.kind == tyProc: if n.kind == nkSym and tracked.owner == n.sym.owner and sfEffectsDelayed in n.sym.flags: discard "the arg is itself a delayed parameter, so do nothing" else: var effectList = t.n[0] if effectList.len == effectListLen: mergeRaises(tracked, effectList[exceptionEffects], n) mergeTags(tracked, effectList[tagEffects], n) if not importedFromC(n): if notGcSafe(t): if tracked.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n, tracked.config) markGcUnsafe(tracked, n) if tfNoSideEffect notin t.flags: markSideEffect(tracked, n, n.info) ]# proc checkForSink(tracked: PEffects; n: PNode) = if tracked.inIfStmt == 0 and optSinkInference in tracked.config.options: checkForSink(tracked.config, tracked.c.idgen, tracked.owner, n) proc markCaughtExceptions(tracked: PEffects; g: ModuleGraph; info: TLineInfo; s: PSym; usageSym: var PSym) = when defined(nimsuggest): proc internalMarkCaughtExceptions(tracked: PEffects; q: var SuggestFileSymbolDatabase; info: TLineInfo) = var si = q.findSymInfoIndex(info) if si != -1: q.caughtExceptionsSet[si] = true for w1 in tracked.caughtExceptions.nodes: for w2 in w1: q.caughtExceptions[si].add(w2) if optIdeExceptionInlayHints in tracked.config.globalOptions: internalMarkCaughtExceptions(tracked, g.suggestSymbols.mgetOrPut(info.fileIndex, newSuggestFileSymbolDatabase(info.fileIndex, true)), info) proc trackCall(tracked: PEffects; n: PNode) = template gcsafeAndSideeffectCheck() = if notGcSafe(op) and not importedFromC(a): # and it's not a recursive call: if not (a.kind == nkSym and a.sym == tracked.owner): if tracked.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n, tracked.config) markGcUnsafe(tracked, a) if tfNoSideEffect notin op.flags and not importedFromC(a): # and it's not a recursive call: if not (a.kind == nkSym and a.sym == tracked.owner): markSideEffect(tracked, a, n.info) # p's effects are ours too: var a = n[0] #if canRaise(a): # echo "this can raise ", tracked.config $ n.info let op = a.typ if n.typ != nil: if tracked.owner.kind != skMacro and n.typ.skipTypes(abstractVar).kind != tyOpenArray: createTypeBoundOps(tracked, n.typ, n.info) when defined(nimsuggest): var actualLoc = a.info if n.kind == nkHiddenCallConv: actualLoc = n.info if a.kind == nkSym: markCaughtExceptions(tracked, tracked.graph, actualLoc, a.sym, tracked.graph.usageSym) let notConstExpr = getConstExpr(tracked.ownerModule, n, tracked.c.idgen, tracked.graph) == nil if notConstExpr: if a.kind == nkCast and a[1].typ.kind == tyProc: a = a[1] # XXX: in rare situations, templates and macros will reach here after # calling getAst(templateOrMacro()). Currently, templates and macros # are indistinguishable from normal procs (both have tyProc type) and # we can detect them only by checking for attached nkEffectList. if op != nil and op.kind == tyProc and op.n[0].kind == nkEffectList: if a.kind == nkSym: if a.sym == tracked.owner: tracked.isRecursive = true # even for recursive calls we need to check the lock levels (!): if sfSideEffect in a.sym.flags: markSideEffect(tracked, a, n.info) else: discard var effectList = op.n[0] if a.kind == nkSym and a.sym.kind == skMethod: if {sfBase, sfThread} * a.sym.flags == {sfBase}: if tracked.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n, tracked.config) markGcUnsafe(tracked, a) propagateEffects(tracked, n, a.sym) elif isNoEffectList(effectList): if isForwardedProc(a): propagateEffects(tracked, n, a.sym) elif isIndirectCall(tracked, a): assumeTheWorst(tracked, n, op) gcsafeAndSideeffectCheck() else: if laxEffects notin tracked.c.config.legacyFeatures and a.kind == nkSym and a.sym.kind in routineKinds: propagateEffects(tracked, n, a.sym) else: mergeRaises(tracked, effectList[exceptionEffects], n) mergeTags(tracked, effectList[tagEffects], n) gcsafeAndSideeffectCheck() if a.kind != nkSym or a.sym.magic notin {mNBindSym, mFinished, mExpandToAst, mQuoteAst}: for i in 1.. 0 and a.sym.name.s[0] == '=' and tracked.owner.kind != skMacro: var opKind = find(AttachedOpToStr, a.sym.name.s.normalize) if a.sym.name.s == "=": opKind = attachedAsgn.int if opKind != -1: # rebind type bounds operations after createTypeBoundOps call let t = n[1].typ.skipTypes({tyAlias, tyVar}) if a.sym != getAttachedOp(tracked.graph, t, TTypeAttachedOp(opKind)): createTypeBoundOps(tracked, t, n.info) let op = getAttachedOp(tracked.graph, t, TTypeAttachedOp(opKind)) if op != nil: n[0].sym = op if op != nil and op.kind == tyProc: for i in 1.. 1: # XXX this is a bit hacky: if iterCall[1].typ != nil and iterCall[1].typ.skipTypes(abstractVar).kind notin {tyVarargs, tyOpenArray}: createTypeBoundOps(tracked, iterCall[1].typ, iterCall[1].info) track(tracked, iterCall) track(tracked, loopBody) setLen(tracked.init, oldState) setLen(tracked.guards.s, oldFacts) dec tracked.currentBlock of nkObjConstr: when false: track(tracked, n[0]) let oldFacts = tracked.guards.s.len for i in 1.. 0: rr = rr.lastSon message(g.config, r.info, if emitWarnings: warnEffect else: errGenerated, renderTree(rr) & " " & msg & typeToString(r.typ)) popInfoContext(g.config) # hint about unnecessarily listed exception types: if hints: for s in 0..