Files
Nim/compiler/sempass2.nim
ringabout d13aab50cf fixes branches interacting with break, raise etc. in strictdefs (#22627)
```nim
{.experimental: "strictdefs".}

type Test = object
  id: int

proc test(): Test =
  if true:
    return Test()
  else:
    return
echo test()
```

I will tackle https://github.com/nim-lang/Nim/issues/16735 and #21615 in
the following PR.


The old code just premises that in branches ended with returns, raise
statements etc. , all variables including the result variable are
initialized for that branch. It's true for noreturn statements. But it
is false for the result variable in a branch tailing with a return
statement, in which the result variable is not initialized. The solution
is not perfect for usages below branch statements with the result
variable uninitialized, but it should suffice for now, which gives a
proper warning.

It also fixes

```nim

{.experimental: "strictdefs".}

type Test = object
  id: int

proc foo {.noreturn.} = discard

proc test9(x: bool): Test =
  if x:
    foo()
  else:
    foo()
```
which gives a warning, but shouldn't
2023-09-04 14:36:45 +02:00

1648 lines
61 KiB
Nim

#
#
# The Nim Compiler
# (c) Copyright 2015 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
import
intsets, ast, astalgo, msgs, renderer, magicsys, types, idents, trees,
wordrecg, strutils, options, guards, lineinfos, semfold, semdata,
modulegraphs, varpartitions, typeallowed, nilcheck, errorhandling, tables,
semstrictfuncs
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
TEffects = object
exc: PNode # stack of exceptions
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
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 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..<inter.len:
if s == inter[j].id:
if state == bsNone:
inc inter[j].count
return
if state == bsNone:
inter.add((id: s, count: 1))
else:
inter.add((id: s, count: 0))
proc throws(tracked, n, orig: PNode) =
if n.typ == nil or n.typ.kind != tyError:
if orig != nil:
let x = copyTree(orig)
x.typ = n.typ
tracked.add x
else:
tracked.add n
proc getEbase(g: ModuleGraph; info: TLineInfo): PType =
result = g.sysTypeFromName(info, "Exception")
proc excType(g: ModuleGraph; n: PNode): PType =
# reraise is like raising E_Base:
let t = if n.kind == nkEmpty or n.typ.isNil: getEbase(g, n.info) else: n.typ
result = skipTypes(t, skipPtrs)
proc createRaise(g: ModuleGraph; n: PNode): PNode =
result = newNode(nkType)
result.typ = getEbase(g, n.info)
if not n.isNil: result.info = n.info
proc createTag(g: ModuleGraph; n: PNode): PNode =
result = newNode(nkType)
result.typ = g.sysTypeFromName(n.info, "RootEffect")
if not n.isNil: result.info = n.info
proc addRaiseEffect(a: PEffects, e, comesFrom: PNode) =
#assert e.kind != nkRaiseStmt
var aa = a.exc
for i in a.bottom..<aa.len:
# we only track the first node that can have the effect E in order
# to safe space and time.
if sameType(a.graph.excType(aa[i]), a.graph.excType(e)): return
if e.typ != nil:
if not isDefectException(e.typ):
throws(a.exc, e, comesFrom)
proc addTag(a: PEffects, e, comesFrom: PNode) =
var aa = a.tags
for i in 0..<aa.len:
# we only track the first node that can have the effect E in order
# to safe space and time.
if sameType(aa[i].typ.skipTypes(skipPtrs), e.typ.skipTypes(skipPtrs)): return
throws(a.tags, e, comesFrom)
proc addNotTag(a: PEffects, e, comesFrom: PNode) =
var aa = a.forbids
for i in 0..<aa.len:
if sameType(aa[i].typ.skipTypes(skipPtrs), e.typ.skipTypes(skipPtrs)): return
throws(a.forbids, e, comesFrom)
proc mergeRaises(a: PEffects, b, comesFrom: PNode) =
if b.isNil:
addRaiseEffect(a, createRaise(a.graph, comesFrom), comesFrom)
else:
for effect in items(b): addRaiseEffect(a, effect, comesFrom)
proc mergeTags(a: PEffects, b, comesFrom: PNode) =
if b.isNil:
addTag(a, createTag(a.graph, comesFrom), comesFrom)
else:
for effect in items(b): addTag(a, effect, comesFrom)
proc listEffects(a: PEffects) =
for e in items(a.exc): message(a.config, e.info, hintUser, typeToString(e.typ))
for e in items(a.tags): message(a.config, e.info, hintUser, typeToString(e.typ))
for e in items(a.forbids): message(a.config, e.info, hintUser, typeToString(e.typ))
proc catches(tracked: PEffects, e: PType) =
let e = skipTypes(e, skipPtrs)
var L = tracked.exc.len
var i = tracked.bottom
while i < L:
# r supertype of e?
if safeInheritanceDiff(tracked.graph.excType(tracked.exc[i]), e) <= 0:
tracked.exc[i] = tracked.exc[L-1]
dec L
else:
inc i
if tracked.exc.len > 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 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 = @[]
inc tracked.inTryStmt
track(tracked, n[0])
dec tracked.inTryStmt
for i in oldState..<tracked.init.len:
addToIntersection(inter, tracked.init[i], bsNone)
var branches = 1
var hasFinally = false
inc tracked.inExceptOrFinallyStmt
# Collect the exceptions caught by the except branches
for i in 1..<n.len:
let b = n[i]
if b.kind == nkExceptBranch:
inc branches
if b.len == 1:
catchesAll(tracked)
else:
for j in 0..<b.len - 1:
if b[j].isInfixAs():
assert(b[j][1].kind == nkType)
catches(tracked, b[j][1].typ)
createTypeBoundOps(tracked, b[j][2].typ, b[j][2].info)
else:
assert(b[j].kind == nkType)
catches(tracked, b[j].typ)
else:
assert b.kind == nkFinally
# Add any other exception raised in the except bodies
for i in 1..<n.len:
let b = n[i]
if b.kind == nkExceptBranch:
setLen(tracked.init, oldState)
for j in 0..<b.len - 1:
if b[j].isInfixAs(): # skips initialization checks
assert(b[j][2].kind == nkSym)
tracked.init.add b[j][2].sym.id
track(tracked, b[^1])
for i in oldState..<tracked.init.len:
addToIntersection(inter, tracked.init[i], bsNone)
else:
setLen(tracked.init, oldState)
track(tracked, b[^1])
hasFinally = true
tracked.bottom = oldBottom
dec tracked.inExceptOrFinallyStmt
if not hasFinally:
setLen(tracked.init, oldState)
for id, count in items(inter):
if count == branches: tracked.init.add id
proc isIndirectCall(tracked: PEffects; n: PNode): bool =
# we don't count f(...) as an indirect call if 'f' is an parameter.
# Instead we track expressions of type tyProc too. See the manual for
# details:
if n.kind != nkSym:
result = true
elif n.sym.kind == skParam:
if laxEffects notin tracked.c.config.legacyFeatures:
if tracked.owner == n.sym.owner and sfEffectsDelayed in n.sym.flags:
result = false # it is not a harmful call
else:
result = true
else:
result = tracked.owner != n.sym.owner or tracked.owner == nil
elif n.sym.kind notin routineKinds:
result = true
else:
result = false
proc isForwardedProc(n: PNode): bool =
result = n.kind == nkSym and sfForward in n.sym.flags
proc trackPragmaStmt(tracked: PEffects, n: PNode) =
for i in 0..<n.len:
var it = n[i]
let pragma = whichPragma(it)
if pragma == wEffects:
# list the computed effects up to here:
listEffects(tracked)
template notGcSafe(t): untyped = {tfGcSafe, tfNoSideEffect} * t.flags == {}
proc importedFromC(n: PNode): bool =
# when imported from C, we assume GC-safety.
result = n.kind == nkSym and sfImportc in n.sym.flags
proc propagateEffects(tracked: PEffects, n: PNode, s: PSym) =
let pragma = s.ast[pragmasPos]
let spec = effectSpec(pragma, wRaises)
mergeRaises(tracked, spec, n)
let tagSpec = effectSpec(pragma, wTags)
mergeTags(tracked, tagSpec, n)
if notGcSafe(s.typ) and sfImportc notin s.flags:
if tracked.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n, tracked.config)
markGcUnsafe(tracked, s)
if tfNoSideEffect notin s.typ.flags:
markSideEffect(tracked, s, n.info)
proc procVarCheck(n: PNode; conf: ConfigRef) =
if n.kind in nkSymChoices:
for x in n: procVarCheck(x, conf)
elif n.kind == nkSym and n.sym.magic != mNone and n.sym.kind in routineKinds:
localError(conf, n.info, ("'$1' is a built-in and cannot be used as " &
"a first-class procedure") % n.sym.name.s)
proc notNilCheck(tracked: PEffects, n: PNode, paramType: PType) =
let n = n.skipConv
if paramType.isNil or paramType.kind != tyTypeDesc:
procVarCheck skipConvCastAndClosure(n), tracked.config
#elif n.kind in nkSymChoices:
# echo "came here"
let paramType = paramType.skipTypesOrNil(abstractInst)
if paramType != nil and tfNotNil in paramType.flags and n.typ != nil:
let ntyp = n.typ.skipTypesOrNil({tyVar, tyLent, tySink})
if ntyp != nil and tfNotNil notin ntyp.flags:
if isAddrNode(n):
# addr(x[]) can't be proven, but addr(x) can:
if not containsNode(n, {nkDerefExpr, nkHiddenDeref}): return
elif (n.kind == nkSym and n.sym.kind in routineKinds) or
(n.kind in procDefs+{nkObjConstr, nkBracket, nkClosure, nkStrLit..nkTripleStrLit}) or
(n.kind in nkCallKinds and n[0].kind == nkSym and n[0].sym.magic == mArrToSeq) or
n.typ.kind == tyTypeDesc:
# 'p' is not nil obviously:
return
case impliesNotNil(tracked.guards, n)
of impUnknown:
message(tracked.config, n.info, errGenerated,
"cannot prove '$1' is not nil" % n.renderTree)
of impNo:
message(tracked.config, n.info, errGenerated,
"'$1' is provably nil" % n.renderTree)
of impYes: discard
proc assumeTheWorst(tracked: PEffects; n: PNode; op: PType) =
addRaiseEffect(tracked, createRaise(tracked.graph, n), nil)
addTag(tracked, createTag(tracked.graph, n), nil)
proc isOwnedProcVar(tracked: PEffects; n: PNode): bool =
# XXX prove the soundness of this effect system rule
result = n.kind == nkSym and n.sym.kind == skParam and
tracked.owner == n.sym.owner
#if result and sfPolymorphic notin n.sym.flags:
# echo tracked.config $ n.info, " different here!"
if laxEffects notin tracked.c.config.legacyFeatures:
result = result and sfEffectsDelayed in n.sym.flags
proc isNoEffectList(n: PNode): bool {.inline.} =
assert n.kind == nkEffectList
n.len == 0 or (n[tagEffects] == nil and n[exceptionEffects] == nil and n[forbiddenEffects] == nil)
proc isTrival(caller: PNode): bool {.inline.} =
result = caller.kind == nkSym and caller.sym.magic in {mEqProc, mIsNil, mMove, mWasMoved, mSwap}
proc trackOperandForIndirectCall(tracked: PEffects, n: PNode, formals: PType; argIndex: int; caller: PNode) =
let a = skipConvCastAndClosure(n)
let op = a.typ
let param = if formals != nil and argIndex < formals.len and formals.n != nil: formals.n[argIndex].sym else: nil
# assume indirect calls are taken here:
if op != nil and op.kind == tyProc and n.skipConv.kind != nkNilLit and
not isTrival(caller) and
((param != nil and sfEffectsDelayed in param.flags) or laxEffects in tracked.c.config.legacyFeatures):
internalAssert tracked.config, op.n[0].kind == nkEffectList
var effectList = op.n[0]
var s = n.skipConv
if s.kind == nkCast and s[1].typ.kind == tyProc:
s = s[1]
if s.kind == nkSym and s.sym.kind in routineKinds and isNoEffectList(effectList):
propagateEffects(tracked, n, s.sym)
elif isNoEffectList(effectList):
if isForwardedProc(n):
# we have no explicit effects but it's a forward declaration and so it's
# stated there are no additional effects, so simply propagate them:
propagateEffects(tracked, n, n.sym)
elif not isOwnedProcVar(tracked, a):
# we have no explicit effects so assume the worst:
assumeTheWorst(tracked, n, op)
# assume GcUnsafe unless in its type; 'forward' does not matter:
if notGcSafe(op) and not isOwnedProcVar(tracked, a):
if tracked.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n, tracked.config)
markGcUnsafe(tracked, a)
elif tfNoSideEffect notin op.flags and not isOwnedProcVar(tracked, a):
markSideEffect(tracked, a, n.info)
else:
mergeRaises(tracked, effectList[exceptionEffects], n)
mergeTags(tracked, effectList[tagEffects], n)
if notGcSafe(op):
if tracked.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n, tracked.config)
markGcUnsafe(tracked, a)
elif tfNoSideEffect notin op.flags:
markSideEffect(tracked, a, n.info)
let paramType = if formals != nil and argIndex < formals.len: formals[argIndex] else: nil
if paramType != nil and paramType.kind in {tyVar}:
invalidateFacts(tracked.guards, n)
if n.kind == nkSym and isLocalSym(tracked, n.sym):
makeVolatile(tracked, n.sym)
if paramType != nil and paramType.kind == tyProc and tfGcSafe in paramType.flags:
let argtype = skipTypes(a.typ, abstractInst)
# XXX figure out why this can be a non tyProc here. See httpclient.nim for an
# example that triggers it.
if argtype.kind == tyProc and notGcSafe(argtype) and not tracked.inEnforcedGcSafe:
localError(tracked.config, n.info, $n & " is not GC safe")
notNilCheck(tracked, n, paramType)
proc breaksBlock(n: PNode): BreakState =
# semantic check doesn't allow statements after raise, break, return or
# call to noreturn proc, so it is safe to check just the last statements
var it = n
while it.kind in {nkStmtList, nkStmtListExpr} and it.len > 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..<tracked.init.len:
if tracked.init[i] == resSym.id:
if not alreadySatisfy:
inc resCounter
alreadySatisfy = true
else:
addToIntersection(inter, tracked.init[i], hasBreaksBlock)
else:
for i in oldState..<tracked.init.len:
addToIntersection(inter, tracked.init[i], hasBreaksBlock)
template hasResultSym(s: PSym): bool =
s != nil and s.kind in {skProc, skFunc, skConverter, skMethod} and
not isEmptyType(s.typ[0])
proc trackCase(tracked: PEffects, n: PNode) =
track(tracked, n[0])
inc tracked.inIfStmt
let oldState = tracked.init.len
let oldFacts = tracked.guards.s.len
let stringCase = n[0].typ != nil and skipTypes(n[0].typ,
abstractVarRange-{tyTypeDesc}).kind in {tyFloat..tyFloat128, tyString, tyCstring}
let interesting = not stringCase and interestingCaseExpr(n[0]) and
(tracked.config.hasWarn(warnProveField) or strictCaseObjects in tracked.c.features)
var inter: TIntersection = @[]
var toCover = 0
let hasResult = hasResultSym(tracked.owner)
let resSym = if hasResult: tracked.owner.ast[resultPos].sym else: nil
var resCounter = 0
for i in 1..<n.len:
let branch = n[i]
setLen(tracked.init, oldState)
if interesting:
setLen(tracked.guards.s, oldFacts)
addCaseBranchFacts(tracked.guards, n, i)
for i in 0..<branch.len:
track(tracked, branch[i])
let hasBreaksBlock = breaksBlock(branch.lastSon)
if hasBreaksBlock == bsNone:
inc toCover
addIdToIntersection(tracked, inter, resCounter, hasBreaksBlock, oldState, resSym, hasResult)
setLen(tracked.init, oldState)
if not stringCase or lastSon(n).kind == nkElse:
if hasResult and resCounter == n.len-1:
tracked.init.add resSym.id
for id, count in items(inter):
if count >= 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..<n.len:
let branch = n[i]
setLen(tracked.guards.s, oldFacts)
for j in 0..i-1:
addFactNeg(tracked.guards, n[j][0])
if branch.len > 1:
addFact(tracked.guards, branch[0])
setLen(tracked.init, oldState)
for i in 0..<branch.len:
track(tracked, branch[i])
let hasBreaksBlock = breaksBlock(branch.lastSon)
if hasBreaksBlock == bsNone:
inc toCover
addIdToIntersection(tracked, inter, resCounter, hasBreaksBlock, oldState, resSym, hasResult)
setLen(tracked.init, oldState)
if lastSon(n).len == 1:
if hasResult and resCounter == n.len:
tracked.init.add resSym.id
for id, count in items(inter):
if count >= 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..<n.len:
if hasSubnodeWith(n[i], nkBreakStmt):
# block:
# x = def
# if ...: ... break # some nested break
# y = def
# --> '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..<safeLen(n):
patchResult(c, n[i])
proc checkLe(c: PEffects; a, b: PNode) =
case proveLe(c.guards, a, b)
of impUnknown:
#for g in c.guards.s:
# if g != nil: echo "I Know ", g
message(c.config, a.info, warnStaticIndexCheck,
"cannot prove: " & $a & " <= " & $b)
of impYes:
discard
of impNo:
message(c.config, a.info, warnStaticIndexCheck,
"can prove: " & $a & " > " & $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 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)
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..<n.len:
trackOperandForIndirectCall(tracked, n[i], op, i, a)
if a.kind == nkSym and a.sym.magic in {mNew, mNewFinalize, mNewSeq}:
# may not look like an assignment, but it is:
let arg = n[1]
initVarViaNew(tracked, arg)
if arg.typ.len != 0 and {tfRequiresInit} * arg.typ.lastSon.flags != {}:
if a.sym.magic == mNewSeq and n[2].kind in {nkCharLit..nkUInt64Lit} and
n[2].intVal == 0:
# var s: seq[notnil]; newSeq(s, 0) is a special case!
discard
else:
message(tracked.config, arg.info, warnProveInit, $arg)
# check required for 'nim check':
if n[1].typ.len > 0:
createTypeBoundOps(tracked, n[1].typ.lastSon, n.info)
createTypeBoundOps(tracked, n[1].typ, n.info)
# new(x, finalizer): Problem: how to move finalizer into 'createTypeBoundOps'?
elif a.kind == nkSym and a.sym.magic in {mArrGet, mArrPut} and
optStaticBoundsCheck in tracked.currOptions:
checkBounds(tracked, n[1], n[2])
if a.kind == nkSym and a.sym.name.s.len > 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..<min(n.safeLen, op.len):
let paramType = op[i]
case paramType.kind
of tySink:
createTypeBoundOps(tracked, paramType[0], n.info)
checkForSink(tracked, n[i])
of tyVar:
if isOutParam(paramType):
# consider this case: p(out x, x); we want to remark that 'x' is not
# initialized until after the call. Since we do this after we analysed the
# call, this is fine.
initVar(tracked, n[i].skipAddr, false)
if strictFuncs in tracked.c.features and not tracked.inEnforcedNoSideEffects and
isDangerousLocation(n[i].skipAddr, tracked.owner):
if sfNoSideEffect in tracked.owner.flags:
localError(tracked.config, n[i].info,
"cannot pass $1 to `var T` parameter within a strict func" % renderTree(n[i]))
tracked.hasSideEffect = true
else: discard
if notConstExpr and (a.kind != nkSym or
a.sym.magic notin {mRunnableExamples, mNBindSym, mExpandToAst, mQuoteAst}
):
# tracked after out analysis
for i in 0..<n.safeLen:
track(tracked, n[i])
type
PragmaBlockContext = object
oldLocked: int
enforcedGcSafety, enforceNoSideEffects: bool
oldExc, oldTags, oldForbids: int
exc, tags, forbids: PNode
proc createBlockContext(tracked: PEffects): PragmaBlockContext =
var oldForbidsLen = 0
if tracked.forbids != nil: oldForbidsLen = tracked.forbids.len
result = PragmaBlockContext(oldLocked: tracked.locked.len,
enforcedGcSafety: false, enforceNoSideEffects: false,
oldExc: tracked.exc.len, oldTags: tracked.tags.len,
oldForbids: oldForbidsLen)
proc applyBlockContext(tracked: PEffects, bc: PragmaBlockContext) =
if bc.enforcedGcSafety: tracked.inEnforcedGcSafe = true
if bc.enforceNoSideEffects: tracked.inEnforcedNoSideEffects = true
proc unapplyBlockContext(tracked: PEffects; bc: PragmaBlockContext) =
if bc.enforcedGcSafety: tracked.inEnforcedGcSafe = false
if bc.enforceNoSideEffects: tracked.inEnforcedNoSideEffects = false
setLen(tracked.locked, bc.oldLocked)
if bc.exc != nil:
# beware that 'raises: []' is very different from not saying
# anything about 'raises' in the 'cast' at all. Same applies for 'tags'.
setLen(tracked.exc.sons, bc.oldExc)
for e in bc.exc:
addRaiseEffect(tracked, e, e)
if bc.tags != nil:
setLen(tracked.tags.sons, bc.oldTags)
for t in bc.tags:
addTag(tracked, t, t)
if bc.forbids != nil:
setLen(tracked.forbids.sons, bc.oldForbids)
for t in bc.forbids:
addNotTag(tracked, t, t)
proc castBlock(tracked: PEffects, pragma: PNode, bc: var PragmaBlockContext) =
case whichPragma(pragma)
of wGcSafe:
bc.enforcedGcSafety = true
of wNoSideEffect:
bc.enforceNoSideEffects = true
of wTags:
let n = pragma[1]
if n.kind in {nkCurly, nkBracket}:
bc.tags = n
else:
bc.tags = newNodeI(nkArgList, pragma.info)
bc.tags.add n
of wForbids:
let n = pragma[1]
if n.kind in {nkCurly, nkBracket}:
bc.forbids = n
else:
bc.forbids = newNodeI(nkArgList, pragma.info)
bc.forbids.add n
of wRaises:
let n = pragma[1]
if n.kind in {nkCurly, nkBracket}:
bc.exc = n
else:
bc.exc = newNodeI(nkArgList, pragma.info)
bc.exc.add n
of wUncheckedAssign:
discard "handled in sempass1"
else:
localError(tracked.config, pragma.info,
"invalid pragma block: " & $pragma)
proc trackInnerProc(tracked: PEffects, n: PNode) =
case n.kind
of nkSym:
let s = n.sym
if s.kind == skParam and s.owner == tracked.owner:
tracked.escapingParams.incl s.id
of nkNone..pred(nkSym), succ(nkSym)..nkNilLit:
discard
of nkProcDef, nkConverterDef, nkMethodDef, nkIteratorDef, nkLambda, nkFuncDef, nkDo:
if n[0].kind == nkSym and n[0].sym.ast != nil:
trackInnerProc(tracked, getBody(tracked.graph, n[0].sym))
of nkTypeSection, nkMacroDef, nkTemplateDef, nkError,
nkConstSection, nkConstDef, nkIncludeStmt, nkImportStmt,
nkExportStmt, nkPragma, nkCommentStmt, nkBreakState,
nkTypeOfExpr, nkMixinStmt, nkBindStmt:
discard
else:
for ch in n: trackInnerProc(tracked, ch)
proc allowCStringConv(n: PNode): bool =
case n.kind
of nkStrLit..nkTripleStrLit: result = true
of nkSym: result = n.sym.kind in {skConst, skParam}
of nkAddr: result = isCharArrayPtr(n.typ, true)
of nkCallKinds:
result = isCharArrayPtr(n.typ, n[0].kind == nkSym and n[0].sym.magic == mAddr)
else: result = isCharArrayPtr(n.typ, false)
proc track(tracked: PEffects, n: PNode) =
case n.kind
of nkSym:
useVar(tracked, n)
if n.sym.typ != nil and tfHasAsgn in n.sym.typ.flags:
tracked.owner.flags.incl sfInjectDestructors
# bug #15038: ensure consistency
if not hasDestructor(n.typ) and sameType(n.typ, n.sym.typ): n.typ = n.sym.typ
of nkHiddenAddr, nkAddr:
if n[0].kind == nkSym and isLocalSym(tracked, n[0].sym):
useVarNoInitCheck(tracked, n[0], n[0].sym)
else:
track(tracked, n[0])
of nkRaiseStmt:
if n[0].kind != nkEmpty:
n[0].info = n.info
#throws(tracked.exc, n[0])
addRaiseEffect(tracked, n[0], n)
for i in 0..<n.safeLen:
track(tracked, n[i])
createTypeBoundOps(tracked, n[0].typ, n.info)
else:
# A `raise` with no arguments means we're going to re-raise the exception
# being handled or, if outside of an `except` block, a `ReraiseDefect`.
# Here we add a `Exception` tag in order to cover both the cases.
addRaiseEffect(tracked, createRaise(tracked.graph, n), nil)
of nkCallKinds:
trackCall(tracked, n)
of nkDotExpr:
guardDotAccess(tracked, n)
for i in 0..<n.len: track(tracked, n[i])
of nkCheckedFieldExpr:
track(tracked, n[0])
if tracked.config.hasWarn(warnProveField) or strictCaseObjects in tracked.c.features:
checkFieldAccess(tracked.guards, n, tracked.config, strictCaseObjects in tracked.c.features)
of nkTryStmt: trackTryStmt(tracked, n)
of nkPragma: trackPragmaStmt(tracked, n)
of nkAsgn, nkFastAsgn, nkSinkAsgn:
track(tracked, n[1])
initVar(tracked, n[0], volatileCheck=true)
invalidateFacts(tracked.guards, n[0])
inc tracked.leftPartOfAsgn
track(tracked, n[0])
dec tracked.leftPartOfAsgn
addAsgnFact(tracked.guards, n[0], n[1])
notNilCheck(tracked, n[1], n[0].typ)
when false: cstringCheck(tracked, n)
if tracked.owner.kind != skMacro and n[0].typ.kind notin {tyOpenArray, tyVarargs}:
createTypeBoundOps(tracked, n[0].typ, n.info)
if n[0].kind != nkSym or not isLocalSym(tracked, n[0].sym):
checkForSink(tracked, n[1])
if strictFuncs in tracked.c.features and not tracked.inEnforcedNoSideEffects and
isDangerousLocation(n[0], tracked.owner):
tracked.hasSideEffect = true
if sfNoSideEffect in tracked.owner.flags:
localError(tracked.config, n[0].info,
"cannot mutate location $1 within a strict func" % renderTree(n[0]))
of nkVarSection, nkLetSection:
for child in n:
let last = lastSon(child)
if last.kind != nkEmpty: track(tracked, last)
if tracked.owner.kind != skMacro:
if child.kind == nkVarTuple:
createTypeBoundOps(tracked, child[^1].typ, child.info)
for i in 0..<child.len-2:
createTypeBoundOps(tracked, child[i].typ, child.info)
else:
createTypeBoundOps(tracked, skipPragmaExpr(child[0]).typ, child.info)
if child.kind == nkIdentDefs:
for i in 0..<child.len-2:
let a = skipPragmaExpr(child[i])
varDecl(tracked, a)
if last.kind != nkEmpty:
initVar(tracked, a, volatileCheck=false)
addAsgnFact(tracked.guards, a, last)
notNilCheck(tracked, last, a.typ)
elif child.kind == nkVarTuple:
for i in 0..<child.len-1:
if child[i].kind == nkEmpty or
child[i].kind == nkSym and child[i].sym.name.id == ord(wUnderscore):
continue
varDecl(tracked, child[i])
if last.kind != nkEmpty:
initVar(tracked, child[i], volatileCheck=false)
if last.kind in {nkPar, nkTupleConstr}:
addAsgnFact(tracked.guards, child[i], last[i])
notNilCheck(tracked, last[i], child[i].typ)
# since 'var (a, b): T = ()' is not even allowed, there is always type
# inference for (a, b) and thus no nil checking is necessary.
of nkConstSection:
for child in n:
let last = lastSon(child)
track(tracked, last)
of nkCaseStmt: trackCase(tracked, n)
of nkWhen, nkIfStmt, nkIfExpr: trackIf(tracked, n)
of nkBlockStmt, nkBlockExpr: trackBlock(tracked, n[1])
of nkWhileStmt:
# 'while true' loop?
inc tracked.currentBlock
if isTrue(n[0]):
trackBlock(tracked, n[1])
else:
# loop may never execute:
let oldState = tracked.init.len
let oldFacts = tracked.guards.s.len
addFact(tracked.guards, n[0])
track(tracked, n[0])
track(tracked, n[1])
setLen(tracked.init, oldState)
setLen(tracked.guards.s, oldFacts)
dec tracked.currentBlock
of nkForStmt, nkParForStmt:
# we are very conservative here and assume the loop is never executed:
inc tracked.currentBlock
let oldState = tracked.init.len
let oldFacts = tracked.guards.s.len
let iterCall = n[n.len-2]
if optStaticBoundsCheck in tracked.currOptions and iterCall.kind in nkCallKinds:
let op = iterCall[0]
if op.kind == nkSym and fromSystem(op.sym):
let iterVar = n[0]
case op.sym.name.s
of "..", "countup", "countdown":
let lower = iterCall[1]
let upper = iterCall[2]
# for i in 0..n means 0 <= i and i <= n. Countdown is
# the same since only the iteration direction changes.
addFactLe(tracked.guards, lower, iterVar)
addFactLe(tracked.guards, iterVar, upper)
of "..<":
let lower = iterCall[1]
let upper = iterCall[2]
addFactLe(tracked.guards, lower, iterVar)
addFactLt(tracked.guards, iterVar, upper)
else: discard
for i in 0..<n.len-2:
let it = n[i]
track(tracked, it)
if tracked.owner.kind != skMacro:
if it.kind == nkVarTuple:
for x in it:
createTypeBoundOps(tracked, x.typ, x.info)
else:
createTypeBoundOps(tracked, it.typ, it.info)
let loopBody = n[^1]
if tracked.owner.kind != skMacro and iterCall.safeLen > 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..<n.len:
let x = n[i]
track(tracked, x)
if x[0].kind == nkSym and sfDiscriminant in x[0].sym.flags:
addDiscriminantFact(tracked.guards, x)
if tracked.owner.kind != skMacro:
createTypeBoundOps(tracked, x[1].typ, n.info)
if x.kind == nkExprColonExpr:
if x[0].kind == nkSym:
notNilCheck(tracked, x[1], x[0].sym.typ)
checkForSink(tracked, x[1])
else:
checkForSink(tracked, x)
setLen(tracked.guards.s, oldFacts)
if tracked.owner.kind != skMacro:
# XXX n.typ can be nil in runnableExamples, we need to do something about it.
if n.typ != nil and n.typ.skipTypes(abstractInst).kind == tyRef:
createTypeBoundOps(tracked, n.typ.lastSon, n.info)
createTypeBoundOps(tracked, n.typ, n.info)
of nkTupleConstr:
for i in 0..<n.len:
track(tracked, n[i])
notNilCheck(tracked, n[i].skipColon, n[i].typ)
if tracked.owner.kind != skMacro:
if n[i].kind == nkExprColonExpr:
createTypeBoundOps(tracked, n[i][0].typ, n.info)
else:
createTypeBoundOps(tracked, n[i].typ, n.info)
checkForSink(tracked, n[i])
of nkPragmaBlock:
let pragmaList = n[0]
var bc = createBlockContext(tracked)
for i in 0..<pragmaList.len:
let pragma = whichPragma(pragmaList[i])
case pragma
of wLocks:
lockLocations(tracked, pragmaList[i])
of wGcSafe:
bc.enforcedGcSafety = true
of wNoSideEffect:
bc.enforceNoSideEffects = true
of wCast:
castBlock(tracked, pragmaList[i][1], bc)
else:
discard
applyBlockContext(tracked, bc)
track(tracked, n.lastSon)
unapplyBlockContext(tracked, bc)
of nkProcDef, nkConverterDef, nkMethodDef, nkIteratorDef, nkLambda, nkFuncDef, nkDo:
if n[0].kind == nkSym and n[0].sym.ast != nil:
trackInnerProc(tracked, getBody(tracked.graph, n[0].sym))
of nkTypeSection, nkMacroDef, nkTemplateDef:
discard
of nkCast:
if n.len == 2:
track(tracked, n[1])
if tracked.owner.kind != skMacro:
createTypeBoundOps(tracked, n.typ, n.info)
of nkHiddenStdConv, nkHiddenSubConv, nkConv:
if n.kind in {nkHiddenStdConv, nkHiddenSubConv} and
n.typ.skipTypes(abstractInst).kind == tyCstring and
not allowCStringConv(n[1]):
message(tracked.config, n.info, warnCstringConv,
"implicit conversion to 'cstring' from a non-const location: $1; this will become a compile time error in the future" %
$n[1])
if n.typ.skipTypes(abstractInst).kind == tyCstring and
isCharArrayPtr(n[1].typ, true):
message(tracked.config, n.info, warnPtrToCstringConv,
$n[1].typ)
let t = n.typ.skipTypes(abstractInst)
if t.kind == tyEnum:
if tfEnumHasHoles in t.flags:
message(tracked.config, n.info, warnHoleEnumConv, "conversion to enum with holes is unsafe: $1" % $n)
else:
message(tracked.config, n.info, warnAnyEnumConv, "enum conversion: $1" % $n)
if n.len == 2:
track(tracked, n[1])
if tracked.owner.kind != skMacro:
createTypeBoundOps(tracked, n.typ, n.info)
# This is a hacky solution in order to fix bug #13110. Hopefully
# a better solution will come up eventually.
if n[1].typ.kind != tyString:
createTypeBoundOps(tracked, n[1].typ, n[1].info)
if optStaticBoundsCheck in tracked.currOptions:
checkRange(tracked, n[1], n.typ)
of nkObjUpConv, nkObjDownConv, nkChckRange, nkChckRangeF, nkChckRange64:
if n.len == 1:
track(tracked, n[0])
if tracked.owner.kind != skMacro:
createTypeBoundOps(tracked, n.typ, n.info)
createTypeBoundOps(tracked, n[0].typ, n[0].info)
if optStaticBoundsCheck in tracked.currOptions:
checkRange(tracked, n[0], n.typ)
of nkBracket:
for i in 0..<n.safeLen:
track(tracked, n[i])
checkForSink(tracked, n[i])
if tracked.owner.kind != skMacro:
createTypeBoundOps(tracked, n.typ, n.info)
of nkBracketExpr:
if optStaticBoundsCheck in tracked.currOptions and n.len == 2:
if n[0].typ != nil and skipTypes(n[0].typ, abstractVar).kind != tyTuple:
checkBounds(tracked, n[0], n[1])
track(tracked, n[0])
dec tracked.leftPartOfAsgn
for i in 1 ..< n.len: track(tracked, n[i])
inc tracked.leftPartOfAsgn
of nkError:
localError(tracked.config, n.info, errorToString(tracked.config, n))
else:
for i in 0..<n.safeLen: track(tracked, n[i])
proc subtypeRelation(g: ModuleGraph; spec, real: PNode): bool =
if spec.typ.kind == tyOr:
result = false
for t in spec.typ:
if safeInheritanceDiff(g.excType(real), t) <= 0:
return true
else:
return safeInheritanceDiff(g.excType(real), spec.typ) <= 0
proc checkRaisesSpec(g: ModuleGraph; emitWarnings: bool; spec, real: PNode, msg: string, hints: bool;
effectPredicate: proc (g: ModuleGraph; a, b: PNode): bool {.nimcall.};
hintsArg: PNode = nil; isForbids: bool = false) =
# check that any real exception is listed in 'spec'; mark those as used;
# report any unused exception
var used = initIntSet()
for r in items(real):
block search:
for s in 0..<spec.len:
if effectPredicate(g, spec[s], r):
if isForbids: break
used.incl(s)
break search
if isForbids:
break search
# XXX call graph analysis would be nice here!
pushInfoContext(g.config, spec.info)
var rr = if r.kind == nkRaiseStmt: r[0] else: r
while rr.kind in {nkStmtList, nkStmtListExpr} and rr.len > 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..<spec.len:
if not used.contains(s):
message(g.config, spec[s].info, hintXCannotRaiseY,
"'$1' cannot raise '$2'" % [renderTree(hintsArg), renderTree(spec[s])])
proc checkMethodEffects*(g: ModuleGraph; disp, branch: PSym) =
## checks for consistent effects for multi methods.
let actual = branch.typ.n[0]
if actual.len != effectListLen: return
let p = disp.ast[pragmasPos]
let raisesSpec = effectSpec(p, wRaises)
if not isNil(raisesSpec):
checkRaisesSpec(g, false, raisesSpec, actual[exceptionEffects],
"can raise an unlisted exception: ", hints=off, subtypeRelation)
let tagsSpec = effectSpec(p, wTags)
if not isNil(tagsSpec):
checkRaisesSpec(g, false, tagsSpec, actual[tagEffects],
"can have an unlisted effect: ", hints=off, subtypeRelation)
let forbidsSpec = effectSpec(p, wForbids)
if not isNil(forbidsSpec):
checkRaisesSpec(g, false, forbidsSpec, actual[tagEffects],
"has an illegal effect: ", hints=off, subtypeRelation, isForbids=true)
if sfThread in disp.flags and notGcSafe(branch.typ):
localError(g.config, branch.info, "base method is GC-safe, but '$1' is not" %
branch.name.s)
when defined(drnim):
if not g.compatibleProps(g, disp.typ, branch.typ):
localError(g.config, branch.info, "for method '" & branch.name.s &
"' the `.requires` or `.ensures` properties are incompatible.")
proc setEffectsForProcType*(g: ModuleGraph; t: PType, n: PNode; s: PSym = nil) =
var effects = t.n[0]
if t.kind != tyProc or effects.kind != nkEffectList: return
if n.kind != nkEmpty:
internalAssert g.config, effects.len == 0
newSeq(effects.sons, effectListLen)
let raisesSpec = effectSpec(n, wRaises)
if not isNil(raisesSpec):
effects[exceptionEffects] = raisesSpec
elif s != nil and (s.magic != mNone or {sfImportc, sfExportc} * s.flags == {sfImportc}):
effects[exceptionEffects] = newNodeI(nkArgList, effects.info)
let tagsSpec = effectSpec(n, wTags)
if not isNil(tagsSpec):
effects[tagEffects] = tagsSpec
elif s != nil and (s.magic != mNone or {sfImportc, sfExportc} * s.flags == {sfImportc}):
effects[tagEffects] = newNodeI(nkArgList, effects.info)
let forbidsSpec = effectSpec(n, wForbids)
if not isNil(forbidsSpec):
effects[forbiddenEffects] = forbidsSpec
elif s != nil and (s.magic != mNone or {sfImportc, sfExportc} * s.flags == {sfImportc}):
effects[forbiddenEffects] = newNodeI(nkArgList, effects.info)
let requiresSpec = propSpec(n, wRequires)
if not isNil(requiresSpec):
effects[requiresEffects] = requiresSpec
let ensuresSpec = propSpec(n, wEnsures)
if not isNil(ensuresSpec):
effects[ensuresEffects] = ensuresSpec
effects[pragmasEffects] = n
if s != nil and s.magic != mNone:
if s.magic != mEcho:
t.flags.incl tfNoSideEffect
proc rawInitEffects(g: ModuleGraph; effects: PNode) =
newSeq(effects.sons, effectListLen)
effects[exceptionEffects] = newNodeI(nkArgList, effects.info)
effects[tagEffects] = newNodeI(nkArgList, effects.info)
effects[forbiddenEffects] = newNodeI(nkArgList, effects.info)
effects[requiresEffects] = g.emptyNode
effects[ensuresEffects] = g.emptyNode
effects[pragmasEffects] = g.emptyNode
proc initEffects(g: ModuleGraph; effects: PNode; s: PSym; c: PContext): TEffects =
rawInitEffects(g, effects)
result = TEffects(exc: effects[exceptionEffects], tags: effects[tagEffects],
forbids: effects[forbiddenEffects], owner: s, ownerModule: s.getModule,
init: @[], locked: @[], graph: g, config: g.config, c: c,
currentBlock: 1
)
result.guards.s = @[]
result.guards.g = g
when defined(drnim):
result.currOptions = g.config.options + s.options - {optStaticBoundsCheck}
else:
result.currOptions = g.config.options + s.options
result.guards.beSmart = optStaticBoundsCheck in result.currOptions
proc hasRealBody(s: PSym): bool =
## also handles importc procs with runnableExamples, which requires `=`,
## which is not a real implementation, refs #14314
result = {sfForward, sfImportc} * s.flags == {}
proc trackProc*(c: PContext; s: PSym, body: PNode) =
let g = c.graph
when defined(nimsuggest):
if g.config.expandDone():
return
var effects = s.typ.n[0]
if effects.kind != nkEffectList: return
# effects already computed?
if not s.hasRealBody: return
let emitWarnings = tfEffectSystemWorkaround in s.typ.flags
if effects.len == effectListLen and not emitWarnings: return
var inferredEffects = newNodeI(nkEffectList, s.info)
var t: TEffects = initEffects(g, inferredEffects, s, c)
rawInitEffects g, effects
if not isEmptyType(s.typ[0]) and
s.kind in {skProc, skFunc, skConverter, skMethod}:
var res = s.ast[resultPos].sym # get result symbol
t.scopes[res.id] = t.currentBlock
track(t, body)
if s.kind != skMacro:
let params = s.typ.n
for i in 1..<params.len:
let param = params[i].sym
let typ = param.typ
if isSinkTypeForParam(typ) or
(t.config.selectedGC in {gcArc, gcOrc, gcAtomicArc} and
(isClosure(typ.skipTypes(abstractInst)) or param.id in t.escapingParams)):
createTypeBoundOps(t, typ, param.info)
if isOutParam(typ) and param.id notin t.init:
message(g.config, param.info, warnProveInit, param.name.s)
if not isEmptyType(s.typ[0]) and
(s.typ[0].requiresInit or s.typ[0].skipTypes(abstractInst).kind == tyVar or
strictDefs in c.features) and
s.kind in {skProc, skFunc, skConverter, skMethod} and s.magic == mNone:
var res = s.ast[resultPos].sym # get result symbol
if res.id notin t.init:
if tfRequiresInit in s.typ[0].flags:
localError(g.config, body.info, "'$1' requires explicit initialization" % "result")
else:
message(g.config, body.info, warnProveInit, "result")
let p = s.ast[pragmasPos]
let raisesSpec = effectSpec(p, wRaises)
if not isNil(raisesSpec):
let useWarning = s.name.s == "=destroy"
checkRaisesSpec(g, useWarning, raisesSpec, t.exc, "can raise an unlisted exception: ",
hints=on, subtypeRelation, hintsArg=s.ast[0])
# after the check, use the formal spec:
effects[exceptionEffects] = raisesSpec
else:
effects[exceptionEffects] = t.exc
let tagsSpec = effectSpec(p, wTags)
if not isNil(tagsSpec):
checkRaisesSpec(g, false, tagsSpec, t.tags, "can have an unlisted effect: ",
hints=off, subtypeRelation)
# after the check, use the formal spec:
effects[tagEffects] = tagsSpec
else:
effects[tagEffects] = t.tags
let forbidsSpec = effectSpec(p, wForbids)
if not isNil(forbidsSpec):
checkRaisesSpec(g, false, forbidsSpec, t.tags, "has an illegal effect: ",
hints=off, subtypeRelation, isForbids=true)
# after the check, use the formal spec:
effects[forbiddenEffects] = forbidsSpec
else:
effects[forbiddenEffects] = t.forbids
let requiresSpec = propSpec(p, wRequires)
if not isNil(requiresSpec):
effects[requiresEffects] = requiresSpec
let ensuresSpec = propSpec(p, wEnsures)
if not isNil(ensuresSpec):
patchResult(t, ensuresSpec)
effects[ensuresEffects] = ensuresSpec
var mutationInfo = MutationInfo()
if views in c.features:
var partitions = computeGraphPartitions(s, body, g, {borrowChecking})
checkBorrowedLocations(partitions, body, g.config)
if sfThread in s.flags and t.gcUnsafe:
if optThreads in g.config.globalOptions and optThreadAnalysis in g.config.globalOptions:
#localError(s.info, "'$1' is not GC-safe" % s.name.s)
listGcUnsafety(s, onlyWarning=false, g.config)
else:
listGcUnsafety(s, onlyWarning=true, g.config)
#localError(s.info, warnGcUnsafe2, s.name.s)
if sfNoSideEffect in s.flags and t.hasSideEffect:
when false:
listGcUnsafety(s, onlyWarning=false, g.config)
else:
if c.compilesContextId == 0: # don't render extended diagnostic messages in `system.compiles` context
var msg = ""
listSideEffects(msg, s, g.config, t.c)
message(g.config, s.info, errGenerated, msg)
else:
localError(g.config, s.info, "") # simple error for `system.compiles` context
if not t.gcUnsafe:
s.typ.flags.incl tfGcSafe
if not t.hasSideEffect and sfSideEffect notin s.flags:
s.typ.flags.incl tfNoSideEffect
when defined(drnim):
if c.graph.strongSemCheck != nil: c.graph.strongSemCheck(c.graph, s, body)
when defined(useDfa):
if s.name.s == "testp":
dataflowAnalysis(s, body)
when false: trackWrites(s, body)
if strictNotNil in c.features and s.kind == skProc:
checkNil(s, body, g.config, c.idgen)
proc trackStmt*(c: PContext; module: PSym; n: PNode, isTopLevel: bool) =
if n.kind in {nkPragma, nkMacroDef, nkTemplateDef, nkProcDef, nkFuncDef,
nkTypeSection, nkConverterDef, nkMethodDef, nkIteratorDef}:
return
let g = c.graph
var effects = newNodeI(nkEffectList, n.info)
var t: TEffects = initEffects(g, effects, module, c)
t.isTopLevel = isTopLevel
track(t, n)
when defined(drnim):
if c.graph.strongSemCheck != nil: c.graph.strongSemCheck(c.graph, module, n)