Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix noreturn/implicit discard check logic #23681

Merged
merged 5 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 1 addition & 60 deletions compiler/sem.nim
Original file line number Diff line number Diff line change
Expand Up @@ -222,66 +222,7 @@ proc shouldCheckCaseCovered(caseTyp: PType): bool =
else:
discard

proc endsInNoReturn(n: PNode): bool =
## check if expr ends the block like raising or call of noreturn procs do
result = false # assume it does return

template checkBranch(branch) =
if not endsInNoReturn(branch):
# proved a branch returns
return false

var it = n
# skip these beforehand, no special handling needed
while it.kind in {nkStmtList, nkStmtListExpr} and it.len > 0:
it = it.lastSon

case it.kind
of nkIfStmt:
var hasElse = false
for branch in it:
checkBranch:
if branch.len == 2:
branch[1]
elif branch.len == 1:
hasElse = true
branch[0]
else:
raiseAssert "Malformed `if` statement during endsInNoReturn"
# none of the branches returned
result = hasElse # Only truly a no-return when it's exhaustive
of nkCaseStmt:
let caseTyp = skipTypes(it[0].typ, abstractVar-{tyTypeDesc})
# semCase should already have checked for exhaustiveness in this case
# effectively the same as having an else
var hasElse = caseTyp.shouldCheckCaseCovered()

# actual noreturn checks
for i in 1 ..< it.len:
let branch = it[i]
checkBranch:
case branch.kind
of nkOfBranch:
branch[^1]
of nkElifBranch:
branch[1]
of nkElse:
hasElse = true
branch[0]
else:
raiseAssert "Malformed `case` statement in endsInNoReturn"
# Can only guarantee a noreturn if there is an else or it's exhaustive
result = hasElse
of nkTryStmt:
checkBranch(it[0])
for i in 1 ..< it.len:
let branch = it[i]
checkBranch(branch[^1])
# none of the branches returned
result = true
else:
result = it.kind in nkLastBlockStmts or
it.kind in nkCallKinds and it[0].kind == nkSym and sfNoReturn in it[0].sym.flags
proc endsInNoReturn(n: PNode): bool

proc commonType*(c: PContext; x: PType, y: PNode): PType =
# ignore exception raising branches in case/if expressions
Expand Down
145 changes: 132 additions & 13 deletions compiler/semstmts.nim
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,140 @@ proc semExprBranchScope(c: PContext, n: PNode; expectedType: PType = nil): PNode
closeScope(c)

