Shared CFG: support for-else and while-else loops

Add two default predicates to AstSig:

  default AstNode getWhileElse(WhileStmt loop) { none() }
  default AstNode getForeachElse(ForeachStmt loop) { none() }

When defined, the explicit-step rules for While/Do and Foreach
route the loop's normal-completion exits through the else block
before reaching the after-loop node:

  - WhileStmt: after-false condition -> before-else -> after-while
    (instead of directly after-while).
  - ForeachStmt: after-collection [empty] and the LoopHeader exit
    are both routed through before-else -> after-foreach.

Python's Ast module overrides the predicates to return the
synthetic BlockStmt for the orelse slot, replacing the previous
customisations in Input::step. This eliminates parallel direct
successors emitted by the previous Python-side step additions
(verified: multipleSuccessors on a CPython database goes from
1340 to 0).

Java and C# CFG tests are unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot
2026-05-05 14:45:48 +00:00
committed by yoff
parent 158c81c06d
commit 577cf4a630
3 changed files with 47 additions and 32 deletions

View File

@@ -54,8 +54,6 @@
| test_loops.py:53:12:53:38 | After Compare | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break |
| test_loops.py:53:12:53:38 | After Compare | Timestamp 13 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break |
| test_loops.py:53:12:53:38 | After Compare | Timestamp 13 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break |
| test_loops.py:53:12:53:38 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break |
| test_loops.py:53:12:53:38 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break |
| test_loops.py:54:13:54:40 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break |
| test_loops.py:54:13:54:40 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break |
| test_loops.py:54:13:54:40 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break |
@@ -158,8 +156,6 @@
| test_loops.py:111:14:111:43 | After List | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break |
| test_loops.py:111:14:111:43 | After List | Timestamp 8 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break |
| test_loops.py:111:14:111:43 | After List | Timestamp 8 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break |
| test_loops.py:111:14:111:43 | After List | Timestamp 11 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break |
| test_loops.py:111:14:111:43 | After List | Timestamp 11 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break |
| test_loops.py:112:13:112:38 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break |
| test_loops.py:112:13:112:38 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break |
| test_loops.py:112:13:112:38 | After Compare | Timestamp 11 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break |
@@ -168,8 +164,6 @@
| test_loops.py:114:9:114:9 | x | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break |
| test_loops.py:114:9:114:9 | x | Timestamp 8 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break |
| test_loops.py:114:9:114:9 | x | Timestamp 8 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break |
| test_loops.py:114:9:114:9 | x | Timestamp 11 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break |
| test_loops.py:114:9:114:9 | x | Timestamp 11 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break |
| test_loops.py:123:14:123:33 | After List | Timestamp 21 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops |
| test_loops.py:123:14:123:33 | After List | Timestamp 21 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops |
| test_loops.py:124:18:124:47 | After List | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops |