Extended side effect error messages (#18418)

* Extended side effect error messages

* Applied feedback:
- refactored `markSideEffect`
- refactored string interpolations
- single message
- skip diagnostics in `system.compiles` context

Other:
- started a test of diagnostic messages

[ci skip] Tests aren't updated yet because messaging isn't nailed down.

* - Added hints of where for side effect call locations.
- Tried to clarify the reasons.

* fix tests

* Applied PR review feedback:
  - moved collection of side effects from TSym to TContext
  - used pragma shorthand form `.sideEffect` and `.noSideEffect` in messages
  - added leading '>' to structured messages for readability
  - changed `sempass2.markSideEffect` to a proc
  - replaced `system.echo` in the test to make the test compatible with Windows

* Applied NEP1 formatting suggestion

Co-authored-by: quantimnot <quantimnot@users.noreply.github.com>
This commit is contained in:
quantimnot 2021-07-15 14:43:57 -04:00 • committed by GitHub
commit a9701f6531
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 95 additions and 17 deletions

View file

@ -159,6 +159,7 @@ type
exportIndirections*: HashSet[(int, int)] # (module.id, symbol.id) exportIndirections*: HashSet[(int, int)] # (module.id, symbol.id)
importModuleMap*: Table[int, int] # (module.id, module.id) importModuleMap*: Table[int, int] # (module.id, module.id)
lastTLineInfo*: TLineInfo lastTLineInfo*: TLineInfo
sideEffects*: Table[int, seq[(TLineInfo, PSym)]] # symbol.id index
template config*(c: PContext): ConfigRef = c.graph.config template config*(c: PContext): ConfigRef = c.graph.config

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, varpartitions, typeallowed, nilcheck, errorhandling modulegraphs, varpartitions, typeallowed, nilcheck, errorhandling, tables
when defined(useDfa): when defined(useDfa):
import dfa import dfa
@ -223,13 +223,21 @@ proc markGcUnsafe(a: PEffects; reason: PNode) =
a.owner.gcUnsafetyReason = newSym(skUnknown, a.owner.name, nextSymId a.c.idgen, a.owner.gcUnsafetyReason = newSym(skUnknown, a.owner.name, nextSymId a.c.idgen,
a.owner, reason.info, {}) a.owner, reason.info, {})
when true: proc markSideEffect(a: PEffects; reason: PNode | PSym; useLoc: TLineInfo) =
template markSideEffect(a: PEffects; reason: typed) = if not a.inEnforcedNoSideEffects:
if not a.inEnforcedNoSideEffects: a.hasSideEffect = true a.hasSideEffect = true
else: if a.owner.kind in routineKinds:
template markSideEffect(a: PEffects; reason: typed) = var sym: PSym
if not a.inEnforcedNoSideEffects: a.hasSideEffect = true when reason is PNode:
markGcUnsafe(a, reason) if reason.kind == nkSym:
sym = reason.sym
else:
let kind = if reason.kind == nkHiddenDeref: skParam else: skUnknown
sym = newSym(kind, a.owner.name, nextSymId 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) = proc listGcUnsafety(s: PSym; onlyWarning: bool; cycleCheck: var IntSet; conf: ConfigRef) =
let u = s.gcUnsafetyReason let u = s.gcUnsafetyReason
@ -259,6 +267,31 @@ proc listGcUnsafety(s: PSym; onlyWarning: bool; conf: ConfigRef) =
var cycleCheck = initIntSet() var cycleCheck = initIntSet()
listGcUnsafety(s, onlyWarning, cycleCheck, conf) 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) = proc useVarNoInitCheck(a: PEffects; n: PNode; s: PSym) =
if {sfGlobal, sfThread} * s.flags != {} and s.kind in {skVar, skLet} and if {sfGlobal, sfThread} * s.flags != {} and s.kind in {skVar, skLet} and
s.magic != mNimvm: s.magic != mNimvm:
@ -267,9 +300,7 @@ proc useVarNoInitCheck(a: PEffects; n: PNode; s: PSym) =
(tfHasGCedMem in s.typ.flags or s.typ.isGCedMem): (tfHasGCedMem in s.typ.flags or s.typ.isGCedMem):
#if a.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n) #if a.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n)
markGcUnsafe(a, s) markGcUnsafe(a, s)
markSideEffect(a, s) markSideEffect(a, s, n.info)
else:
markSideEffect(a, s)
if s.owner != a.owner and s.kind in {skVar, skLet, skForVar, skResult, skParam} and if s.owner != a.owner and s.kind in {skVar, skLet, skForVar, skResult, skParam} and
{sfGlobal, sfThread} * s.flags == {}: {sfGlobal, sfThread} * s.flags == {}:
a.isInnerProc = true a.isInnerProc = true
@ -502,7 +533,7 @@ proc propagateEffects(tracked: PEffects, n: PNode, s: PSym) =
if tracked.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n, tracked.config) if tracked.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n, tracked.config)
markGcUnsafe(tracked, s) markGcUnsafe(tracked, s)
if tfNoSideEffect notin s.typ.flags: if tfNoSideEffect notin s.typ.flags:
markSideEffect(tracked, s) markSideEffect(tracked, s, n.info)
mergeLockLevels(tracked, n, s.getLockLevel) mergeLockLevels(tracked, n, s.getLockLevel)
proc procVarCheck(n: PNode; conf: ConfigRef) = proc procVarCheck(n: PNode; conf: ConfigRef) =
@ -584,7 +615,7 @@ proc trackOperandForIndirectCall(tracked: PEffects, n: PNode, paramType: PType;
if tracked.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n, tracked.config) if tracked.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n, tracked.config)
markGcUnsafe(tracked, a) markGcUnsafe(tracked, a)
elif tfNoSideEffect notin op.flags and not isOwnedProcVar(a, tracked.owner): elif tfNoSideEffect notin op.flags and not isOwnedProcVar(a, tracked.owner):
markSideEffect(tracked, a) markSideEffect(tracked, a, n.info)
else: else:
mergeRaises(tracked, effectList[exceptionEffects], n) mergeRaises(tracked, effectList[exceptionEffects], n)
mergeTags(tracked, effectList[tagEffects], n) mergeTags(tracked, effectList[tagEffects], n)
@ -592,7 +623,7 @@ proc trackOperandForIndirectCall(tracked: PEffects, n: PNode, paramType: PType;
if tracked.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n, tracked.config) if tracked.config.hasWarn(warnGcUnsafe): warnAboutGcUnsafe(n, tracked.config)
markGcUnsafe(tracked, a) markGcUnsafe(tracked, a)
elif tfNoSideEffect notin op.flags: elif tfNoSideEffect notin op.flags:
markSideEffect(tracked, a) markSideEffect(tracked, a, n.info)
if paramType != nil and paramType.kind in {tyVar}: if paramType != nil and paramType.kind in {tyVar}:
invalidateFacts(tracked.guards, n) invalidateFacts(tracked.guards, n)
if n.kind == nkSym and isLocalVar(tracked, n.sym): if n.kind == nkSym and isLocalVar(tracked, n.sym):
@ -749,7 +780,7 @@ proc trackCall(tracked: PEffects; n: PNode) =
if tfNoSideEffect notin op.flags and not importedFromC(a): if tfNoSideEffect notin op.flags and not importedFromC(a):
# and it's not a recursive call: # and it's not a recursive call:
if not (a.kind == nkSym and a.sym == tracked.owner): if not (a.kind == nkSym and a.sym == tracked.owner):
markSideEffect(tracked, a) markSideEffect(tracked, a, n.info)
# p's effects are ours too: # p's effects are ours too:
var a = n[0] var a = n[0]
@ -771,7 +802,7 @@ proc trackCall(tracked: PEffects; n: PNode) =
if a.sym == tracked.owner: tracked.isRecursive = true if a.sym == tracked.owner: tracked.isRecursive = true
# even for recursive calls we need to check the lock levels (!): # even for recursive calls we need to check the lock levels (!):
mergeLockLevels(tracked, n, a.sym.getLockLevel) mergeLockLevels(tracked, n, a.sym.getLockLevel)
if sfSideEffect in a.sym.flags: markSideEffect(tracked, a) if sfSideEffect in a.sym.flags: markSideEffect(tracked, a, n.info)
else: else:
mergeLockLevels(tracked, n, op.lockLevel) mergeLockLevels(tracked, n, op.lockLevel)
var effectList = op.n[0] var effectList = op.n[0]
@ -1336,6 +1367,7 @@ proc trackProc*(c: PContext; s: PSym, body: PNode) =
effects[ensuresEffects] = ensuresSpec effects[ensuresEffects] = ensuresSpec
var mutationInfo = MutationInfo() var mutationInfo = MutationInfo()
var hasMutationSideEffect = false
if {strictFuncs, views} * c.features != {}: if {strictFuncs, views} * c.features != {}:
var goals: set[Goal] = {} var goals: set[Goal] = {}
if strictFuncs in c.features: goals.incl constParameters if strictFuncs in c.features: goals.incl constParameters
@ -1343,6 +1375,7 @@ proc trackProc*(c: PContext; s: PSym, body: PNode) =
var partitions = computeGraphPartitions(s, body, g, goals) var partitions = computeGraphPartitions(s, body, g, goals)
if not t.hasSideEffect and t.hasDangerousAssign: if not t.hasSideEffect and t.hasDangerousAssign:
t.hasSideEffect = varpartitions.hasSideEffect(partitions, mutationInfo) t.hasSideEffect = varpartitions.hasSideEffect(partitions, mutationInfo)
hasMutationSideEffect = t.hasSideEffect
if views in c.features: if views in c.features:
checkBorrowedLocations(partitions, body, g.config) checkBorrowedLocations(partitions, body, g.config)
@ -1357,7 +1390,14 @@ 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:
localError(g.config, s.info, ("'$1' can have side effects" % s.name.s) & (g.config $ mutationInfo)) 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
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: if not t.gcUnsafe:
s.typ.flags.incl tfGcSafe s.typ.flags.incl tfGcSafe
if not t.hasSideEffect and sfSideEffect notin s.flags: if not t.hasSideEffect and sfSideEffect notin s.flags:

View file

@ -0,0 +1,37 @@
discard """
nimoutFull: true
action: "reject"
cmd: "nim r --hint:Conf:off $file"
nimout: '''
tdiagnostic_messages.nim(36, 6) Error: 'a' can have side effects
> tdiagnostic_messages.nim(37, 30) Hint: 'a' calls `.sideEffect` 'callWithSideEffects'
>> tdiagnostic_messages.nim(29, 6) Hint: 'callWithSideEffects' called by 'a'
>>> tdiagnostic_messages.nim(31, 34) Hint: 'callWithSideEffects' calls `.sideEffect` 'indirectCallViaVarParam'
>>>> tdiagnostic_messages.nim(25, 6) Hint: 'indirectCallViaVarParam' called by 'callWithSideEffects'
>>>>> tdiagnostic_messages.nim(26, 7) Hint: 'indirectCallViaVarParam' calls routine via hidden pointer indirection
>>> tdiagnostic_messages.nim(32, 33) Hint: 'callWithSideEffects' calls `.sideEffect` 'indirectCallViaPointer'
>>>> tdiagnostic_messages.nim(27, 6) Hint: 'indirectCallViaPointer' called by 'callWithSideEffects'
>>>>> tdiagnostic_messages.nim(28, 32) Hint: 'indirectCallViaPointer' calls routine via pointer indirection
>>> tdiagnostic_messages.nim(33, 10) Hint: 'callWithSideEffects' calls `.sideEffect` 'myEcho'
>>>> tdiagnostic_messages.nim(24, 6) Hint: 'myEcho' called by 'callWithSideEffects'
>>> tdiagnostic_messages.nim(34, 3) Hint: 'callWithSideEffects' accesses global state 'globalVar'
>>>> tdiagnostic_messages.nim(23, 5) Hint: 'globalVar' accessed by 'callWithSideEffects'
'''
"""
var globalVar = 0
proc myEcho(a: string) {.sideEffect.} = discard
proc indirectCallViaVarParam(call: var proc(): int {.nimcall.}): int =
call()
proc indirectCallViaPointer(call: pointer): int =
cast[ptr proc(): int](call)[]()
proc callWithSideEffects(): int =
var p = proc (): int {.nimcall.} = 0
discard indirectCallViaVarParam(p)
discard indirectCallViaPointer(addr p)
myEcho ""
globalVar
func a: int =
discard callWithSideEffects()