alternative, much simpler algorithm for strict func checking (#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 #16305; closes #17387; closes #20863
This commit is contained in:
Andreas Rumpf 2022-12-11 16:58:50 +01:00 • committed by GitHub
commit 3812d91390
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 187 additions and 78 deletions

View file

@ -1260,8 +1260,12 @@ proc gsub(g: var TSrcGen, n: PNode, c: TContext, fromStmtList = false) =
gsub(g, n, 0) gsub(g, n, 0)
of nkCheckedFieldExpr, nkHiddenAddr, nkHiddenDeref, nkStringToCString, nkCStringToString: of nkCheckedFieldExpr, nkHiddenAddr, nkHiddenDeref, nkStringToCString, nkCStringToString:
if renderIds in g.flags: if renderIds in g.flags:
putWithSpace(g, tkAddr, $n.kind) put(g, tkAddr, $n.kind)
put(g, tkParLe, "(")
gsub(g, n, 0) gsub(g, n, 0)
if renderIds in g.flags:
put(g, tkParRi, ")")
of nkLambda: of nkLambda:
putWithSpace(g, tkProc, "proc") putWithSpace(g, tkProc, "proc")
gsub(g, n, paramsPos) gsub(g, n, paramsPos)

View file

@ -10,7 +10,8 @@
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, varpartitions, typeallowed, nilcheck, errorhandling, tables modulegraphs, varpartitions, typeallowed, nilcheck, errorhandling, tables,
semstrictfuncs
when defined(nimPreviewSlimSystem): when defined(nimPreviewSlimSystem):
import std/assertions import std/assertions
@ -75,7 +76,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, isInnerProc: bool isInnerProc: bool
inEnforcedNoSideEffects: bool inEnforcedNoSideEffects: bool
currOptions: TOptions currOptions: TOptions
config: ConfigRef config: ConfigRef
@ -817,6 +818,9 @@ proc checkForSink(tracked: PEffects; n: PNode) =
if tracked.inIfStmt == 0 and optSinkInference in tracked.config.options: if tracked.inIfStmt == 0 and optSinkInference in tracked.config.options:
checkForSink(tracked.config, tracked.c.idgen, tracked.owner, n) 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) = proc trackCall(tracked: PEffects; n: PNode) =
template gcsafeAndSideeffectCheck() = template gcsafeAndSideeffectCheck() =
if notGcSafe(op) and not importedFromC(a): if notGcSafe(op) and not importedFromC(a):
@ -921,12 +925,15 @@ proc trackCall(tracked: PEffects; n: PNode) =
createTypeBoundOps(tracked, paramType[0], n.info) createTypeBoundOps(tracked, paramType[0], n.info)
checkForSink(tracked, n[i]) checkForSink(tracked, n[i])
of tyVar: of tyVar:
tracked.hasDangerousAssign = true
if isOutParam(paramType): if isOutParam(paramType):
# consider this case: p(out x, x); we want to remark that 'x' is not # 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 # initialized until after the call. Since we do this after we analysed the
# call, this is fine. # call, this is fine.
initVar(tracked, n[i].skipAddr, false) 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 else: discard
type type
@ -1079,8 +1086,10 @@ 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 isLocalSym(tracked, n[0].sym): if n[0].kind != nkSym or not isLocalSym(tracked, n[0].sym):
checkForSink(tracked, n[1]) checkForSink(tracked, n[1])
if not tracked.hasDangerousAssign and n[0].kind != nkSym: if tracked.strictFuncsActive and isDangerousLocation(n[0], tracked.owner):
tracked.hasDangerousAssign = true tracked.hasSideEffect = true
localError(tracked.config, n[0].info,
"cannot mutate location $1 within a strict func" % renderTree(n[0]))
of nkVarSection, nkLetSection: of nkVarSection, nkLetSection:
for child in n: for child in n:
let last = lastSon(child) let last = lastSon(child)
@ -1513,17 +1522,9 @@ proc trackProc*(c: PContext; s: PSym, body: PNode) =
effects[ensuresEffects] = ensuresSpec effects[ensuresEffects] = ensuresSpec
var mutationInfo = MutationInfo() var mutationInfo = MutationInfo()
var hasMutationSideEffect = false if views in c.features:
if {strictFuncs, views} * c.features != {}: var partitions = computeGraphPartitions(s, body, g, {borrowChecking})
var goals: set[Goal] = {} checkBorrowedLocations(partitions, body, g.config)
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 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:
@ -1536,9 +1537,7 @@ proc trackProc*(c: PContext; s: PSym, body: PNode) =
when false: when false:
listGcUnsafety(s, onlyWarning=false, g.config) listGcUnsafety(s, onlyWarning=false, g.config)
else: else:
if hasMutationSideEffect: if c.compilesContextId == 0: # don't render extended diagnostic messages in `system.compiles` context
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
var msg = "" var msg = ""
listSideEffects(msg, s, g.config, t.c) listSideEffects(msg, s, g.config, t.c)
message(g.config, s.info, errGenerated, msg) message(g.config, s.info, errGenerated, msg)

