Skip to content

transpile: rewrite relooper to be iterative to avoid stack overflow on large CFGs - #1995

Open
Hellboy28D wants to merge 2 commits into
immunant:masterfrom
Hellboy28D:iterative-relooper
Open

transpile: rewrite relooper to be iterative to avoid stack overflow on large CFGs#1995
Hellboy28D wants to merge 2 commits into
immunant:masterfrom
Hellboy28D:iterative-relooper

Conversation

@Hellboy28D

Copy link
Copy Markdown

Summary

The relooper in c2rust-transpile used a recursive algorithm for control-flow
reconstruction. On functions with very large/deep CFGs (e.g. thousands of
sequential basic blocks, as in the BLAKE-256 example in #1821), this could
exhaust the stack and crash the transpiler.

This PR rewrites the relooper to work iteratively, using an explicit
dominator computation instead of recursion, so structured control-flow
reconstruction no longer depends on stack depth proportional to CFG size.

Changes

  • Replaced the recursive relooper implementation in
    c2rust-transpile/src/cfg/relooper.rs with an iterative one
  • Added unit tests for the dominator computation

Testing

  • cargo test -p c2rust-transpile relooper — all 7 new dominator tests pass
  • Transpiled the BLAKE-256 C implementation from Reworked relooper regressed on some large CFGs #1821: output is
    byte-identical to the output produced by upstream master, confirming the
    rewrite preserves translation semantics
  • Transpiles previously-failing large-CFG inputs without requiring an
    increased ulimit -s

Notes

Happy to adjust the approach or add more tests based on maintainer feedback.

@thedataking

thedataking commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Please fix formatting issues.

The PR body uses codex/gpt default style which explains how your agent tested the PR. It is better that you only present the information necessary for review; this is also more consistent with other PRs.

Other than that, this looks good to me (thanks!) but @randomPoison must decide whether this PR can land.

@randomPoison randomPoison left a comment

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.

Well, this seems better than your other PR; Tests pass, and there are few extraneous changes that break things. You need to fix formatting to satisfy CI, though.

The main thing I'd like to see changed is that we're still recursing at the end of make_loop to process the things that follow the loop. That means that a sufficiently long sequence of loops will still cause us to stack overflow. Change make_loop to not call relooper at the end, and in the cases where we're returning after calling make_loop that should change to continue.

You also need to update the issue_1821_transpiles_quickly test case to remove the extended stack limit so that we can verify that we no longer overflow the stack for that test case. That test case is setup to run in another thread as a hack, so that extra thread setup should also be able to be removed as part of this PR.

Comment on lines +307 to +316
entries: entries.clone(),
body,
span,
terminator,
});

// Continue processing the successors of the block we just
// emitted instead of recursing. This keeps sequential runs
// of `Simple` blocks from growing the stack (c2rust#1908).
entries = new_entries;

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.

You're cloning entries only to immediately throw away the old value and replace it with new_entries. You can avoid this by mem::takeing the old value, which will replace it with an empty IndexSet.

Comment on lines +504 to +513
let inlined: Vec<Label> = follow_entries
.iter()
.filter(|&e| self.global_predecessors[e].len() == 1);
.filter(|&e| self.global_predecessors[e].len() == 1)
.cloned()
.collect();

// Move all nodes dominated by an inlined node into the loop. This will include
// the inlined node since all nodes dominate themself.
for inlined in inlined {
for dominated in &self.domination_sets[inlined] {
for entry in &inlined {
for dominated in &self.domination_sets[entry] {

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.

What are these changed bits doing? I don't see a reason why we need to collect this iterator, and this change doesn't seem like it affects behavior at all.


impl RelooperState {
/// Recursive helper for `reloop`.
/// Helper for `reloop`.

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.

This function is still recursive.

// can be reached by other entries. This means irreducible control flow, which
// we have to process by making a loop.
self.make_loop(&strict_reachable_from, blocks, entries, result);
return;

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.

We don't want to return after make_loop, we want to continue iterating to process the blocks that follow the loop.


result.push(Structure::Multiple { entries, branches });

// Continue processing the follow blocks instead of recursing.

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.

We don't need this "instead of recursing" comment.

Comment on lines +76 to +77
// `cfg.entries` is the CFG's single entry `Label`; `relooper` wants a set
// of entries so it can be reused uniformly for `Multiple` branches too.

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.

This comment is unnecessary.

Comment on lines 216 to +223
///
/// TODO: perhaps manually perform TCO?
/// Sequential chains — runs of `Simple` blocks, and the follow-blocks
/// after a `Multiple` — are processed with an explicit loop rather than
/// tail recursion, so large functions with long straight-line CFGs
/// cannot exhaust the stack (c2rust#1908). Recursion is still used for
/// the individual branches of a `Multiple` and inside `make_loop`, since
/// that recursion depth is bounded by nesting depth rather than block
/// 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.

This comment is unnecessary.

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.

3 participants