Skip to content

Translate switch with default case in the middle#30

Merged
nunoplopes merged 44 commits intoCpp2Rust:masterfrom
lucic71:default-case-in-the-middle
Apr 28, 2026
Merged

Translate switch with default case in the middle#30
nunoplopes merged 44 commits intoCpp2Rust:masterfrom
lucic71:default-case-in-the-middle

Conversation

@lucic71
Copy link
Copy Markdown
Contributor

@lucic71 lucic71 commented Apr 27, 2026

This PR handles code as:

// tests/unit/switch_default_middle.cpp
int default_middle(int x) {
  int r = 0;
  switch (x) {
  case 1:
    r = 1;
    break;
  default:
    r = 99;
    break;
  case 2:
    r = 2;
    break;
  }
  return r;
}

In rust, the default arm has to be the last arm, otherwise, arms following the default one will be ignored, making this assertion fail assert(default_middle(2) == 2). Hence, the rust translation becomes:

 match x {
     v if v == 1 => {
         r = 1;
     }
     v if v == 2 => {
         r = 2;
     }
     _ => {
         r = 99;
     }
 }

This is achieved by iterating over all top-level case statements in VisitSwitchStmt and deferring the translation of the default arm until every othter arm is translated. A top-level case statement is defined as:

switch (x) {
case TOP_LEVEL_STMT_1:
case NOT_TOP_LEVEL_STMT_1:
default: // not top level as well
  ...
  break;
case TOP_LEVEL_STMT_2:
case NOT_TOP_LEVEL_STMT_2:
  ...
  break
}

Furthermore, chains of case statements that contain a default statement are reduced to default, effectively making the above code translate as:

match x {
  v if v == TOP_LEVEL_STMT_2 || v == NOT_TOP_LEVEL_STMT_2 => {}
  _ => {} // TOP_LEVEL_STMT_1, NOT_TOP_LEVEL_STMT_1, default was reduced to default
}

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.
} else {
StrCat("_ => {");
}
StrCat("v if v == ");
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.

instead of v == 1 || v == 2 || v == 3, can't you do:

 match x {
            1 | 2 | 3 => ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's because of enums. A switch such as:

enum E {
        A,
        B,
        C
};

int main() {
        enum E e = A;
        switch (e) {
        case A:
        case B:
                break;
        case C:
                break;
        }
        return 0;
}

would get translated as:

        match e {
            // (E::A as u32)  is an expression, not a pattern, so it's not allowed to be chained using |
            (E::A as u32) | (E::B as u32) => {
                break;
            }
            v if v == (E::C as u32) => {
                break;
            }
            _ => {}
        }

I think we can just ignore the implicit enum to integer cast in order to allow this to work with | instead of ||

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.

Ok, then let's keep it as-is. In C++ you can mix enums and integers.

@nunoplopes
Copy link
Copy Markdown
Contributor

Please rebase; the branch has conflicts.

@lucic71
Copy link
Copy Markdown
Contributor Author

lucic71 commented Apr 27, 2026

Please rebase; the branch has conflicts.

Done

@nunoplopes
Copy link
Copy Markdown
Contributor

CI failed.

@lucic71 lucic71 force-pushed the default-case-in-the-middle branch from 7e175d1 to aa3013e Compare April 27, 2026 21:18
@lucic71
Copy link
Copy Markdown
Contributor Author

lucic71 commented Apr 27, 2026

CI failed.

Fixed

@nunoplopes nunoplopes merged commit 9802d0a into Cpp2Rust:master Apr 28, 2026
9 checks passed
@lucic71 lucic71 deleted the default-case-in-the-middle branch April 29, 2026 13:20
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