writing to a location counts as "side effect"; implements https://github.com/nim-lang/RFCs/issues/234 (#15030)

This commit is contained in:
Andreas Rumpf 2020-07-25 15:14:28 +02:00 • committed by GitHub
commit 7ca32c86bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 283 additions and 5 deletions

View file

@ -64,7 +64,7 @@ proc canAlias(arg, ret: PType; marker: var IntSet): bool =
else: else:
result = false result = false
proc canAlias(arg, ret: PType): bool = proc canAlias*(arg, ret: PType): bool =
var marker = initIntSet() var marker = initIntSet()
result = canAlias(arg, ret, marker) result = canAlias(arg, ret, marker)

View file

@ -163,6 +163,7 @@ type
## which itself requires `nimble install libffi`, see #10150 ## which itself requires `nimble install libffi`, see #10150
## Note: this feature can't be localized with {.push.} ## Note: this feature can't be localized with {.push.}
vmopsDanger, vmopsDanger,
strictFuncs
LegacyFeature* = enum LegacyFeature* = enum
allowSemcheckedAstModification, allowSemcheckedAstModification,

View file

@ -10,7 +10,7 @@
import import
intsets, ast, astalgo, msgs, renderer, magicsys, types, idents, trees, intsets, ast, astalgo, msgs, renderer, magicsys, types, idents, trees,
wordrecg, strutils, options, guards, lineinfos, semfold, semdata, wordrecg, strutils, options, guards, lineinfos, semfold, semdata,
modulegraphs modulegraphs, varpartitions
when defined(useDfa): when defined(useDfa):
import dfa import dfa
@ -36,9 +36,6 @@ For every sink parameter of type T T is marked.
For every call f() the return type of f() is marked. For every call f() the return type of f() is marked.
]# ]#
# ------------------------ exception and tag tracking ------------------------- # ------------------------ exception and tag tracking -------------------------
@ -73,6 +70,7 @@ type
guards: TModel # nested guards guards: TModel # nested guards
locked: seq[PNode] # locked locations locked: seq[PNode] # locked locations
gcUnsafe, isRecursive, isTopLevel, hasSideEffect, inEnforcedGcSafe: bool gcUnsafe, isRecursive, isTopLevel, hasSideEffect, inEnforcedGcSafe: bool
hasDangerousAssign: bool
inEnforcedNoSideEffects: bool inEnforcedNoSideEffects: bool
maxLockLevel, currLockLevel: TLockLevel maxLockLevel, currLockLevel: TLockLevel
currOptions: TOptions currOptions: TOptions
@ -885,6 +883,8 @@ proc track(tracked: PEffects, n: PNode) =
createTypeBoundOps(tracked, n[0].typ, n.info) createTypeBoundOps(tracked, n[0].typ, n.info)
if n[0].kind != nkSym or not isLocalVar(tracked, n[0].sym): if n[0].kind != nkSym or not isLocalVar(tracked, n[0].sym):
checkForSink(tracked.config, tracked.owner, n[1]) checkForSink(tracked.config, tracked.owner, n[1])
if not tracked.hasDangerousAssign and n[0].kind != nkSym:
tracked.hasDangerousAssign = true
of nkVarSection, nkLetSection: of nkVarSection, nkLetSection:
for child in n: for child in n:
let last = lastSon(child) let last = lastSon(child)
@ -1237,6 +1237,9 @@ proc trackProc*(c: PContext; s: PSym, body: PNode) =
patchResult(t, ensuresSpec) patchResult(t, ensuresSpec)
effects[ensuresEffects] = ensuresSpec effects[ensuresEffects] = ensuresSpec
if strictFuncs in c.features and not t.hasSideEffect and t.hasDangerousAssign:
t.hasSideEffect = mutatesNonVarParameters(s, body)
if sfThread in s.flags and t.gcUnsafe: if sfThread in s.flags and t.gcUnsafe:
if optThreads in g.config.globalOptions and optThreadAnalysis in g.config.globalOptions: if optThreads in g.config.globalOptions and optThreadAnalysis in g.config.globalOptions:
#localError(s.info, "'$1' is not GC-safe" % s.name.s) #localError(s.info, "'$1' is not GC-safe" % s.name.s)

