Skip to content

Commit

Permalink
alternative, much simpler algorithm for strict func checking (nim-lan…
Browse files Browse the repository at this point in the history
…g#21066)

* alternative, much simpler algorithm for strict func checking

* forgot to git add new compiler module

* new spec is incredibly simple to describe

* fixes bigints regression

* typos

* closes nim-lang#16305; closes nim-lang#17387; closes nim-lang#20863
  • Loading branch information
Araq authored and capocasa committed Mar 31, 2023
1 parent 596101b commit 7cdd7c9
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 78 deletions.
6 changes: 5 additions & 1 deletion compiler/renderer.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1260,8 +1260,12 @@ proc gsub(g: var TSrcGen, n: PNode, c: TContext, fromStmtList = false) =
gsub(g, n, 0)
of nkCheckedFieldExpr, nkHiddenAddr, nkHiddenDeref, nkStringToCString, nkCStringToString:
if renderIds in g.flags:
putWithSpace(g, tkAddr, $n.kind)
put(g, tkAddr, $n.kind)
put(g, tkParLe, "(")
gsub(g, n, 0)
if renderIds in g.flags:
put(g, tkParRi, ")")

of nkLambda:
putWithSpace(g, tkProc, "proc")
gsub(g, n, paramsPos)
Expand Down
37 changes: 18 additions & 19 deletions compiler/sempass2.nim
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
import
intsets, ast, astalgo, msgs, renderer, magicsys, types, idents, trees,
wordrecg, strutils, options, guards, lineinfos, semfold, semdata,
modulegraphs, varpartitions, typeallowed, nilcheck, errorhandling, tables
modulegraphs, varpartitions, typeallowed, nilcheck, errorhandling, tables,
semstrictfuncs

when defined(nimPreviewSlimSystem):
import std/assertions
Expand Down Expand Up @@ -75,7 +76,7 @@ type
guards: TModel # nested guards
locked: seq[PNode] # locked locations
gcUnsafe, isRecursive, isTopLevel, hasSideEffect, inEnforcedGcSafe: bool
hasDangerousAssign, isInnerProc: bool
isInnerProc: bool
inEnforcedNoSideEffects: bool
currOptions: TOptions
config: ConfigRef
Expand Down Expand Up @@ -817,6 +818,9 @@ 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 strictFuncsActive(tracked: PEffects): bool {.inline.} =
sfNoSideEffect in tracked.owner.flags and strictFuncs in tracked.c.features and not tracked.inEnforcedNoSideEffects

proc trackCall(tracked: PEffects; n: PNode) =
template gcsafeAndSideeffectCheck() =
if notGcSafe(op) and not importedFromC(a):
Expand Down Expand Up @@ -921,12 +925,15 @@ proc trackCall(tracked: PEffects; n: PNode) =
createTypeBoundOps(tracked, paramType[0], n.info)
checkForSink(tracked, n[i])
of tyVar:
tracked.hasDangerousAssign = true
if isOutParam(paramType):
# consider this case: p(out x, x); we want to remark that 'x' is not
# initialized until after the call. Since we do this after we analysed the
# call, this is fine.
initVar(tracked, n[i].skipAddr, false)
if tracked.strictFuncsActive and isDangerousLocation(n[i].skipAddr, tracked.owner):
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

type
Expand Down Expand Up @@ -1079,8 +1086,10 @@ proc track(tracked: PEffects, n: PNode) =
createTypeBoundOps(tracked, n[0].typ, n.info)
if n[0].kind != nkSym or not isLocalSym(tracked, n[0].sym):
checkForSink(tracked, n[1])
if not tracked.hasDangerousAssign and n[0].kind != nkSym:
tracked.hasDangerousAssign = true
if tracked.strictFuncsActive and isDangerousLocation(n[0], tracked.owner):
tracked.hasSideEffect = true
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)
Expand Down Expand Up @@ -1513,17 +1522,9 @@ proc trackProc*(c: PContext; s: PSym, body: PNode) =
effects[ensuresEffects] = ensuresSpec

