Skip to content

Translate switches containing fallthrough#38

Merged
nunoplopes merged 57 commits intoCpp2Rust:masterfrom
lucic71:fallthrough
Apr 29, 2026
Merged

Translate switches containing fallthrough#38
nunoplopes merged 57 commits intoCpp2Rust:masterfrom
lucic71:fallthrough

Conversation

@lucic71
Copy link
Copy Markdown
Contributor

@lucic71 lucic71 commented Apr 28, 2026

All switch tests pass now.

This PR translates switches containing fallthrough using the new switch! macro defined in libcc2rs-macros:

    switch!(match <condition> {
        <pat> [if <guard>] => { /* body; may contain break or continue */ },
        ...
        _ => <body>,
    });

Desugars to a goto_block! with a synthetic dispatch arm prepended.

    goto_block! {
        '__dispatch => {
            match <condition> {
                <pat_1> => { __s = 1; continue '__sm; }
                ...
                _       => break '__sm,
            }
        },
        '__c1 => { /* body_1 with `break` rewritten to `break '__sm` */ },
        ...
        '__cN => { /* body_N with same rewrite */ },
    };

__sm is the inner label used to describe the state machine insinde goto_block. See goto_block!
for more info.

It's necessary to translate the switch into a goto because later we will add support for jumping between switch arms using goto.

Below is an example of how the switch! macro expands:

int fallthrough_one(int x) {
  int r = 0;
  switch (x) {
  case 1:
    r += 10;
  case 2:
    r += 20;
    break;
  default:
    r = -1;
    break;
  }
  return r;
}

Will be translated as:

    switch!(match x {
        v if v == 1 => {
            r += 10;
        }
        v if v == 2 => {
            r += 20;
            break;
        }
        _ => {
            r = -1_i32;
            break;
        }
    });

Which will expand into (see comments attached to each arm):

    {                                                                                                                                                                                         
        let mut __s: u32 = 0;                                                                                                                                                                 
                                                                                                                                                                                              
        #[allow(unreachable_code, unused_labels)]                      
        '__sm: loop {                
            match __s {
                // First arm is the dispatch arm. It decides which is the first state: [1u32, ...]
                0u32 => {

                    #[allow(unreachable_code)]
                    {
                        {

                            #[allow(unreachable_patterns)]
                            match x {
                                v if v == 1 => { __s = 1u32; continue '__sm; }
                                v if v == 2 => { __s = 2u32; continue '__sm; }
                                _ => { __s = 3u32; continue '__sm; }
                                _ => break '__sm,
                            }
                        };
                        __s = 1u32;
                        continue '__sm;
                    }
                }
                1u32 => {
                    // First real arm, it contains the body of the original arm + fallthrough to the following state, i.e. 2u32
                    #[allow(unreachable_code)]
                    { { r += 10; }; __s = 2u32; continue '__sm; }
                }
                2u32 => {
                    // Second real arm, inside the body there is a break statement that stops the fallthrough
                    #[allow(unreachable_code)]
                    { { r += 20; break '__sm; }; __s = 3u32; continue '__sm; }
                }
                3u32 => {
                    // Default arm from the original match, the last arm breaks the state machine instea of continuing
                    #[allow(unreachable_code)]
                    { { r = -1_i32; break '__sm; }; break '__sm; }
                }
                // This is here only for match exhaustiveness, it's never used
                _ => break '__sm,
            }
        }
    };

lucic71 added 30 commits April 22, 2026 16:46
Rename visited_cases to visited_switch_cases_ and move it in class
scope. VisitSwitchStmt always resets the set of visited switch cases.

The old implementation was buggy because it saved reused SwitchCase
pointers across translation units, making contain() return true for
switches declared in other TUs.
C++ allows:

  switch (x) {
  default:
    ...
  case 1:
    ...
  }

In rust, default needs to be always on the last position, otherwise,
all values of x, even 1, will hit the default arm. Hence, the above C++
exmaple becomes:

  match x {
    1 => ...
    _ => ...
  }

As such, the algorithm for translating the cases becomes:

  for (case: GetTopLevelSwitchCases()) {
    if (ChainContainsDefault(case)) {
      defer the conversion for the end
    }
    Convert(case)
    Convert(GetSwitchArmBody(case))
  }
  Convert(deferred default)

ChainContainsDefault traverses the stacked case statements starting from
a top level case. For example:

  case 2:
  case 3:
  default:
    ...

is deferred for the end of the match arms and is translated as:

  _ => {}

i.e., drop the case 2, case 3 from the output, only convert as if it was
only default.
@lucic71 lucic71 marked this pull request as draft April 28, 2026 20:34
@lucic71 lucic71 marked this pull request as ready for review April 29, 2026 09:17
@nunoplopes nunoplopes merged commit 2165500 into Cpp2Rust:master Apr 29, 2026
9 checks passed
@lucic71 lucic71 deleted the fallthrough branch April 29, 2026 13:19
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.

2 participants