243
compiler/varpartitions.nim Normal file
View file

@ -0,0 +1,243 @@
#
#
# The Nim Compiler
# (c) Copyright 2020 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Partition variables into different graphs. Used for
## Nim's write tracking. The used algorithm is "union find"
## with path compression.
import ast, types
from trees import getMagic
from isolation_check import canAlias
type
SubgraphFlag = enum
isMutated, # graph might be mutated
connectsConstParam, # graph is connected to a non-var parameter.
VarIndexKind = enum
isEmptyRoot,
dependsOn,
isRootOf
VarIndex = object
case kind: VarIndexKind
of isEmptyRoot: discard
of dependsOn: parent: int
of isRootOf: graphIndex: int
Partitions = object
symToId: seq[PSym]
s: seq[VarIndex]
graphs: seq[set[SubgraphFlag]]
proc hasSideEffect(p: Partitions): bool =
for g in p.graphs:
if g == {isMutated, connectsConstParam}: return true
return false
template isConstParam(a): bool = a.kind == skParam and a.typ.kind != tyVar
proc registerVariable(p: var Partitions; n: PNode) =
if n.kind == nkSym:
p.symToId.add n.sym
if isConstParam(n.sym):
p.s.add VarIndex(kind: isRootOf, graphIndex: p.graphs.len)
p.graphs.add({connectsConstParam})
else:
p.s.add VarIndex(kind: isEmptyRoot)
proc variableId(p: Partitions; x: PSym): int {.inline.} = system.find(p.symToId, x)
proc root(v: var Partitions; start: int): int =
result = start
while v.s[result].kind == dependsOn:
result = v.s[result].parent
# path compression:
var it = start
while v.s[it].kind == dependsOn:
let next = v.s[it].parent
v.s[it] = VarIndex(kind: dependsOn, parent: result)
it = next
proc potentialMutation(v: var Partitions; s: PSym) =
let id = variableId(v, s)
if id >= 0:
let r = root(v, id)
case v.s[r].kind
of isEmptyRoot:
v.s[r] = VarIndex(kind: isRootOf, graphIndex: v.graphs.len)
v.graphs.add({isMutated})
of isRootOf:
v.graphs[v.s[r].graphIndex].incl isMutated
else:
assert false, "cannot happen"
else:
discard "we are not interested in the mutation"
proc connect(v: var Partitions; a, b: PSym) =
let aid = variableId(v, a)
if aid < 0:
return
let bid = variableId(v, b)
if bid < 0:
return
let ra = root(v, aid)
let rb = root(v, bid)
if ra != rb:
let paramFlags =
if isConstParam(a) or isConstParam(b):
{connectsConstParam}
else:
{}
# for now we always make 'rb' the slave and 'ra' the master:
let rbFlags =
if v.s[rb].kind == isRootOf:
v.graphs[v.s[rb].graphIndex]
else:
{}
v.s[rb] = VarIndex(kind: dependsOn, parent: ra)
case v.s[ra].kind
of isEmptyRoot:
v.s[ra] = VarIndex(kind: isRootOf, graphIndex: v.graphs.len)
v.graphs.add(paramFlags + rbFlags)
of isRootOf:
v.graphs[v.s[ra].graphIndex].incl paramFlags + rbFlags
else:
assert false, "cannot happen"
proc allRoots(n: PNode; result: var seq[PSym]) =
case n.kind
of nkSym:
if n.sym.kind in {skParam, skVar, skTemp, skLet, skResult, skForVar}:
result.add(n.sym)
of nkHiddenDeref, nkDerefExpr, nkAddr, nkDotExpr, nkBracketExpr,
nkCheckedFieldExpr, nkHiddenAddr, nkObjUpConv, nkObjDownConv:
allRoots(n[0], result)
of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv, nkConv,
nkStmtList, nkStmtListExpr, nkBlockStmt, nkBlockExpr, nkCast:
if n.len > 0:
allRoots(n.lastSon, result)
of nkCaseStmt, nkObjConstr:
for i in 1..<n.len:
allRoots(n[i].lastSon, result)
of nkIfStmt, nkIfExpr:
for i in 0..<n.len:
allRoots(n[i].lastSon, result)
of nkBracket, nkTupleConstr, nkPar:
for i in 0..<n.len:
allRoots(n[i], result)
of nkCallKinds:
if n.typ != nil and n.typ.kind in {tyVar, tyLent}:
if n.len > 1:
allRoots(n[1], result)
else:
let m = getMagic(n)
case m
of mNone:
# we do significantly better here by using the available escape
# information:
if n[0].typ.isNil: return
var typ = n[0].typ
if typ != nil:
typ = skipTypes(typ, abstractInst)
if typ.kind != tyProc: typ = nil
else: assert(typ.len == typ.n.len)
for i in 1 ..< n.len:
let it = n[i]
if typ != nil and i < typ.len:
assert(typ.n[i].kind == nkSym)
let paramType = typ.n[i].typ
if not paramType.isCompileTimeOnly and not typ.sons[0].isEmptyType and
canAlias(paramType, typ.sons[0]):
allRoots(it, result)
else:
allRoots(it, result)
of mSlice:
allRoots(n[1], result)
else:
discard "harmless operation"
else:
discard "nothing to do"
proc deps(p: var Partitions; dest, src: PNode) =
var targets, sources: seq[PSym]
allRoots(dest, targets)
allRoots(src, sources)
for t in targets:
if dest.kind != nkSym:
potentialMutation(p, t)
proc wrap(t: PType): bool {.nimcall.} = t.kind in {tyRef, tyPtr}
if types.searchTypeFor(t.typ, wrap):
for s in sources:
connect(p, t, s)
proc traverse(p: var Partitions; n: PNode) =
case n.kind
of nkLetSection, nkVarSection:
for child in n:
let last = lastSon(child)
traverse(p, last)
if child.kind == nkVarTuple and last.kind in {nkPar, nkTupleConstr}:
if child.len-2 != last.len: return
for i in 0..<child.len-2:
registerVariable(p, child[i])
deps(p, child[i], last[i])
else:
for i in 0..<child.len-2:
registerVariable(p, child[i])
deps(p, child[i], last)
of nkAsgn, nkFastAsgn:
traverse(p, n[0])
traverse(p, n[1])
deps(p, n[0], n[1])
of nkNone..nkNilLit, nkTypeSection, nkProcDef, nkConverterDef,
nkMethodDef, nkIteratorDef, nkMacroDef, nkTemplateDef, nkLambda, nkDo,
nkFuncDef, nkConstSection, nkConstDef, nkIncludeStmt, nkImportStmt,
nkExportStmt, nkPragma, nkCommentStmt, nkBreakState, nkTypeOfExpr:
discard "do not follow the construct"
of nkCallKinds:
for child in n: traverse(p, child)
if n[0].typ.isNil: return
var typ = skipTypes(n[0].typ, abstractInst)
if typ.kind != tyProc: return
assert(typ.len == typ.n.len)
for i in 1..<n.len:
let it = n[i]
if i < typ.len:
assert(typ.n[i].kind == nkSym)
let paramType = typ.n[i]
if paramType.typ.isCompileTimeOnly: continue
if paramType.typ.kind == tyVar:
var roots: seq[PSym]
allRoots(n, roots)
for r in roots: potentialMutation(p, r)
else:
for child in n: traverse(p, child)
proc mutatesNonVarParameters*(s: PSym; n: PNode): bool =
var par = Partitions()
if s.kind != skMacro:
let params = s.typ.n
for i in 1..<params.len:
registerVariable(par, params[i])
if resultPos < s.ast.safeLen:
registerVariable(par, s.ast[resultPos])
traverse(par, n)
result = hasSideEffect(par)

View file

@ -0,0 +1,31 @@
discard """
errormsg: "'mutate' can have side effects"
line: 25
"""
{.experimental: "strictFuncs".}
type
Node = ref object
le, ri: Node
data: string
func len(n: Node): int =
var it = n
while it != nil:
inc result
it = it.ri
func doNotDistract(n: Node) =
var m = Node()
m.data = "abc"
func select(a, b: Node): Node = b
func mutate(n: Node) =
var it = n
let x = it
let y = x
let z = y
select(x, z).data = "tricky"