mirror of
https://github.com/nim-lang/Nim.git
synced 2026-08-04 22:48:38 +00:00
fixes #22122 The root cause is in the effect tracker: raise was recording the whole conditional expression as one exception source, so semantic checking only saw the widened common base type instead of the concrete exception classes from each branch.
1909 lines
71 KiB
Nim
1909 lines
71 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
|
|
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
|
|
isArrayIndexing: bool
|
|
currentExceptType: PType
|
|
unknownRaises: seq[(PSym, TLineInfo)]
|
|
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; explicit = false) =
|
|
if typ == nil or (sfGeneratedOp in tracked.owner.flags and not explicit):
|
|
# don't create type bound ops for anything in a function with a `nodestroy` pragma
|
|
# bug #21987
|
|
# unless this is an explicit call, bug #24626
|
|
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 tracked.config.selectedGC == gcRefc or
|
|
optSeqDestructors in tracked.config.globalOptions or
|
|
tfHasAsgn in typ.flags:
|
|
tracked.owner.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 isRangeSupertype(conf: ConfigRef; wider, narrower: PType): bool =
|
|
## Check if `wider` type fully contains `narrower` type
|
|
## Returns true if narrower fits entirely within wider (safe conversion)
|
|
if wider.isOrdinalType:
|
|
let wideFirst = firstOrd(conf, wider)
|
|
let wideLast = lastOrd(conf, wider)
|
|
let narrowFirst = firstOrd(conf, narrower)
|
|
let narrowLast = lastOrd(conf, narrower)
|
|
result = narrowFirst >= wideFirst and narrowLast <= wideLast
|
|
elif not narrower.isOrdinalType:
|
|
let wideFirst = firstFloat(wider)
|
|
let wideLast = lastFloat(wider)
|
|
let narrowFirst = firstFloat(narrower)
|
|
let narrowLast = lastFloat(narrower)
|
|
result = narrowFirst >= wideFirst and narrowLast <= wideLast
|
|
else:
|
|
# int -> float ranges; warn
|
|
result = false
|
|
|
|
proc shouldWarnRangeConversion(conf: ConfigRef; info: TLineInfo; formalType, argType: PType): bool =
|
|
## Determine if an implicit range conversion should warn
|
|
## We warn on conversions that are likely to cause panics
|
|
let f = formalType.skipTypes({tyGenericInst, tyAlias, tySink, tyDistinct})
|
|
let a = argType.skipTypes({tyGenericInst, tyAlias, tySink, tyDistinct})
|
|
if f.kind == tyRange:
|
|
# Only warn if formal range doesn't fully contain argument range
|
|
# Check if the ranges don't perfectly overlap
|
|
if a.kind == tyInt and f.sym != nil and f.sym.owner != nil and
|
|
sfSystemModule in f.sym.owner.flags and
|
|
(f.sym.name.s == "Positive" or
|
|
f.sym.name.s == "Natural"):
|
|
# Positive and Natural are special cases that we do not warn on with
|
|
# ImplicitRangeConversion, but may warn on with systemRangeConversion
|
|
# if that warning is enabled.
|
|
if conf.hasWarn(warnSystemRangeConversion):
|
|
message(conf, info, warnSystemRangeConversion,
|
|
typeToString(argType) & " -> " & typeToString(formalType))
|
|
result = false
|
|
else:
|
|
result = not isRangeSupertype(conf, f, a)
|
|
else:
|
|
result = false
|
|
|
|
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, 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 isLocalSym(a, s):
|
|
if a.inExceptOrFinallyStmt > 0:
|
|
incl s, sfUsedInFinallyOrExcept
|
|
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.kind == skResult and tfRequiresInit in s.typ.flags:
|
|
localError(a.config, n.info, "'result' requires explicit initialization")
|
|
elif 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 addRaiseEffectsFromExpr(a: PEffects, e, comesFrom: PNode) =
|
|
if e.isNil:
|
|
return
|
|
let x = skipConvCastAndClosure(e)
|
|
case x.kind
|
|
of nkStmtList, nkStmtListExpr, nkBlockStmt, nkBlockExpr:
|
|
if x.len > 0:
|
|
addRaiseEffectsFromExpr(a, x.lastSon, comesFrom)
|
|
of nkIfExpr, nkIfStmt:
|
|
for branch in items(x):
|
|
if branch.len > 0:
|
|
addRaiseEffectsFromExpr(a, branch.lastSon, comesFrom)
|
|
of nkCaseStmt:
|
|
for i in 1..<x.len:
|
|
let branch = x[i]
|
|
if branch.len > 0:
|
|
addRaiseEffectsFromExpr(a, branch.lastSon, comesFrom)
|
|
else:
|
|
addRaiseEffect(a, x, x)
|
|
|
|
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 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..<n.len:
|
|
let b = n[i]
|
|
if b.kind == nkExceptBranch:
|
|
if b.len == 1:
|
|
tracked.caughtExceptions.addCatchAll
|
|
else:
|
|
for j in 0..<b.len - 1:
|
|
if b[j].isInfixAs():
|
|
assert(b[j][1].kind == nkType)
|
|
tracked.caughtExceptions.addCatch(b[j][1].typ)
|
|
else:
|
|
assert(b[j].kind == nkType)
|
|
tracked.caughtExceptions.addCatch(b[j].typ)
|
|
else:
|
|
assert b.kind == nkFinally
|
|
|
|
inc tracked.inTryStmt
|
|
track(tracked, n[0])
|
|
dec tracked.inTryStmt
|
|
for i in oldState..<tracked.init.len:
|
|
addToIntersection(inter, tracked.init[i], bsNone)
|
|
|
|
when defined(nimsuggest):
|
|
tracked.caughtExceptions.pop
|
|
|
|
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)
|
|
# If this except branch catches exactly one type, record it so an
|
|
# empty `raise` inside the branch can be inferred as re-raising that
|
|
# specific exception type instead of the generic `Exception`.
|
|
var savedExcept: PType = tracked.currentExceptType
|
|
var inferredExcept: PType = nil
|
|
if b.len == 2:
|
|
if b[0].isInfixAs():
|
|
assert(b[0][1].kind == nkType)
|
|
inferredExcept = b[0][1].typ
|
|
else:
|
|
assert(b[0].kind == nkType)
|
|
inferredExcept = b[0].typ
|
|
tracked.currentExceptType = inferredExcept
|
|
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])
|
|
tracked.currentExceptType = savedExcept
|
|
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)
|
|
case pragma
|
|
of wEffects:
|
|
# list the computed effects up to here:
|
|
listEffects(tracked)
|
|
of wPush:
|
|
processPushBackendOption(tracked.c.config, tracked.optionsStack, tracked.currOptions, n, i+1)
|
|
of wPop:
|
|
processPopBackendOption(tracked.c.config, tracked.optionsStack, tracked.currOptions)
|
|
else:
|
|
discard
|
|
|
|
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)
|
|
if spec.isNil and sfForward in s.flags:
|
|
tracked.unknownRaises.add (s, n.info)
|
|
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 n.kind in {nkAddr, nkHiddenAddr}:
|
|
# 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 isTrivial(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 formals.n != nil and argIndex < formals.n.len: 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 isTrivial(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.signatureLen: formals[argIndex] else: nil
|
|
if paramType != nil and paramType.kind in {tyVar}:
|
|
let arg = n.skipAddr()
|
|
if isSsoStringIndex(tracked.config, arg):
|
|
localError(tracked.config, arg.info,
|
|
"expression '$1' is immutable, not 'var'" % renderNotLValue(arg))
|
|
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.returnType)
|
|
|
|
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; typ: PType) =
|
|
if n.kind in {nkStmtList, nkStmtListExpr}:
|
|
let myBlock = tracked.currentBlock
|
|
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)
|
|
if typ != nil and typ.kind in {tyVar, tyLent, tyOpenArray, tyVarargs}:
|
|
let last = lastSon(n)
|
|
let root = getRoot(last)
|
|
if root != nil:
|
|
let owner = tracked.scopes.getOrDefault(root.id, -1)
|
|
if owner >= 0:
|
|
localError(tracked.config, last.info, "'" & renderTree(last) & "' borrows from location '" & root.name.s &
|
|
"' which does not live long enough")
|
|
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 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, true)
|
|
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 findHookKind(name: string): (bool, TTypeAttachedOp) =
|
|
case name.normalize
|
|
of "=wasmoved":
|
|
result = (true, attachedWasMoved)
|
|
of "=destroy":
|
|
result = (true, attachedDestructor)
|
|
of "=copy", "=":
|
|
result = (true, attachedAsgn)
|
|
of "=dup":
|
|
result = (true, attachedDup)
|
|
of "=sink":
|
|
result = (true, attachedSink)
|
|
of "=trace":
|
|
result = (true, attachedTrace)
|
|
of "=deepcopy":
|
|
result = (true, attachedDeepCopy)
|
|
else:
|
|
result = (false, attachedWasMoved)
|
|
|
|
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..<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.hasElementType and {tfRequiresInit} * arg.typ.elementType.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.hasElementType:
|
|
createTypeBoundOps(tracked, n[1].typ.elementType, 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])
|
|
|
|
|
|
var n = n
|
|
if a.kind == nkSym and a.sym.name.s.len > 0 and a.sym.name.s[0] == '=' and
|
|
tracked.owner.kind != skMacro:
|
|
var (isHook, opKind) = findHookKind(a.sym.name.s)
|
|
if isHook:
|
|
# rebind type bounds operations after createTypeBoundOps call
|
|
let t = n[1].typ.skipTypes({tyAlias, tyVar, tySink})
|
|
if a.sym != getAttachedOp(tracked.graph, t, opKind):
|
|
createTypeBoundOps(tracked, t, n.info, explicit = true)
|
|
# replace builtin hooks with lifted ones
|
|
n = replaceHookMagic(tracked.c, n, opKind)
|
|
|
|
if op != nil and op.kind == tyProc:
|
|
for i in 1..<min(n.safeLen, op.signatureLen):
|
|
let paramType = op[i]
|
|
case paramType.kind
|
|
of tySink:
|
|
createTypeBoundOps(tracked, paramType.elementType, 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].skipHiddenAddr, false)
|
|
if strictFuncs in tracked.c.features and not tracked.inEnforcedNoSideEffects and
|
|
isDangerousLocation(n[i].skipHiddenAddr, 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
|
|
excSource, tagsSource, forbidsSource: 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, if bc.excSource != nil: bc.excSource else: e)
|
|
if bc.tags != nil:
|
|
setLen(tracked.tags.sons, bc.oldTags)
|
|
for t in bc.tags:
|
|
addTag(tracked, t, if bc.tagsSource != nil: bc.tagsSource else: t)
|
|
if bc.forbids != nil:
|
|
setLen(tracked.forbids.sons, bc.oldForbids)
|
|
for t in bc.forbids:
|
|
addNotTag(tracked, t, if bc.forbidsSource != nil: bc.forbidsSource else: t)
|
|
|
|
proc castBlock(tracked: PEffects, castPragma: PNode, bc: var PragmaBlockContext) =
|
|
let pragma = castPragma[1]
|
|
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
|
|
bc.tagsSource = castPragma
|
|
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
|
|
bc.forbidsSource = castPragma
|
|
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
|
|
bc.excSource = castPragma
|
|
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 nkTypeOfExpr:
|
|
discard "typeof() never evaluates its operand; not a definite-assignment use"
|
|
of nkSym:
|
|
useVar(tracked, n)
|
|
if n.sym.typ != nil and tfHasAsgn in n.sym.typ.flags:
|
|
tracked.owner.incl sfInjectDestructors
|
|
# bug #15038: ensure consistency
|
|
if n.typ == nil or (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) and
|
|
n.typ.kind notin {tyVar, tyLent}:
|
|
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])
|
|
addRaiseEffectsFromExpr(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.
|
|
if tracked.currentExceptType != nil:
|
|
var en = newNode(nkType)
|
|
en.typ = tracked.currentExceptType
|
|
en.info = n.info
|
|
addRaiseEffect(tracked, en, nil)
|
|
createTypeBoundOps(tracked, tracked.currentExceptType, n.info)
|
|
else:
|
|
addRaiseEffect(tracked, createRaise(tracked.graph, n), nil)
|
|
of nkCallKinds:
|
|
trackCall(tracked, n)
|
|
of nkDotExpr:
|
|
guardDotAccess(tracked, n)
|
|
let oldLeftPartOfAsgn = tracked.leftPartOfAsgn
|
|
tracked.leftPartOfAsgn = 0
|
|
for i in 0..<n.len: track(tracked, n[i])
|
|
tracked.leftPartOfAsgn = oldLeftPartOfAsgn
|
|
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: # This should be a "when nimvm" node.
|
|
let oldState = tracked.init.len
|
|
track(tracked, n[0][1])
|
|
tracked.init.setLen(oldState)
|
|
track(tracked, n[1][0])
|
|
of nkIfStmt, nkIfExpr: trackIf(tracked, n)
|
|
of nkBlockStmt, nkBlockExpr: trackBlock(tracked, n[1], n.typ)
|
|
of nkWhileStmt:
|
|
# 'while true' loop?
|
|
inc tracked.currentBlock
|
|
if isTrue(n[0]):
|
|
trackBlock(tracked, n[1], nil)
|
|
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.elementType, 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], 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 nkMacroDef, nkTemplateDef:
|
|
discard
|
|
of nkTypeSection:
|
|
if tracked.isTopLevel:
|
|
collectObjectTree(tracked.graph, n)
|
|
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)
|
|
|
|
# Check for implicit range conversions
|
|
if n.kind == nkHiddenStdConv and (not tracked.isArrayIndexing) and
|
|
n[1].kind notin {nkCharLit..nkUInt64Lit, nkFloatLit..nkFloat128Lit} and
|
|
shouldWarnRangeConversion(tracked.config, n.info, n.typ, n[1].typ):
|
|
message(tracked.config, n.info, warnImplicitRangeConversion,
|
|
typeToString(n[1].typ) & " -> " & typeToString(n.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:
|
|
if i == 1:
|
|
tracked.isArrayIndexing = true
|
|
track(tracked, n[i])
|
|
if i == 1:
|
|
tracked.isArrayIndexing = false
|
|
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.kids:
|
|
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; unknownRaises: seq[(PSym, TLineInfo)] = @[]) =
|
|
# 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
|
|
for (s, info) in unknownRaises.items:
|
|
message(g.config, info, hintUnknownRaises, s.name.s)
|
|
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.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, optionsStack: @[(g.config.options, g.config.notes)]
|
|
)
|
|
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.returnType) and
|
|
s.kind in {skProc, skFunc, skConverter, skMethod}:
|
|
var res = s.ast[resultPos].sym # get result symbol
|
|
t.scopes[res.id] = t.currentBlock
|
|
if sfNoInit in s.flags:
|
|
# marks result "noinit"
|
|
incl res, sfNoInit
|
|
|
|
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, gcYrc, 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 and s.magic == mNone:
|
|
message(g.config, param.info, warnProveInit, param.name.s)
|
|
|
|
if not isEmptyType(s.typ.returnType) and
|
|
(s.typ.returnType.requiresInit or s.typ.returnType.skipTypes(abstractInst).kind == tyVar or
|
|
strictDefs in c.features) and
|
|
s.kind in {skProc, skFunc, skConverter, skMethod} and s.magic == mNone and
|
|
sfNoInit notin s.flags:
|
|
var res = s.ast[resultPos].sym # get result symbol
|
|
if res.id notin t.init and breaksBlock(body) != bsNoReturn:
|
|
if tfRequiresInit in s.typ.returnType.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], unknownRaises = t.unknownRaises)
|
|
# 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.incl tfGcSafe
|
|
if not t.hasSideEffect and sfSideEffect notin s.flags:
|
|
s.typ.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 in {skProc, skFunc, skMethod, skConverter}:
|
|
checkNil(s, body, g.config, c.idgen)
|
|
|
|
proc trackStmt*(c: PContext; module: PSym; n: PNode, isTopLevel: bool) =
|
|
case n.kind
|
|
of {nkPragma, nkMacroDef, nkTemplateDef, nkProcDef, nkFuncDef,
|
|
nkConverterDef, nkMethodDef, nkIteratorDef}:
|
|
discard
|
|
of nkTypeSection:
|
|
if isTopLevel:
|
|
collectObjectTree(c.graph, n)
|
|
else:
|
|
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)
|