View file

@ -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

View file

@ -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 In addition to the existing rule that a side effect is calling a function
with side effects, the following rule is also enforced: 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 A store to the heap via a `ref` or `ptr` indirection is not allowed.
via a parameter that is not declared as a `var` parameter.
For example: For example:
@ -540,17 +539,14 @@ For example:
it = it.ri it = it.ri
func mut(n: Node) = func mut(n: Node) =
let m = n # is the statement that connected the mutation to the parameter var it = n
m.data = "yeah" # the mutation is here while it != nil:
# Error: 'mut' can have side effects it.data = "yeah" # forbidden mutation
# an object reachable from 'n' is potentially mutated it = it.ri
``` ```
The algorithm behind this analysis is described in
the [view types algorithm][Algorithm].
View types View types
========== ==========

View file

@ -132,17 +132,13 @@ func toCaseInsensitive(headers: HttpHeaders, s: string): string {.inline.} =
func newHttpHeaders*(titleCase=false): HttpHeaders = func newHttpHeaders*(titleCase=false): HttpHeaders =
## Returns a new `HttpHeaders` object. if `titleCase` is set to true, ## Returns a new `HttpHeaders` object. if `titleCase` is set to true,
## headers are passed to the server in title case (e.g. "Content-Length") ## headers are passed to the server in title case (e.g. "Content-Length")
new result result = HttpHeaders(table: newTable[string, seq[string]](), isTitleCase: titleCase)
result.table = newTable[string, seq[string]]()
result.isTitleCase = titleCase
func newHttpHeaders*(keyValuePairs: func newHttpHeaders*(keyValuePairs:
openArray[tuple[key: string, val: string]], titleCase=false): HttpHeaders = openArray[tuple[key: string, val: string]], titleCase=false): HttpHeaders =
## Returns a new `HttpHeaders` object from an array. if `titleCase` is set to true, ## 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") ## headers are passed to the server in title case (e.g. "Content-Length")
new result result = HttpHeaders(table: newTable[string, seq[string]](), isTitleCase: titleCase)
result.table = newTable[string, seq[string]]()
result.isTitleCase = titleCase
for pair in keyValuePairs: for pair in keyValuePairs:
let key = result.toCaseInsensitive(pair.key) let key = result.toCaseInsensitive(pair.key)

View file

