Skip to content

Extend switch matcher: disjunction, conjunction, single-entry demotion#256

Open
AaronWebster wants to merge 1 commit into
emboss/sort-coalescefrom
emboss/extended-matcher
Open

Extend switch matcher: disjunction, conjunction, single-entry demotion#256
AaronWebster wants to merge 1 commit into
emboss/sort-coalescefrom
emboss/extended-matcher

Conversation

@AaronWebster
Copy link
Copy Markdown
Collaborator

PR #241's _get_switch_candidate only matched bare discrim == K equalities. Real protocol grammars express the same intent in many forms; this PR rewrites the matcher to recognize them all.

_extract_switch_arms decomposes an existence_condition into a (discriminant, [(case_value, residual), …]) tuple, handling:

  • Bare equality (tag == K) — one arm, no residual.
  • Disjunction of equalities on a shared discriminant (tag == A || tag == B || tag == C) — one arm per Ki, no residual. Common for tagged unions where several values share a payload. Combined with Sort switch cases by value; coalesce identical-body cases #254's identical-body coalescing, this collapses to a single multi-label switch arm.
  • Conjunction with an equality (tag == K && other_predicate) — one arm carrying [other_predicate] as residual. Useful for nested guards like if outer_flag && tag == K.
  • Mixed shapes inside a disjunction: (tag == 0 && a) || (tag == 1 && b) || tag == 2 — three arms with respective residuals [a], [b], [].

An arm with a residual emits the has_${field}()-based check as its case body (the residual is folded into the existing accessor) — sound and lets the C++ compiler fold the case-pinned discriminant comparison via inlining.

A demotion pass measures total arm-entries per group and falls back to ok_method_test when the count is below 2 — without this, a lone field like if outer && tag == K: xc would get wrapped in a one-case switch whose overhead (temporary, Known() guard, scope braces) exceeds the dedupe. This also fixes a latent bug from #253: the scoped discriminant render mutated subexpressions during the grouping pass, so groups that later got demoted would have left dead const auto = …; definitions in the emitted Ok(). The scoped render now happens at emit time, only for surviving groups.

The benchmark schema gains a DisjunctionConditionals struct exercising three || chains (2, 3, 3 labels) and the benchmark TU gains a runtime test for it. Golden churn: many_conditionals.emb.h gains the new struct's output; condition.emb.h and virtual_field.emb.h shrink because several BasicConditional-style structs had a lone xc field that PR #241 was wrapping in a one-arm switch — demotion brings them back to the cheaper has_xc() check.

Size impact (cumulative vs. master)

Target Metric Master PR Delta
ARM Thumb-2 TU .text 18962 18208 −754 (−4.0%)
ARM Thumb-2 LargeConditionals::Ok() 5382 4746 −636 (−11.8%)
ARM Thumb-2 DisjunctionConditionals::Ok() 358 270 −88 (−24.6%)
MicroBlaze TU .text 43640 42612 −1028 (−2.4%)
MicroBlaze LargeConditionals::Ok() 14824 14104 −720 (−4.9%)
MicroBlaze DisjunctionConditionals::Ok() 640 552 −88 (−13.8%)
Host x86-64 TU .text 29166 28304 −862 (−3.0%)
Host x86-64 LargeConditionals::Ok() 3948 3065 −883 (−22.4%)
Host x86-64 DisjunctionConditionals::Ok() 431 375 −56 (−13.0%)

Incremental vs. #254: DisjunctionConditionals::Ok() drops 24.6% on Thumb-2, 13.8% on MicroBlaze, 13.0% on x86-64 — the matcher extensions account for nearly all of those.

Stacked on #254.

PR 241's _get_switch_candidate only matched bare \`discrim == K\`
equalities. Real protocol grammars express the same intent in many
forms; this PR rewrites the matcher to recognize them all.

_extract_switch_arms decomposes an existence_condition into a
(discriminant, [(case_value, residual), ...]) tuple, handling:

  * Bare equality (\`tag == K\`) — one arm, no residual.
  * Disjunction of equalities on a shared discriminant
    (\`tag == A || tag == B || tag == C\`) — one arm per Ki, no
    residual. Common for tagged unions where several values share a
    payload. Combined with the identical-body coalescing already in
    place, this collapses to a single multi-label switch arm.
  * Conjunction with an equality (\`tag == K && other_predicate\`) —
    one arm carrying \`[other_predicate]\` as residual. Useful for
    nested guards like \`if outer_flag && tag == K\`.
  * Mixed shapes inside a disjunction:
    \`(tag == 0 && a) || (tag == 1 && b) || tag == 2\` — three arms
    with respective residuals \`[a]\`, \`[b]\`, \`[]\`.

An arm with a residual emits the has_\${field}()-based check as its
case body (the residual is folded into the existing accessor) — sound
and lets the C++ compiler fold the case-pinned discriminant
comparison via inlining.

A demotion pass measures total arm-entries per group and falls back
to ok_method_test when the count is below 2 — without this, a lone
field like \`if outer && tag == K: xc\` would get wrapped in a one-case
switch whose overhead (temporary, Known() guard, scope braces)
exceeds the dedupe. This also fixes a latent bug introduced when
render-dedup was added: the scoped discriminant render mutated
subexpressions during the grouping pass, so groups that later got
demoted would have left dead \`const auto = ...\` definitions in the
emitted Ok(). The scoped render now happens at emit time, only for
surviving groups.

The benchmark schema gains a DisjunctionConditionals struct
exercising three \`||\` chains (2, 3, 3 labels) and the benchmark TU
gains a runtime test for it. Golden churn: many_conditionals.emb.h
gains the new struct's output; condition.emb.h and virtual_field.emb.h
shrink because several BasicConditional-style structs had a lone xc
field that PR 241 was wrapping in a one-arm switch — demotion brings
them back to the cheaper has_xc() check.
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