Skip to content

feat: Modify dead code elimination pass to remove unreachable basic blocks#2884

Merged
tatiana-s merged 6 commits intomainfrom
ts/dead-code-blocks
Mar 2, 2026
Merged

feat: Modify dead code elimination pass to remove unreachable basic blocks#2884
tatiana-s merged 6 commits intomainfrom
ts/dead-code-blocks

Conversation

@tatiana-s
Copy link
Copy Markdown
Contributor

Closes #2597

@tatiana-s tatiana-s requested a review from acl-cqc February 18, 2026 14:57
@tatiana-s tatiana-s requested a review from a team as a code owner February 18, 2026 14:57
@codecov
Copy link
Copy Markdown

codecov bot commented Feb 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.83%. Comparing base (d146ebc) to head (6011a15).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2884      +/-   ##
==========================================
+ Coverage   83.81%   83.83%   +0.01%     
==========================================
  Files         269      269              
  Lines       53967    54018      +51     
  Branches    47920    47971      +51     
==========================================
+ Hits        45235    45286      +51     
  Misses       6317     6317              
  Partials     2415     2415              
Flag Coverage Δ
python 88.65% <ø> (ø)
rust 83.22% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@acl-cqc acl-cqc left a comment

Choose a reason for hiding this comment

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

Thanks @tatiana-s! The logic looks fine+good; a few ideas/suggestions for tidying slightly, but only thing I'm really bothered about is that I would like to strengthen the test - we looked at diagrams to see the action was correct, which is much more than we are doing in the test at the moment.

