From 507136ec1a37a7e967785c37341f0c9c27233215 Mon Sep 17 00:00:00 2001 From: Aaron Webster Date: Tue, 19 May 2026 15:42:46 -0700 Subject: [PATCH 1/2] Render switch discriminant once per group; drop dead inner scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../back_end/cpp/generated_code_templates | 1 - compiler/back_end/cpp/header_generator.py | 26 +++++++------------ 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/compiler/back_end/cpp/generated_code_templates b/compiler/back_end/cpp/generated_code_templates index ef995612..21ace12d 100644 --- a/compiler/back_end/cpp/generated_code_templates +++ b/compiler/back_end/cpp/generated_code_templates @@ -402,7 +402,6 @@ ${write_fields} // ** 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()) { diff --git a/compiler/back_end/cpp/header_generator.py b/compiler/back_end/cpp/header_generator.py index c8df2902..0c46bc06 100644 --- a/compiler/back_end/cpp/header_generator.py +++ b/compiler/back_end/cpp/header_generator.py @@ -1457,14 +1457,19 @@ def _generate_optimized_ok_method_body(fields, ir, subexpressions): 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, + "discrim_rendered": discrim_rendered, "cases": [], "seen_cases": set(), } @@ -1482,7 +1487,6 @@ def _generate_optimized_ok_method_body(fields, ir, subexpressions): if if_key not in groups: groups[if_key] = { "type": "if", - "expr": field.existence_condition, "fields": [], } ordered_keys.append(if_key) @@ -1495,7 +1499,6 @@ def _generate_optimized_ok_method_body(fields, ir, subexpressions): if key not in groups: groups[key] = { "type": "if", - "expr": field.existence_condition, "fields": [], } ordered_keys.append(key) @@ -1505,14 +1508,6 @@ def _generate_optimized_ok_method_body(fields, ir, subexpressions): 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"]: @@ -1527,8 +1522,7 @@ def _generate_optimized_ok_method_body(fields, ir, subexpressions): blocks.append( code_template.format_template( _TEMPLATES.ok_method_switch_block, - inner_scope_definitions=inner_scope.definition_code().rstrip(), - discriminant=discrim_rendered, + discriminant=group["discrim_rendered"], switch_cases="".join(switch_cases), ) ) From a5f26ac612a3e67430fa6d6a49b9c24b1f16cfa2 Mon Sep 17 00:00:00 2001 From: Aaron Webster Date: Wed, 3 Jun 2026 17:30:40 -0700 Subject: [PATCH 2/2] Sort switch cases by value; coalesce identical-body cases (#254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../back_end/cpp/generated_code_templates | 6 +- compiler/back_end/cpp/header_generator.py | 114 ++++++++++++------ testdata/golden_cpp/many_conditionals.emb.h | 4 +- 3 files changed, 81 insertions(+), 43 deletions(-) diff --git a/compiler/back_end/cpp/generated_code_templates b/compiler/back_end/cpp/generated_code_templates index 21ace12d..6d0de85b 100644 --- a/compiler/back_end/cpp/generated_code_templates +++ b/compiler/back_end/cpp/generated_code_templates @@ -412,10 +412,8 @@ ${switch_cases} } -// ** 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 0c46bc06..9322e1f8 100644 --- a/compiler/back_end/cpp/header_generator.py +++ b/compiler/back_end/cpp/header_generator.py @@ -1364,6 +1364,22 @@ def _render_case_label(expression, ir): 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): @@ -1470,27 +1486,14 @@ def _generate_optimized_ok_method_body(fields, ir, subexpressions): groups[key] = { "type": "switch", "discrim_rendered": discrim_rendered, - "cases": [], - "seen_cases": set(), + "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", - "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 @@ -1508,24 +1511,7 @@ def _generate_optimized_ok_method_body(fields, ir, subexpressions): for key in ordered_keys: group = groups[key] if group["type"] == "switch": - # 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, - discriminant=group["discrim_rendered"], - switch_cases="".join(switch_cases), - ) - ) + blocks.append(_emit_switch_block(group)) else: for field in group["fields"]: blocks.append( @@ -1538,6 +1524,62 @@ def _generate_optimized_ok_method_body(fields, ir, subexpressions): 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 9963594f..ba120fb4 100644 --- a/testdata/golden_cpp/many_conditionals.emb.h +++ b/testdata/golden_cpp/many_conditionals.emb.h @@ -97,6 +97,7 @@ class GenericLargeConditionalsView final { switch (emboss_reserved_switch_discrim.ValueOrDefault()) { case static_cast(0LL): if (!f0().Ok()) return false; + if (!f0_copy().Ok()) return false; break; case static_cast(1LL): @@ -500,9 +501,6 @@ class GenericLargeConditionalsView final { } } - if (!has_f0_copy().Known()) return false; - if (has_f0_copy().ValueOrDefault() && !f0_copy().Ok()) return false; - return true; } Storage BackingStorage() const { return backing_; }