summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAndreas Rumpf <rumpf_a@web.de>2020-07-25 15:14:28 +0200
committerGitHub <noreply@github.com>2020-07-25 15:14:28 +0200
commit7ca32c86bbb84ac7bcadd0a2bc2cd5fc277f1bff (patch)
tree1bce8429d7d592ae8115aa13699c58d6a5605fe1
parent1330597f6df1d2f7d23ec25a7705e21be2e78d29 (diff)
downloadNim-7ca32c86bbb84ac7bcadd0a2bc2cd5fc277f1bff.tar.gz
writing to a location counts as "side effect"; implements https://github.com/nim-lang/RFCs/issues/234 (#15030)
-rw-r--r--compiler/isolation_check.nim2
-rw-r--r--compiler/options.nim1
-rw-r--r--compiler/sempass2.nim11
-rw-r--r--compiler/varpartitions.nim243
-rw-r--r--tests/effects/tfuncs_cannot_mutate.nim31
5 files changed, 283 insertions, 5 deletions
diff --git a/compiler/isolation_check.nim b/compiler/isolation_check.nim
index a77e8c97a..2b53bce92 100644
--- a/compiler/isolation_check.nim
+++ b/compiler/isolation_check.nim
@@ -64,7 +64,7 @@ proc canAlias(arg, ret: PType; marker: var IntSet): bool =
   else:
     result = false
 
-proc canAlias(arg, ret: PType): bool =
+proc canAlias*(arg, ret: PType): bool =
   var marker = initIntSet()
   result = canAlias(arg, ret, marker)
 
diff --git a/compiler/options.nim b/compiler/options.nim
index 8b73c07fc..5d9bc245b 100644
--- a/compiler/options.nim
+++ b/compiler/options.nim
@@ -163,6 +163,7 @@ type
       ## which itself requires `nimble install libffi`, see #10150
       ## Note: this feature can't be localized with {.push.}
     vmopsDanger,
+    strictFuncs
 
   LegacyFeature* = enum
     allowSemcheckedAstModification,
diff --git a/compiler/sempass2.nim b/compiler/sempass2.nim
index 49fa44ca2..c6d6d9241 100644
--- a/compiler/sempass2.nim
+++ b/compiler/sempass2.nim
@@ -10,7 +10,7 @@
 import
   intsets, ast, astalgo, msgs, renderer, magicsys, types, idents, trees,
   wordrecg, strutils, options, guards, lineinfos, semfold, semdata,
-  modulegraphs
+  modulegraphs, varpartitions
 
 when defined(useDfa):
   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.
 
-
-
-
 ]#
 
 # ------------------------ exception and tag tracking -------------------------
@@ -73,6 +70,7 @@ type
     guards: TModel # nested guards
     locked: seq[PNode] # locked locations
     gcUnsafe, isRecursive, isTopLevel, hasSideEffect, inEnforcedGcSafe: bool
+    hasDangerousAssign: bool
     inEnforcedNoSideEffects: bool
     maxLockLevel, currLockLevel: TLockLevel
     currOptions: TOptions
@@ -885,6 +883,8 @@ proc track(tracked: PEffects, n: PNode) =
       createTypeBoundOps(tracked, n[0].typ, n.info)
     if n[0].kind != nkSym or not isLocalVar(tracked, n[0].sym):
       checkForSink(tracked.config, tracked.owner, n[1])
+      if not tracked.hasDangerousAssign and n[0].kind != nkSym:
+        tracked.hasDangerousAssign = true
   of nkVarSection, nkLetSection:
     for child in n:
       let last = lastSon(child)
@@ -1237,6 +1237,9 @@ proc trackProc*(c: PContext; s: PSym, body: PNode) =
     patchResult(t, 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 optThreads in g.config.globalOptions and optThreadAnalysis in g.config.globalOptions:
       #localError(s.info, "'$1' is not GC-safe" % s.name.s)
diff --git a/compiler/varpartitions.nim b/compiler/varpartitions.nim
new file mode 100644
index 000000000..b89b7d1da
--- /dev/null
+++ b/compiler/varpartitions.nim
@@ -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)
diff --git a/tests/effects/tfuncs_cannot_mutate.nim b/tests/effects/tfuncs_cannot_mutate.nim
new file mode 100644
index 000000000..ec3ad43f7
--- /dev/null
+++ b/tests/effects/tfuncs_cannot_mutate.nim
@@ -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"