Render switch discriminant once per group; drop dead inner scope (#253)

* Render switch discriminant once per group; drop dead inner scope

The optimized Ok() switch-block code in _generate_optimized_ok_method_body
was rendering each switch group's discriminant twice: once unscoped at
grouping time to build the SWITCH: key, and again with the active
ExpressionScope at emit time. Render it once with the scope and reuse
the result. The result is stable for equivalent discriminants because
ExpressionScope.add dedupes by inner rendered form, so it still works
as a grouping key.

The ok_method_switch_block template's \${inner_scope_definitions}
placeholder was unused — the inner ExpressionScope it referenced had
nothing added to it. Removed both, which also drops the blank line
each switch block carried in the goldens.

No behavioral change; purely compile-time cleanup. Golden churn
limited to the blank-line removal in four files (one line per existing
switch block).

* Sort switch cases by value; coalesce identical-body cases (#254)

Three composing changes to the optimized Ok() switch generator:

1. Case-label sort. Each switch arm's labels are sorted by the
   underlying integer/enum value before emit. _case_sort_key()
   returns the int for sorting. Sorted cases give older embedded
   GCCs (the ones shipped with microblaze-elf and many bare-metal
   arm-none-eabi toolchains) a better shot at emitting a dense
   jump table rather than an if-ladder.

2. Identical-body coalescing. Cases whose rendered body text is
   identical (same field set in the same order) are merged into a
   single arm with multiple \`case X:\` labels. The C++ compiler
   emits one body for the whole arm — a real text-size win once a
   later PR (disjunction matching) starts producing such pairs.

3. Multi-field per case. When two conditional fields share a
   discriminant + case value (\`if tag == 0: a\` and \`if tag == 0: b\`),
   they're now bundled into the same case arm rather than the second
   falling back to a separate if-statement. Each field's validation
   becomes one line of the case body.

The ok_method_switch_case template becomes ok_method_switch_arm,
taking pre-formatted \${case_labels} and \${case_body} strings.
Single-label single-field arms render identically to the old
template, so golden churn is limited to the f0_copy field in
testdata/many_conditionals.emb folding into case 0 of the
LargeConditionals switch.
diff --git a/compiler/back_end/cpp/generated_code_templates b/compiler/back_end/cpp/generated_code_templates
index ef99561..6d0de85 100644
--- a/compiler/back_end/cpp/generated_code_templates
+++ b/compiler/back_end/cpp/generated_code_templates
@@ -402,7 +402,6 @@
 // ** ok_method_switch_block ** ////////////////////////////////////////////////
     // Check fields that depend on a common discriminant using a switch.
     {
-${inner_scope_definitions}
       const auto emboss_reserved_switch_discrim = ${discriminant};
       if (!emboss_reserved_switch_discrim.Known()) return false;
       switch (emboss_reserved_switch_discrim.ValueOrDefault()) {
@@ -413,10 +412,8 @@
     }
 
 
-// ** ok_method_switch_case ** /////////////////////////////////////////////////
-        case ${case_value}:
-          if (!${field}().Ok()) return false;
-          break;
+// ** ok_method_switch_arm ** //////////////////////////////////////////////////
+${case_labels}${case_body}          break;
 
 
 
diff --git a/compiler/back_end/cpp/header_generator.py b/compiler/back_end/cpp/header_generator.py
index c8df290..9322e1f 100644
--- a/compiler/back_end/cpp/header_generator.py
+++ b/compiler/back_end/cpp/header_generator.py
@@ -1364,6 +1364,22 @@
         assert False, "Unsupported switch case type"
 
 
+def _case_sort_key(expression):
+    """Returns the underlying integer value of a switch case expression.
+
+    Used to sort case labels within a switch so the emitted C++ presents cases
+    in monotonic order. Compilers (particularly the older GCCs in many
+    embedded toolchains) are more likely to generate a dense jump table when
+    cases are sorted.
+    """
+    if expression.type.which_type == "integer":
+        return int(expression.type.integer.modular_value)
+    elif expression.type.which_type == "enumeration":
+        return int(expression.type.enumeration.value)
+    else:
+        assert False, "Unsupported switch case type"
+
+
 def _get_switch_candidate(expression, ir):
     """Returns (discriminant_expr, case_value_expr) or (None, None)."""
     if not _is_equality_check(expression):
@@ -1457,36 +1473,27 @@
     for field in fields:
         discrim_expr, case_expr = _get_switch_candidate(field.existence_condition, ir)
         if discrim_expr:
-            discrim_str = _render_expression(
-                discrim_expr, ir, subexpressions=None
+            # Render the discriminant once into the active subexpression scope.
+            # The rendered string is stable for equivalent discriminants (the
+            # scope dedupes by the inner rendered form), so it doubles as a
+            # grouping key and as the C++ to emit later. This avoids a second
+            # IR traversal at emit time for every switch group.
+            discrim_rendered = _render_expression(
+                discrim_expr, ir, subexpressions=subexpressions
             ).rendered
-            key = "SWITCH:" + discrim_str
+            key = "SWITCH:" + discrim_rendered
             if key not in groups:
                 groups[key] = {
                     "type": "switch",
-                    "expr": discrim_expr,
-                    "cases": [],
-                    "seen_cases": set(),
+                    "discrim_rendered": discrim_rendered,
+                    "cases_by_label": {},
                 }
                 ordered_keys.append(key)
-
             case_str = _render_case_label(case_expr, ir)
-            if case_str not in groups[key]["seen_cases"]:
-                groups[key]["seen_cases"].add(case_str)
-                groups[key]["cases"].append((case_str, field))
-            else:
-                cond_res = _render_expression(
-                    field.existence_condition, ir, subexpressions=None
-                )
-                if_key = "IF:" + cond_res.rendered
-                if if_key not in groups:
-                    groups[if_key] = {
-                        "type": "if",
-                        "expr": field.existence_condition,
-                        "fields": [],
-                    }
-                    ordered_keys.append(if_key)
-                groups[if_key]["fields"].append(field)
+            case_entry = groups[key]["cases_by_label"].setdefault(
+                case_str, {"sort_key": _case_sort_key(case_expr), "fields": []}
+            )
+            case_entry["fields"].append(field)
         else:
             cond_res = _render_expression(
                 field.existence_condition, ir, subexpressions=None
@@ -1495,7 +1502,6 @@
             if key not in groups:
                 groups[key] = {
                     "type": "if",
-                    "expr": field.existence_condition,
                     "fields": [],
                 }
                 ordered_keys.append(key)
@@ -1505,33 +1511,7 @@
     for key in ordered_keys:
         group = groups[key]
         if group["type"] == "switch":
-            discrim_rendered = _render_expression(
-                group["expr"], ir, subexpressions=subexpressions
-            ).rendered
-            # Create a new scope for the switch block
-            inner_scope = ExpressionScope(
-                "emboss_reserved_switch_scope_", parent=subexpressions
-            )
-
-            # Generate switch cases using template
-            switch_cases = []
-            for case_val, field in group["cases"]:
-                switch_cases.append(
-                    code_template.format_template(
-                        _TEMPLATES.ok_method_switch_case,
-                        case_value=case_val,
-                        field=_cpp_field_name(field.name.name.text),
-                    )
-                )
-
-            blocks.append(
-                code_template.format_template(
-                    _TEMPLATES.ok_method_switch_block,
-                    inner_scope_definitions=inner_scope.definition_code().rstrip(),
-                    discriminant=discrim_rendered,
-                    switch_cases="".join(switch_cases),
-                )
-            )
+            blocks.append(_emit_switch_block(group))
         else:
             for field in group["fields"]:
                 blocks.append(
@@ -1544,6 +1524,62 @@
     return "".join(blocks)
 
 
+def _render_case_body(fields):
+    """Renders the body of a single switch arm — the field validation tests."""
+    return "".join(
+        "          if (!{}().Ok()) return false;\n".format(
+            _cpp_field_name(f.name.name.text)
+        )
+        for f in fields
+    )
+
+
+def _emit_switch_block(group):
+    """Emits a complete switch block from a collected switch group.
+
+    Performs case-label sorting and identical-body coalescing:
+
+      * Case labels within an arm are sorted by underlying numeric value, so
+        the C++ compiler is presented with monotonic case sequences and is
+        more likely to emit a dense jump table on embedded targets.
+      * Distinct case values whose bodies are textually identical (which
+        happens when several disjuncts of `||` guard the same field, or when
+        multiple fields share an existence condition) are merged into a
+        single multi-label arm, so the compiler emits one body for all of
+        them rather than duplicating per case.
+    """
+    body_to_labels = {}
+    body_first_seen = {}
+    for case_str, case_entry in group["cases_by_label"].items():
+        body = _render_case_body(case_entry["fields"])
+        body_to_labels.setdefault(body, []).append((case_entry["sort_key"], case_str))
+        if body not in body_first_seen:
+            body_first_seen[body] = case_entry["sort_key"]
+
+    arms = []
+    for body, labels in body_to_labels.items():
+        labels.sort()  # by (sort_key, case_str)
+        arms.append((body_first_seen[body], labels, body))
+    arms.sort(key=lambda arm: arm[0])
+
+    rendered_arms = []
+    for _, labels, body in arms:
+        case_labels = "".join("        case {}:\n".format(cs) for _, cs in labels)
+        rendered_arms.append(
+            code_template.format_template(
+                _TEMPLATES.ok_method_switch_arm,
+                case_labels=case_labels,
+                case_body=body,
+            )
+        )
+
+    return code_template.format_template(
+        _TEMPLATES.ok_method_switch_block,
+        discriminant=group["discrim_rendered"],
+        switch_cases="".join(rendered_arms),
+    )
+
+
 def _generate_structure_definition(type_ir, ir, config: Config):
     """Generates C++ for an Emboss structure (struct or bits).
 
diff --git a/testdata/golden_cpp/many_conditionals.emb.h b/testdata/golden_cpp/many_conditionals.emb.h
index 9963594..ba120fb 100644
--- a/testdata/golden_cpp/many_conditionals.emb.h
+++ b/testdata/golden_cpp/many_conditionals.emb.h
@@ -97,6 +97,7 @@
       switch (emboss_reserved_switch_discrim.ValueOrDefault()) {
         case static_cast</**/ ::std::int32_t>(0LL):
           if (!f0().Ok()) return false;
+          if (!f0_copy().Ok()) return false;
           break;
 
         case static_cast</**/ ::std::int32_t>(1LL):
@@ -500,9 +501,6 @@
       }
     }
 
-    if (!has_f0_copy().Known()) return false;
-    if (has_f0_copy().ValueOrDefault() && !f0_copy().Ok()) return false;
-
     return true;
   }
   Storage BackingStorage() const { return backing_; }