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(sem): noreturn analysis for void exprs #1454

Merged

Conversation

saem
Copy link
Collaborator

@saem saem commented Sep 10, 2024

Summary

void branches preceeding with no-return terminal branches are checked
with no-return analysis to avoid false positives.

Details

The analysis is updated to recursively check if/elif/of/except
non-terminal branches as well as the terminal branch.

Fixes #1453

Summary
=======

`void` branches preceeding with no-return terminal branches are checked
with no-return analysis to avoid false positives.

Details
=====

The analysis is updated to recursively check `if/elif/of/except`
non-terminal branches as well as the terminal branch.
@saem saem added bug Something isn't working compiler/sem Related to semantic-analysis system of the compiler labels Sep 10, 2024
@zerbina
Copy link
Collaborator

zerbina commented Sep 10, 2024

Ah, dang -- I thought about this case when reviewing the previous PR, but somehow convinced myself it isn't a problem.

I do think it makes sense to fully use recursion now -- in my opinion, it's easier to reason about:

proc endsInNoReturn*(n: PNode): bool =
  ## Checks if expression `n` ends in an unstructured exit (raise, return,
  ## etc.) or a call of a noreturn proc. This is meant to be called on a
  ## semmed `n`.
  case n.kind
  of nkStmtList, nkStmtListExpr:
    result = n.len > 0 and endsInNoReturn(n[^1])
  of nkBlockStmt, nkExceptBranch, nkElifBranch, nkElse, nkOfBranch:
    result = endsInNoReturn(n[^1])
  of nkIfStmt:
    for it in n.items:
      result = endsInNoReturn(it[^1])
      if not result:
        break
  of nkCaseStmt:
    # skip the selector expression
    for i in 1..<n.len:
      result = endsInNoReturn(n[i])
      if not result:
        break
  of nkTryStmt:
    # ignore the 'finally' -- it doesn't contribute to the type
    for i in 0..<(n.len - ord(n[^1].kind == nkFinally)):
      result = endsInNoReturn(n[i])
      if not result:
        break
  of nkLastBlockStmts:
    result = true
  of nkCallKinds:
    result = n[0].kind == nkSym and sfNoReturn in n[0].sym.flags
  else:
    result = false

(this code also fixes the tnoreturn_nested.nim test failure.)

@saem
Copy link
Collaborator Author

saem commented Sep 10, 2024

Ah, dang -- I thought about this case when reviewing the previous PR, but somehow convinced myself it isn't a problem.

You're not the only one. 😆

I do think it makes sense to fully use recursion now -- in my opinion, it's easier to reason about:

proc endsInNoReturn*(n: PNode): bool =
  ## Checks if expression `n` ends in an unstructured exit (raise, return,
  ## etc.) or a call of a noreturn proc. This is meant to be called on a
  ## semmed `n`.
  case n.kind
  of nkStmtList, nkStmtListExpr:
    result = n.len > 0 and endsInNoReturn(n[^1])
  of nkBlockStmt, nkExceptBranch, nkElifBranch, nkElse, nkOfBranch:
    result = endsInNoReturn(n[^1])
  of nkIfStmt:
    for it in n.items:
      result = endsInNoReturn(it[^1])
      if not result:
        break
  of nkCaseStmt:
    # skip the selector expression
    for i in 1..<n.len:
      result = endsInNoReturn(n[i])
      if not result:
        break
  of nkTryStmt:
    # ignore the 'finally' -- it doesn't contribute to the type
    for i in 0..<(n.len - ord(n[^1].kind == nkFinally)):
      result = endsInNoReturn(n[i])
      if not result:
        break
  of nkLastBlockStmts:
    result = true
  of nkCallKinds:
    result = n[0].kind == nkSym and sfNoReturn in n[0].sym.flags
  else:
    result = false

(this code also fixes the tnoreturn_nested.nim test failure.)

Yeah, I didn't want to go that route, but I don't think anything else will be pleasant to read.

@saem
Copy link
Collaborator Author

saem commented Sep 11, 2024

/merge

Copy link

Merge requested by: @saem

Contents after the first section break of the PR description has been removed and preserved below:


Notes for Reviewers

  • yeesh, so much for the shortcuts I thought I could take with the analysis

@chore-runner chore-runner bot added this pull request to the merge queue Sep 11, 2024
Copy link
Collaborator

@zerbina zerbina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, the handling for non-exhaustive if/case statements is still missing.

if true:
  1
else:
  if true:   # <- `endsInNoReturn` would return true
    return

If added two suggestions for the fix. You might also want to add an additional test.

compiler/ast/ast_query.nim Show resolved Hide resolved
compiler/ast/ast_query.nim Show resolved Hide resolved
@zerbina zerbina removed this pull request from the merge queue due to a manual request Sep 11, 2024
@zerbina
Copy link
Collaborator

zerbina commented Sep 11, 2024

@saem: Sorry for aborting the merge, but I just noticed another issue that's probably better fixed as part of this PR.

@saem
Copy link
Collaborator Author

saem commented Sep 11, 2024

@saem: Sorry for aborting the merge, but I just noticed another issue that's probably better fixed as part of this PR.

No problem, I'll add some more tests. I haven't been able to think about the testing for this change all that well, so I'll take a day or two with it.

@saem
Copy link
Collaborator Author

saem commented Sep 13, 2024

I expect the tests to pass, and we could consider this PR done, although see the caveat below.


Thinking about it more an area where this might not work is something nested inside a loop:

proc foo() =
  let x =
  while true:
    return

The chance of writing that code is very small, and thankfully it's only a false-negative, but it looks like I need to expand the traversal to include while and for loop bodies. If I'm being really pedantic, then I should also check for while (return; true): ..., but that applies to all predicate positions.

compiler/ast/ast_query.nim Outdated Show resolved Hide resolved
Copy link
Collaborator

@zerbina zerbina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

The chance of writing that code is very small, and thankfully it's only a false-negative, but it looks like I need to expand the traversal to include while and for loop bodies

Given that neither while nor for can be used as expressions at the moment (outside of typeof expressions, but those are super special anyway), I don't think this is something to worry about.

It'd become relevant once introducing a unit type and making while and for real expressions, but at that point endsInNoReturn would be obsolete anyway.

@saem
Copy link
Collaborator Author

saem commented Sep 13, 2024

/merge

@chore-runner chore-runner bot added this pull request to the merge queue Sep 13, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Sep 13, 2024
@saem saem added this pull request to the merge queue Sep 13, 2024
Merged via the queue into nim-works:devel with commit 3c71f44 Sep 13, 2024
35 checks passed
@saem saem deleted the saem-fix-noreturn-analysis-eval-all-branches branch September 13, 2024 04:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working compiler/sem Related to semantic-analysis system of the compiler
Projects
None yet
Development

Successfully merging this pull request may close these issues.

false positive in nested noreturn algorithm
2 participants