fix(semantic/cfg): connect for update exit to loop test (#23791)

## Summary

Correct the CFG generated for `for` statements whose update expression spans multiple basic blocks, such as conditional or logical expressions.

This addresses the malformed CFG underlying #23789.

## Problem

`update_graph_ix` identifies the entry of the update expression. After visiting the expression, `cfg.current_node_ix` identifies its exit block.

Previously, the loop backedge to the test originated from the update entry. This allowed control flow to bypass the update expression and left its exit block disconnected.

### Before

```mermaid
flowchart LR
    Test["Loop test"] -->|truthy| Body["Loop body"]
    Test -->|falsy| After["After loop"]

    Body -->|backedge| Entry["Update entry"]
    Entry --> Update["Update expression blocks"]
    Update --> Exit["Update exit"]

    Entry -->|backedge| Test
```

### After

```mermaid
flowchart LR
    Test["Loop test"] -->|truthy| Body["Loop body"]
    Test -->|falsy| After["After loop"]

    Body -->|backedge| Entry["Update entry"]
    Entry --> Update["Update expression blocks"]
    Update --> Exit["Update exit"]
    Exit -->|backedge| Test
```

## Implementation

Record `cfg.current_node_ix` after visiting the update expression as `after_update_graph_ix`. The backedge to the loop test now originates from that exit block.

The loop body backedge and `continue` target still point to `update_graph_ix`, ensuring the complete update expression runs before the next test.

A dedicated CFG fixture covers a conditional update expression:

```js
forward ? index++ : index--
```
This commit is contained in:
camc314 committed 2026-06-25 17:25:58 +00:00
1 parent d3e92d5258
commit 869ac200a4
3 files changed
+91 -3

No files matched your search

+4 -3
View File
@@ -1333,10 +1333,11 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> {
/* cfg */
#[cfg(feature = "cfg")]
let before_body_graph_ix = control_flow!(self, |cfg| {
let (after_update_graph_ix, before_body_graph_ix) = control_flow!(self, |cfg| {
let after_update_graph_ix = cfg.current_node_ix;
let before_body_graph_ix = cfg.new_basic_block_normal();
cfg.ctx(None).default().allow_break().allow_continue();
before_body_graph_ix
(after_update_graph_ix, before_body_graph_ix)
});
/* cfg */
@@ -1349,7 +1350,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> {
cfg.add_edge(before_for_graph_ix, test_graph_ix, EdgeType::Normal);
cfg.add_edge(after_test_graph_ix, before_body_graph_ix, EdgeType::Jump);
cfg.add_edge(after_body_graph_ix, update_graph_ix, EdgeType::Backedge);
cfg.add_edge(update_graph_ix, test_graph_ix, EdgeType::Backedge);
cfg.add_edge(after_update_graph_ix, test_graph_ix, EdgeType::Backedge);
cfg.add_edge(after_test_graph_ix, after_for_stmt, EdgeType::Normal);
cfg.ctx(None)
@@ -0,0 +1,3 @@
for (let index = 0; index < length; forward ? index++ : index--) {
consume(index);
}
@@ -0,0 +1,84 @@
---
source: crates/oxc_semantic/tests/integration/cfg.rs
expression: snapshot
input_file: crates/oxc_semantic/tests/integration/cfg_fixtures/for_conditional_update.js
---
bb0: {
}
bb1: {
statement
statement
}
bb2: {
condition
}
bb3: {
}
bb4: {
condition
}
bb5: {
}
bb6: {
}
bb7: {
}
bb8: {
statement
statement
}
bb9: {
}
digraph {
0 [ label = "bb0" shape = box]
1 [ label = "bb1
ForStatement
VariableDeclaration" shape = box]
2 [ label = "bb2
Condition(BinaryExpression(<))" shape = box]
3 [ label = "bb3" shape = box]
4 [ label = "bb4
Condition(IdentifierReference(forward))" shape = box]
5 [ label = "bb5" shape = box]
6 [ label = "bb6" shape = box]
7 [ label = "bb7" shape = box]
8 [ label = "bb8
BlockStatement
ExpressionStatement" shape = box]
9 [ label = "bb9" shape = box]
1 -> 0 [ label="Error(Implicit)", color=red, style=dashed]
2 -> 0 [ label="Error(Implicit)", color=red, style=dashed]
3 -> 0 [ label="Error(Implicit)", color=red, style=dashed]
4 -> 0 [ label="Error(Implicit)", color=red, style=dashed]
5 -> 0 [ label="Error(Implicit)", color=red, style=dashed]
6 -> 0 [ label="Error(Implicit)", color=red, style=dashed]
7 -> 0 [ label="Error(Implicit)", color=red, style=dashed]
3 -> 4 [ label="Normal"]
5 -> 7 [ label="Normal"]
4 -> 5 [ label="Jump", color=green]
4 -> 6 [ label="Normal"]
6 -> 7 [ label="Normal"]
8 -> 0 [ label="Error(Implicit)", color=red, style=dashed]
9 -> 0 [ label="Error(Implicit)", color=red, style=dashed]
1 -> 2 [ label="Normal"]
2 -> 8 [ label="Jump", color=green]
8 -> 3 [ label="Backedge", color=grey]
7 -> 2 [ label="Backedge", color=grey]
2 -> 9 [ label="Normal"]
}