const
skipForDiscardable = {nkIfStmt, nkIfExpr, nkCaseStmt, nkOfBranch,
nkElse, nkStmtListExpr, nkTryStmt, nkFinally, nkExceptBranch,
skipForDiscardable = {nkStmtList, nkStmtListExpr,
nkOfBranch, nkElse, nkFinally, nkExceptBranch,
nkElifBranch, nkElifExpr, nkElseExpr, nkBlockStmt, nkBlockExpr,
nkHiddenStdConv, nkHiddenDeref}

proc implicitlyDiscardable(n: PNode): bool =
var n = n
while n.kind in skipForDiscardable: n = n.lastSon
result = n.kind in nkLastBlockStmts or
(isCallExpr(n) and n[0].kind == nkSym and
sfDiscardable in n[0].sym.flags)
# same traversal as endsInNoReturn
template checkBranch(branch) =
if not implicitlyDiscardable(branch):
return false

var it = n
# skip these beforehand, no special handling needed
while it.kind in skipForDiscardable and it.len > 0:
it = it.lastSon

case it.kind
of nkIfExpr, nkIfStmt:
for branch in it:
checkBranch:
if branch.len == 2:
branch[1]
elif branch.len == 1:
branch[0]
else:
raiseAssert "Malformed `if` statement during implicitlyDiscardable"
# all branches are discardable
result = true
of nkCaseStmt:
for i in 1 ..< it.len:
let branch = it[i]
checkBranch:
case branch.kind
of nkOfBranch:
branch[^1]
of nkElifBranch:
branch[1]
of nkElse:
branch[0]
else:
raiseAssert "Malformed `case` statement in endsInNoReturn"
# all branches are discardable
result = true
of nkTryStmt:
checkBranch(it[0])
for i in 1 ..< it.len:
let branch = it[i]
if branch.kind != nkFinally:
checkBranch(branch[^1])
# all branches are discardable
result = true
of nkCallKinds:
result = it[0].kind == nkSym and {sfDiscardable, sfNoReturn} * it[0].sym.flags != {}
of nkLastBlockStmts:
result = true
else:
result = false

proc endsInNoReturn(n: PNode, returningNode: var PNode): bool =
## check if expr ends the block like raising or call of noreturn procs do
result = false # assume it does return

template checkBranch(branch) =
if not endsInNoReturn(branch, returningNode):
# proved a branch returns
return false

var it = n
# skip these beforehand, no special handling needed
while it.kind in skipForDiscardable and it.len > 0:
it = it.lastSon

case it.kind
of nkIfExpr, nkIfStmt:
var hasElse = false
for branch in it:
checkBranch:
if branch.len == 2:
branch[1]
elif branch.len == 1:
hasElse = true
branch[0]
else:
raiseAssert "Malformed `if` statement during endsInNoReturn"
# none of the branches returned
result = hasElse # Only truly a no-return when it's exhaustive
of nkCaseStmt:
let caseTyp = skipTypes(it[0].typ, abstractVar-{tyTypeDesc})
# semCase should already have checked for exhaustiveness in this case
# effectively the same as having an else
var hasElse = caseTyp.shouldCheckCaseCovered()

# actual noreturn checks
for i in 1 ..< it.len:
let branch = it[i]
checkBranch:
case branch.kind
of nkOfBranch:
branch[^1]
of nkElifBranch:
branch[1]
of nkElse:
hasElse = true
branch[0]
else:
raiseAssert "Malformed `case` statement in endsInNoReturn"
# Can only guarantee a noreturn if there is an else or it's exhaustive
result = hasElse
of nkTryStmt:
checkBranch(it[0])
var lastIndex = it.len - 1
if it[lastIndex].kind == nkFinally:
# if finally is noreturn, then the entire statement is noreturn
if endsInNoReturn(it[lastIndex][^1], returningNode):
return true
dec lastIndex
for i in 1 .. lastIndex:
let branch = it[i]
checkBranch(branch[^1])
# none of the branches returned
result = true
of nkLastBlockStmts:
result = true
of nkCallKinds:
result = it[0].kind == nkSym and sfNoReturn in it[0].sym.flags
if not result:
returningNode = it
else:
result = false
returningNode = it

proc endsInNoReturn(n: PNode): bool =
var dummy: PNode = nil
result = endsInNoReturn(n, dummy)

proc fixNilType(c: PContext; n: PNode) =
if isAtom(n):
Expand All @@ -165,13 +288,9 @@ proc discardCheck(c: PContext, result: PNode, flags: TExprFlags) =
localError(c.config, result.info, "expression has no type: " &
renderTree(result, {renderNoComments}))
else:
var n = result
while n.kind in skipForDiscardable:
if n.kind == nkTryStmt: n = n[0]
else: n = n.lastSon

# Ignore noreturn procs since they don't have a type
if n.endsInNoReturn:
var n = result
if result.endsInNoReturn(n):
return

var s = "expression '" & $n & "' is of type '" &
Expand Down
2 changes: 1 addition & 1 deletion lib/std/syncio.nim
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,7 @@ proc writeFile*(filename: string, content: openArray[byte]) {.since: (1, 1).} =
var f: File = nil
if open(f, filename, fmWrite):
try:
f.writeBuffer(unsafeAddr content[0], content.len)
discard f.writeBuffer(unsafeAddr content[0], content.len)
finally:
close(f)
else:
Expand Down
12 changes: 12 additions & 0 deletions tests/discard/t23677.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
discard """
errormsg: "expression '0' is of type 'int literal(0)' and has to be used (or discarded); start of expression here: t23677.nim(1, 1)"
line: 10
column: 3
"""

# issue #23677

if true:
0
else:
raise newException(ValueError, "err")
30 changes: 30 additions & 0 deletions tests/discard/tdiscardable.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ tdiscardable
1
something defered
something defered
hi
'''
"""

Expand Down Expand Up @@ -110,3 +111,32 @@ block:

doAssertRaises(ValueError):
doAssert foo() == 12

block: # issue #10440
proc x(): int {.discardable.} = discard
try:
x()
finally:
echo "hi"

import macros

block: # issue #14665
macro test(): untyped =
let b = @[1, 2, 3, 4]

result = nnkStmtList.newTree()
var i = 0
while i < b.len:
if false:
# this quote do is mandatory, removing it fixes the problem
result.add quote do:
let testtest = 5
else:
result.add quote do:
let test = 6
inc i
# removing this continue fixes the problem too
continue
inc i
test()
19 changes: 19 additions & 0 deletions tests/discard/tfinallyerrmsg.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
discard """
cmd: "nim check $file"
"""

block: # issue #19672
try:
10 #[tt.Error
^ expression '10' is of type 'int literal(10)' and has to be used (or discarded); start of expression here: tfinallyerrmsg.nim(5, 1)]#
finally:
echo "Finally block"

block: # issue #13871
template t(body: int) =
try:
body
finally:
echo "expression"
t: 2 #[tt.Error
^ expression '2' is of type 'int literal(2)' and has to be used (or discarded)]#
Loading