var mutationInfo = MutationInfo()
var hasMutationSideEffect = false
if {strictFuncs, views} * c.features != {}:
var goals: set[Goal] = {}
if strictFuncs in c.features: goals.incl constParameters
if views in c.features: goals.incl borrowChecking
var partitions = computeGraphPartitions(s, body, g, goals)
if not t.hasSideEffect and t.hasDangerousAssign:
t.hasSideEffect = varpartitions.hasSideEffect(partitions, mutationInfo)
hasMutationSideEffect = t.hasSideEffect
if views in c.features:
checkBorrowedLocations(partitions, body, g.config)
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:
Expand All @@ -1536,9 +1537,7 @@ proc trackProc*(c: PContext; s: PSym, body: PNode) =
when false:
listGcUnsafety(s, onlyWarning=false, g.config)
else:
if hasMutationSideEffect:
localError(g.config, s.info, "'$1' can have side effects$2" % [s.name.s, g.config $ mutationInfo])
elif c.compilesContextId == 0: # don't render extended diagnostic messages in `system.compiles` context
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)
Expand Down
55 changes: 55 additions & 0 deletions compiler/semstrictfuncs.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#
#
# The Nim Compiler
# (c) Copyright 2022 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#

## New "strict funcs" checking. Much simpler and hopefully easier to teach than
## the old but more advanced algorithm that can/could be found in `varpartitions.nim`.

import ast, typeallowed, renderer
from aliasanalysis import PathKinds0, PathKinds1
from trees import getMagic

proc isDangerousLocation*(n: PNode; owner: PSym): bool =
var n = n
var hasDeref = false
while true:
case n.kind
of nkDerefExpr, nkHiddenDeref:
if n[0].typ.kind != tyVar:
hasDeref = true
n = n[0]
of PathKinds0 - {nkDerefExpr, nkHiddenDeref}:
n = n[0]
of PathKinds1:
n = n[1]
of nkCallKinds:
if n.len > 1:
if (n.typ != nil and classifyViewType(n.typ) != noView) or getMagic(n) == mSlice:
# borrow from first parameter:
n = n[1]
else:
break
else:
break
else:
break
if n.kind == nkSym:
# dangerous if contains a pointer deref or if it doesn't belong to us:
result = hasDeref or n.sym.owner != owner
when false:
# store to something that belongs to a `var` parameter is fine:
let s = n.sym
if s.kind == skParam:
# dangerous unless a `var T` parameter:
result = s.typ.kind != tyVar
else:
# dangerous if contains a pointer deref or if it doesn't belong to us:
result = hasDeref or s.owner != owner
else:
# dangerous if it contains a pointer deref
result = hasDeref
16 changes: 6 additions & 10 deletions doc/manual_experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -519,8 +519,7 @@ Since version 1.4, a stricter definition of "side effect" is available.
In addition to the existing rule that a side effect is calling a function
with side effects, the following rule is also enforced:

Any mutation to an object does count as a side effect if that object is reachable
via a parameter that is not declared as a `var` parameter.
A store to the heap via a `ref` or `ptr` indirection is not allowed.

For example:

Expand All @@ -540,15 +539,12 @@ For example:
it = it.ri
func mut(n: Node) =
let m = n # is the statement that connected the mutation to the parameter
m.data = "yeah" # the mutation is here
# Error: 'mut' can have side effects
# an object reachable from 'n' is potentially mutated
```

var it = n
while it != nil:
it.data = "yeah" # forbidden mutation
it = it.ri
The algorithm behind this analysis is described in
the [view types algorithm][Algorithm].
```


View types
Expand Down
8 changes: 2 additions & 6 deletions lib/pure/httpcore.nim
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,13 @@ func toCaseInsensitive(headers: HttpHeaders, s: string): string {.inline.} =
func newHttpHeaders*(titleCase=false): HttpHeaders =
## Returns a new `HttpHeaders` object. if `titleCase` is set to true,
## headers are passed to the server in title case (e.g. "Content-Length")
new result
result.table = newTable[string, seq[string]]()
result.isTitleCase = titleCase
result = HttpHeaders(table: newTable[string, seq[string]](), isTitleCase: titleCase)