@ -168,8 +168,9 @@ func charSet*(s: set[char]): Peg {.rtl, extern: "npegs$1".} =
## constructs a PEG from a character set `s` ## constructs a PEG from a character set `s`
assert '\0' notin s assert '\0' notin s
result = Peg(kind: pkCharChoice) result = Peg(kind: pkCharChoice)
new(result.charChoice) {.cast(noSideEffect).}:
result.charChoice[] = s new(result.charChoice)
result.charChoice[] = s
func len(a: Peg): int {.inline.} = return a.sons.len func len(a: Peg): int {.inline.} = return a.sons.len
func add(d: var Peg, s: Peg) {.inline.} = add(d.sons, s) func add(d: var Peg, s: Peg) {.inline.} = add(d.sons, s)
@ -1823,9 +1824,7 @@ type
skip: Peg skip: Peg
func pegError(p: PegParser, msg: string, line = -1, col = -1) = func pegError(p: PegParser, msg: string, line = -1, col = -1) =
var e: ref EInvalidPeg var e = (ref EInvalidPeg)(msg: errorStr(p, msg, line, col))
new(e)
e.msg = errorStr(p, msg, line, col)
raise e raise e
func getTok(p: var PegParser) = func getTok(p: var PegParser) =
@ -1909,7 +1908,8 @@ func primary(p: var PegParser): Peg =
getTok(p) getTok(p)
elif not arrowIsNextTok(p): elif not arrowIsNextTok(p):
var nt = getNonTerminal(p, p.tok.literal) var nt = getNonTerminal(p, p.tok.literal)
incl(nt.flags, ntUsed) {.cast(noSideEffect).}:
incl(nt.flags, ntUsed)
result = nonterminal(nt).token(p) result = nonterminal(nt).token(p)
getTok(p) getTok(p)
else: else:
@ -2002,12 +2002,14 @@ func parseRule(p: var PegParser): NonTerminal =
result = getNonTerminal(p, p.tok.literal) result = getNonTerminal(p, p.tok.literal)
if ntDeclared in result.flags: if ntDeclared in result.flags:
pegError(p, "attempt to redefine: " & result.name) pegError(p, "attempt to redefine: " & result.name)
result.line = getLine(p) {.cast(noSideEffect).}:
result.col = getColumn(p) result.line = getLine(p)
result.col = getColumn(p)
getTok(p) getTok(p)
eat(p, tkArrow) eat(p, tkArrow)
result.rule = parseExpr(p) {.cast(noSideEffect).}:
incl(result.flags, ntDeclared) # NOW inlining may be attempted result.rule = parseExpr(p)
incl(result.flags, ntDeclared) # NOW inlining may be attempted
else: else:
pegError(p, "rule expected, but found: " & p.tok.literal) pegError(p, "rule expected, but found: " & p.tok.literal)

View file