q.push_back(src);
if h.get_optype(n).is_dataflow_block() || h.get_optype(n).is_exit_block() {
// Follow control flow forwards to find reachable basic blocks besides entry and exit.
for src in h.output_neighbours(n) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: does q.extend(h.output_neighbours(n)) work?

}
} else {
// Follow dataflow demand (including e.g edges from Call to FuncDefn) backwards.
for src in h.input_neighbours(n) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

similarly q.extend(h.input_neighbours(n))

|| match h.get_optype(ch) {
OpType::Case(_) => true, // Include all Cases in Conditionals
OpType::DataflowBlock(_) => h.get_optype(n).is_cfg() && i == 0, // Assumes entry block is always the first child of a CFG.
OpType::ExitBlock(_) => true,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

consider using | in the pattern....even

OpType::Case // Include all Cases in Conditionals
| OpType::ExitBlock
| OpType::AliasDecl` // and all aliases (we do not....
| OpType::Output

OpType::AliasDefn(_) => true,
OpType::Input(_) => true, // Also Dataflow input/output, these are necessary for legality
OpType::Output(_) => true,
// Do not include FuncDecl / FuncDefn / Const unless reachable by static edges (from Call/LoadConst/LoadFunction)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: that is a super-long line

// Following ControlFlow edges backwards is harmless, we've already assumed all
// BBs are reachable above.
q.push_back(src);
if h.get_optype(n).is_dataflow_block() || h.get_optype(n).is_exit_block() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider match? Or matches!(h.get_optype(n), OpType::DataflowBlock(_) | OpType::ExitBlock(_))

// Run pass and check that unreachable block is removed
DeadCodeElimPass::default().run(&mut h).unwrap();
h.validate().unwrap();
let num_nodes_after = h.nodes().count();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think nodes do not get renumbered unless we serialize, so you should be able to get that block_unreachable is no longer a valid node?

You could also check the number of children of the CFG, as that only goes down by one (which is a bit more obvious), and you can verify that the cfg node is still a cfg.

Each of those would be a slightly stronger check that we have done (removed) the right thing. Good to check total number of nodes too tho i.e. that we have removed the entire subtree not just one node!

Extra brownie points for checking that the remaining children of the CFG each have a single output-neighbour being the next child (child ordering stays consistent so this does not require searching); also that the exitblock has only one predecessor now.

@tatiana-s tatiana-s requested a review from acl-cqc February 23, 2026 17:14
Copy link
Copy Markdown
Contributor

@acl-cqc acl-cqc left a comment

Choose a reason for hiding this comment

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

Looks great, thanks @tatiana-s, sorry for delay in rereview!

h.validate().unwrap();
let num_nodes_before = h.nodes().count();
let cfg_node = h.entrypoint();
let num_cfg_children_before: usize = h
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider making this a closure that takes h as a parameter (but captures cfg_node, which is Copy so easy), so you can call it twice (once before, once after)

@tatiana-s tatiana-s added this pull request to the merge queue Mar 2, 2026
Merged via the queue into main with commit 819507b Mar 2, 2026
30 checks passed
@tatiana-s tatiana-s deleted the ts/dead-code-blocks branch March 2, 2026 10:34
@hugrbot hugrbot mentioned this pull request Mar 2, 2026
github-merge-queue bot pushed a commit that referenced this pull request Mar 6, 2026
## 🤖 New release

* `hugr-model`: 0.25.6 -> 0.25.7 (✓ API compatible changes)
* `hugr-core`: 0.25.6 -> 0.25.7 (✓ API compatible changes)
* `hugr-llvm`: 0.25.6 -> 0.25.7 (✓ API compatible changes)
* `hugr-passes`: 0.25.6 -> 0.25.7 (✓ API compatible changes)
* `hugr-persistent`: 0.4.6 -> 0.4.7 (✓ API compatible changes)
* `hugr`: 0.25.6 -> 0.25.7 (✓ API compatible changes)
* `hugr-cli`: 0.25.6 -> 0.25.7 (✓ API compatible changes)

<details><summary><i><b>Changelog</b></i></summary><p>

## `hugr-model`

<blockquote>

##
[0.25.6](hugr-model-v0.25.5...hugr-model-v0.25.6)
- 2026-02-20

### New Features

- Remove size limitation for binary envelopes
([#2880](#2880))
</blockquote>

## `hugr-core`

<blockquote>

##
[0.25.7](hugr-core-v0.25.6...hugr-core-v0.25.7)
- 2026-03-06

### Documentation

- added examples in docs srtring
([#2920](#2920))
</blockquote>

## `hugr-llvm`

<blockquote>

##
[0.25.6](hugr-llvm-v0.25.5...hugr-llvm-v0.25.6)
- 2026-02-20

### New Features

- Add error context when lowering hugrs to LLVM
([#2869](#2869))
</blockquote>

## `hugr-passes`

<blockquote>

##
[0.25.7](hugr-passes-v0.25.6...hugr-passes-v0.25.7)
- 2026-03-06

### Documentation

- added examples in docs srtring
([#2920](#2920))

### New Features

- Define pass application scopes
([#2772](#2772))
- Modify dead code elimination pass to remove unreachable basic blocks
([#2884](#2884))
- Add non-generic `with_scope` method for composable passes
([#2910](#2910))
- update passes to use PassScope where non-breaking
([#2836](#2836))
</blockquote>

## `hugr-persistent`

<blockquote>

##
[0.4.0](hugr-persistent-v0.3.4...hugr-persistent-v0.4.0)
- 2025-12-22

### New Features

- [**breaking**] Remove `RootCheckable`
([#2704](#2704))
- [**breaking**] Bump MSRV to Rust 1.89
([#2747](#2747))
- [**breaking**] Type-safe access for node metadata
([#2755](#2755))

### Refactor

- [**breaking**] Remove multiple deprecated definitions
([#2751](#2751))
</blockquote>

## `hugr`

<blockquote>

##
[0.25.7](hugr-v0.25.6...hugr-v0.25.7)
- 2026-03-06

### Documentation

- added examples in docs srtring
([#2920](#2920))

### New Features

- Define pass application scopes
([#2772](#2772))
- Modify dead code elimination pass to remove unreachable basic blocks
([#2884](#2884))
- Add non-generic `with_scope` method for composable passes
([#2910](#2910))
- update passes to use PassScope where non-breaking
([#2836](#2836))
</blockquote>

## `hugr-cli`

<blockquote>

##
[0.25.6](hugr-cli-v0.25.5...hugr-cli-v0.25.6)
- 2026-02-20

### New Features

- Add s expression format to envelope formats
([#2864](#2864))
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
@hugrbot hugrbot mentioned this pull request Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DeadCodeElim: remove unreachable basic blocks

2 participants