func newHttpHeaders*(keyValuePairs:
openArray[tuple[key: string, val: string]], titleCase=false): HttpHeaders =
## Returns a new `HttpHeaders` object from an array. if `titleCase` is set to true,
## headers are passed to the server in title case (e.g. "Content-Length")
new result
result.table = newTable[string, seq[string]]()
result.isTitleCase = titleCase
result = HttpHeaders(table: newTable[string, seq[string]](), isTitleCase: titleCase)

for pair in keyValuePairs:
let key = result.toCaseInsensitive(pair.key)
Expand Down
22 changes: 12 additions & 10 deletions lib/pure/pegs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,9 @@ func charSet*(s: set[char]): Peg {.rtl, extern: "npegs$1".} =
## constructs a PEG from a character set `s`
assert '\0' notin s
result = Peg(kind: pkCharChoice)
new(result.charChoice)
result.charChoice[] = s
{.cast(noSideEffect).}:
new(result.charChoice)
result.charChoice[] = s

func len(a: Peg): int {.inline.} = return a.sons.len
func add(d: var Peg, s: Peg) {.inline.} = add(d.sons, s)
Expand Down Expand Up @@ -1823,9 +1824,7 @@ type
skip: Peg

func pegError(p: PegParser, msg: string, line = -1, col = -1) =
var e: ref EInvalidPeg
new(e)
e.msg = errorStr(p, msg, line, col)
var e = (ref EInvalidPeg)(msg: errorStr(p, msg, line, col))
raise e

func getTok(p: var PegParser) =
Expand Down Expand Up @@ -1909,7 +1908,8 @@ func primary(p: var PegParser): Peg =
getTok(p)
elif not arrowIsNextTok(p):
var nt = getNonTerminal(p, p.tok.literal)
incl(nt.flags, ntUsed)
{.cast(noSideEffect).}:
incl(nt.flags, ntUsed)
result = nonterminal(nt).token(p)
getTok(p)
else:
Expand Down Expand Up @@ -2002,12 +2002,14 @@ func parseRule(p: var PegParser): NonTerminal =
result = getNonTerminal(p, p.tok.literal)
if ntDeclared in result.flags:
pegError(p, "attempt to redefine: " & result.name)
result.line = getLine(p)
result.col = getColumn(p)
{.cast(noSideEffect).}:
result.line = getLine(p)
result.col = getColumn(p)
getTok(p)
eat(p, tkArrow)
result.rule = parseExpr(p)
incl(result.flags, ntDeclared) # NOW inlining may be attempted
{.cast(noSideEffect).}:
result.rule = parseExpr(p)
incl(result.flags, ntDeclared) # NOW inlining may be attempted
else:
pegError(p, "rule expected, but found: " & p.tok.literal)

