Extend switch matcher: disjunction, conjunction, single-entry demotion#256
Open
AaronWebster wants to merge 1 commit into
Open
Extend switch matcher: disjunction, conjunction, single-entry demotion#256AaronWebster wants to merge 1 commit into
AaronWebster wants to merge 1 commit into
Conversation
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.
This was referenced May 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR #241's
_get_switch_candidateonly matched barediscrim == Kequalities. Real protocol grammars express the same intent in many forms; this PR rewrites the matcher to recognize them all._extract_switch_armsdecomposes anexistence_conditioninto a(discriminant, [(case_value, residual), …])tuple, handling:tag == K) — one arm, no residual.tag == A || tag == B || tag == C) — one arm perKi, 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.tag == K && other_predicate) — one arm carrying[other_predicate]as residual. Useful for nested guards likeif outer_flag && tag == K.(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_testwhen the count is below 2 — without this, a lone field likeif outer && tag == K: xcwould 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 mutatedsubexpressionsduring the grouping pass, so groups that later got demoted would have left deadconst auto = …;definitions in the emittedOk(). The scoped render now happens at emit time, only for surviving groups.The benchmark schema gains a
DisjunctionConditionalsstruct exercising three||chains (2, 3, 3 labels) and the benchmark TU gains a runtime test for it. Golden churn:many_conditionals.emb.hgains the new struct's output;condition.emb.handvirtual_field.emb.hshrink because severalBasicConditional-style structs had a lonexcfield that PR #241 was wrapping in a one-arm switch — demotion brings them back to the cheaperhas_xc()check.Size impact (cumulative vs. master)
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.