@ -261,10 +261,7 @@ proc newStringTable*(mode: StringTableMode): owned(StringTableRef) {.
## See also: ## See also:
## * `newStringTable(keyValuePairs) proc ## * `newStringTable(keyValuePairs) proc
## <#newStringTable,varargs[tuple[string,string]],StringTableMode>`_ ## <#newStringTable,varargs[tuple[string,string]],StringTableMode>`_
new(result) result = StringTableRef(mode: mode, counter: 0, data: newSeq[KeyValuePair](startSize))
result.mode = mode
result.counter = 0
newSeq(result.data, startSize)
proc newStringTable*(keyValuePairs: varargs[string], proc newStringTable*(keyValuePairs: varargs[string],
mode: StringTableMode): owned(StringTableRef) {. mode: StringTableMode): owned(StringTableRef) {.

View file

@ -624,7 +624,7 @@ iterator splitLines*(s: string, keepEol = false): string =
## ##
## Every `character literal <manual.html#lexical-analysis-character-literals>`_ ## Every `character literal <manual.html#lexical-analysis-character-literals>`_
## newline combination (CR, LF, CR-LF) is supported. The result strings ## 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`. ## is set to `true`.
## ##
## Example: ## Example:

View file

@ -2358,7 +2358,8 @@ when not defined(gcArc) and not defined(gcOrc):
if s.len == 0: return if s.len == 0: return
when not defined(js) and not defined(nimscript) and not defined(nimSeqsV2): when not defined(js) and not defined(nimscript) and not defined(nimSeqsV2):
var s = cast[PGenericSeq](s) 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.} = proc shallow*(s: var string) {.noSideEffect, inline.} =
## Marks a string `s` as `shallow`:idx:. Subsequent assignments will not ## Marks a string `s` as `shallow`:idx:. Subsequent assignments will not
@ -2371,7 +2372,8 @@ when not defined(gcArc) and not defined(gcOrc):
s = cast[PGenericSeq](newString(0)) s = cast[PGenericSeq](newString(0))
# string literals cannot become 'shallow': # string literals cannot become 'shallow':
if (s.reserved and strlitFlag) == 0: if (s.reserved and strlitFlag) == 0:
s.reserved = s.reserved or seqShallowFlag {.noSideEffect.}:
s.reserved = s.reserved or seqShallowFlag
type type
NimNodeObj = object NimNodeObj = object

View file

@ -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. ## containers should also call their adding proc `add` for consistency.
## Generic code becomes much easier to write if the Nim naming scheme is ## Generic code becomes much easier to write if the Nim naming scheme is
## respected. ## respected.
let oldLen = x.len {.cast(noSideEffect).}:
var xu = cast[ptr NimSeqV2[T]](addr x) let oldLen = x.len
if xu.p == nil or xu.p.cap < oldLen+1: var xu = cast[ptr NimSeqV2[T]](addr x)
xu.p = cast[typeof(xu.p)](prepareSeqAdd(oldLen, xu.p, 1, sizeof(T), alignof(T))) if xu.p == nil or xu.p.cap < oldLen+1:
xu.len = oldLen+1 xu.p = cast[typeof(xu.p)](prepareSeqAdd(oldLen, xu.p, 1, sizeof(T), alignof(T)))
# .nodestroy means `xu.p.data[oldLen] = value` is compiled into a xu.len = oldLen+1
# copyMem(). This is fine as know by construction that # .nodestroy means `xu.p.data[oldLen] = value` is compiled into a
# in `xu.p.data[oldLen]` there is nothing to destroy. # copyMem(). This is fine as know by construction that
# We also save the `wasMoved + destroy` pair for the sink parameter. # in `xu.p.data[oldLen]` there is nothing to destroy.
xu.p.data[oldLen] = value # 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) = proc setLen[T](s: var seq[T], newlen: Natural) =
{.noSideEffect.}: {.noSideEffect.}:

View file

@ -1,9 +1,6 @@
discard """ discard """
errormsg: "'mutate' can have side effects" errormsg: "cannot mutate location select(x, z).data within a strict func"
nimout: '''an object reachable from 'n' is potentially mutated line: 35
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
'''
""" """
{.experimental: "strictFuncs".} {.experimental: "strictFuncs".}
@ -25,8 +22,7 @@ func len(n: Node): int =
it = it.ri it = it.ri
func doNotDistract(n: Node) = func doNotDistract(n: Node) =
var m = Node() var m = Node(data: "abc")
m.data = "abc"
func select(a, b: Node): Node = b func select(a, b: Node): Node = b

View file

@ -1,9 +1,6 @@
discard """ discard """
errormsg: "'copy' can have side effects" errormsg: "cannot mutate location x[0].a within a strict func"
nimout: '''an object reachable from 'y' is potentially mutated line: 12
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
'''
""" """
{.experimental: "strictFuncs".} {.experimental: "strictFuncs".}

View file

@ -1,7 +1,6 @@
discard """ discard """
errormsg: "'edit' can have side effects" errormsg: '''cannot mutate location x.data within a strict func'''
nimout: '''an object reachable from 'x' is potentially mutated line: 15
tfuncs_cannot_mutate_simple.nim(16, 4) the mutation is here'''
""" """
{.experimental: "strictFuncs".} {.experimental: "strictFuncs".}

View file

@ -0,0 +1,65 @@
discard """
action: compile
"""
{.experimental: "strictFuncs".}
func sortedFake1[T](a: openArray[T]): seq[T] =
for i in 0 .. a.high: result.add a[i]
func sortedFake2[T](a: openArray[T]): seq[T] =
result = newSeq[T](a.len)
for i in 0 .. a.high: result[i] = a[i]
type Foo1 = object
type Foo2 = ref object
block:
let a1 = sortedFake1([Foo1()]) # ok
let a2 = sortedFake1([Foo2()]) # ok
block:
let a1 = sortedFake2([Foo1()]) # ok
let a2 = sortedFake2([Foo2()]) # error: Error: 'sortedFake2' can have side effects
import std/sequtils
type Foob = ref object
x: int
let a1 = zip(@[1,2], @[1,2]) # ok
let a2 = zip(@[Foob(x: 1)], @[Foob(x: 2)]) # error in 1.6.0 RC2, but not 1.4.x
# bug #20863
type
Fooc = ref object
func twice(foo: Fooc) =
var a = newSeq[Fooc](2)
a[0] = foo # No error.
a[1] = foo # Error: 'twice' can have side effects.
let foo = Fooc()
twice(foo)
# bug #17387
import json
func parseColumn(columnNode: JsonNode) =
let columnName = columnNode["name"].str
parseColumn(%*{"a": "b"})
type
MyTable = object
data: seq[int]
JsonNode3 = ref object
fields: MyTable
proc `[]`(t: var MyTable, key: string): var int =
result = t.data[0]
proc `[]`(x: JsonNode3, key: string): int =
result = x.fields[key]
func parseColumn(columnNode: JsonNode3) =
var columnName = columnNode["test"]
parseColumn(JsonNode3())