Expand Down
5 changes: 1 addition & 4 deletions lib/pure/strtabs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,7 @@ proc newStringTable*(mode: StringTableMode): owned(StringTableRef) {.
## See also:
## * `newStringTable(keyValuePairs) proc
## <#newStringTable,varargs[tuple[string,string]],StringTableMode>`_
new(result)
result.mode = mode
result.counter = 0
newSeq(result.data, startSize)
result = StringTableRef(mode: mode, counter: 0, data: newSeq[KeyValuePair](startSize))

proc newStringTable*(keyValuePairs: varargs[string],
mode: StringTableMode): owned(StringTableRef) {.
Expand Down
2 changes: 1 addition & 1 deletion lib/pure/strutils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ iterator splitLines*(s: string, keepEol = false): string =
##
## Every `character literal <manual.html#lexical-analysis-character-literals>`_
## newline combination (CR, LF, CR-LF) is supported. The result strings
## contain no trailing end of line characters unless parameter `keepEol`
## contain no trailing end of line characters unless the parameter `keepEol`
## is set to `true`.
##
## Example:
Expand Down
6 changes: 4 additions & 2 deletions lib/system.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2358,7 +2358,8 @@ when not defined(gcArc) and not defined(gcOrc):
if s.len == 0: return
when not defined(js) and not defined(nimscript) and not defined(nimSeqsV2):
var s = cast[PGenericSeq](s)
s.reserved = s.reserved or seqShallowFlag
{.noSideEffect.}:
s.reserved = s.reserved or seqShallowFlag

proc shallow*(s: var string) {.noSideEffect, inline.} =
## Marks a string `s` as `shallow`:idx:. Subsequent assignments will not
Expand All @@ -2371,7 +2372,8 @@ when not defined(gcArc) and not defined(gcOrc):
s = cast[PGenericSeq](newString(0))
# string literals cannot become 'shallow':
if (s.reserved and strlitFlag) == 0:
s.reserved = s.reserved or seqShallowFlag
{.noSideEffect.}:
s.reserved = s.reserved or seqShallowFlag

type
NimNodeObj = object
Expand Down
21 changes: 11 additions & 10 deletions lib/system/seqs_v2.nim
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,17 @@ proc add*[T](x: var seq[T]; value: sink T) {.magic: "AppendSeqElem", noSideEffec
## containers should also call their adding proc `add` for consistency.
## Generic code becomes much easier to write if the Nim naming scheme is
## respected.
let oldLen = x.len
var xu = cast[ptr NimSeqV2[T]](addr x)
if xu.p == nil or xu.p.cap < oldLen+1:
xu.p = cast[typeof(xu.p)](prepareSeqAdd(oldLen, xu.p, 1, sizeof(T), alignof(T)))
xu.len = oldLen+1
# .nodestroy means `xu.p.data[oldLen] = value` is compiled into a
# copyMem(). This is fine as know by construction that
# in `xu.p.data[oldLen]` there is nothing to destroy.
# We also save the `wasMoved + destroy` pair for the sink parameter.
xu.p.data[oldLen] = value
{.cast(noSideEffect).}:
let oldLen = x.len
var xu = cast[ptr NimSeqV2[T]](addr x)
if xu.p == nil or xu.p.cap < oldLen+1:
xu.p = cast[typeof(xu.p)](prepareSeqAdd(oldLen, xu.p, 1, sizeof(T), alignof(T)))
xu.len = oldLen+1
# .nodestroy means `xu.p.data[oldLen] = value` is compiled into a
# copyMem(). This is fine as know by construction that
# in `xu.p.data[oldLen]` there is nothing to destroy.
# We also save the `wasMoved + destroy` pair for the sink parameter.
xu.p.data[oldLen] = value

proc setLen[T](s: var seq[T], newlen: Natural) =
{.noSideEffect.}:
Expand Down
10 changes: 3 additions & 7 deletions tests/effects/tfuncs_cannot_mutate.nim
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
discard """
errormsg: "'mutate' can have side effects"
nimout: '''an object reachable from 'n' is potentially mutated
tfuncs_cannot_mutate.nim(39, 15) the mutation is here
tfuncs_cannot_mutate.nim(37, 7) is the statement that connected the mutation to the parameter
'''
errormsg: "cannot mutate location select(x, z).data within a strict func"
line: 35
"""

{.experimental: "strictFuncs".}
Expand All @@ -25,8 +22,7 @@ func len(n: Node): int =
it = it.ri

func doNotDistract(n: Node) =
var m = Node()
m.data = "abc"
var m = Node(data: "abc")

func select(a, b: Node): Node = b

Expand Down
7 changes: 2 additions & 5 deletions tests/effects/tfuncs_cannot_mutate2.nim
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
discard """
errormsg: "'copy' can have side effects"
nimout: '''an object reachable from 'y' is potentially mutated
tfuncs_cannot_mutate2.nim(15, 7) the mutation is here
tfuncs_cannot_mutate2.nim(13, 10) is the statement that connected the mutation to the parameter
'''
errormsg: "cannot mutate location x[0].a within a strict func"
line: 12
"""

{.experimental: "strictFuncs".}
Expand Down
5 changes: 2 additions & 3 deletions tests/effects/tfuncs_cannot_mutate_simple.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
discard """
errormsg: "'edit' can have side effects"
nimout: '''an object reachable from 'x' is potentially mutated
tfuncs_cannot_mutate_simple.nim(16, 4) the mutation is here'''
errormsg: '''cannot mutate location x.data within a strict func'''
line: 15
"""

{.experimental: "strictFuncs".}
Expand Down
Loading

0 comments on commit 7cdd7c9

Please sign in to comment.