Skip to content

Guard conditional switch discriminants in optimized Ok()#266

Open
AaronWebster wants to merge 3 commits into
masterfrom
emboss/fix-conditional-switch-guard
Open

Guard conditional switch discriminants in optimized Ok()#266
AaronWebster wants to merge 3 commits into
masterfrom
emboss/fix-conditional-switch-guard

Conversation

@AaronWebster
Copy link
Copy Markdown
Collaborator

Problem

The optimized Ok() switch (introduced in #253, extended in #256/#257) reads its
discriminant once and emits, as a safety net:

const auto emboss_reserved_switch_discrim = <discriminant>;
if (!emboss_reserved_switch_discrim.Known()) return false;
switch (emboss_reserved_switch_discrim.ValueOrDefault()) { ... }

When the discriminant is itself a conditionally-present field and is read from a
buffer too short to contain it, the discriminant reads as Unknown, so this guard
makes Ok() return false — even though the message is valid, because the fields
gated on the (absent) discriminant don't exist.

This is latent on master: no existing test schema produces the shape, and the only
surviving guard in the goldens (parameters.emb's Axis) is on a bare arm, which is
sound.

Root cause

The guard is sound iff the group has at least one bare arm (existence condition
exactly discriminant == K): then an Unknown discriminant leaves that field's
has_X() Unknown, so the unoptimized Ok() bails for the same reason. When every
arm carries a residual (extra conjuncts beyond discriminant == K), a valid message can
have an out-of-bounds (Unknown) discriminant while every arm's residual is statically
false — making every has_X() Known-false (absent). The blanket guard wrongly rejects it.

Fix

In _emit_switch_block, classify each group:

  1. Provably-present discriminant → switch, no guard (unchanged, Drop redundant Known() guard when discriminant is provably Known #257).
  2. Not provably-present, ≥1 bare arm → switch + Known() guard (unchanged — sound).
  3. Not provably-present, all arms have residuals → guarded switch: dispatch only when
    the discriminant is in bounds, otherwise fall back to per-field has_X().Known() checks.
    (An Unknown discriminant can never make has_X() Known-true, so the value check is
    unnecessary in the fallback.)

Cases 1 & 2 are untouched, so every existing golden is byte-for-byte identical; only the
new structs add codegen.

Tests

Adds two structs to testdata/condition.emb:

  • ResidualConditionalDiscriminant — the bug (all-residual arms on a conditional discriminant).
  • BareConditionalDiscriminant — the sound-guard case (bare arms).

with regression tests in condition_test.cc covering: discriminant absent (valid, was
wrongly rejected), discriminant present-but-truncated (invalid), gated field present/truncated,
and discriminant matching no arm. The tests pass under both the optimized and unoptimized
(_no_opts) Ok() implementations, proving the optimized path now matches ground truth.

The same fix + tests will be propagated to the open stacked PRs (#258, #259) so the bug cannot
be reintroduced as they land.

The optimized Ok() switch reads its discriminant once and emits
`if (!emboss_reserved_switch_discrim.Known()) return false;` as a safety
net.  When the discriminant is a conditionally-present field read from a
buffer too short to contain it, the discriminant is Unknown and that
guard makes Ok() reject a message that is actually valid: the fields
gated on the (absent) discriminant simply do not exist.

The guard is only sound when the group has a "bare" arm whose existence
condition is exactly `discrim == K`.  Then an Unknown discriminant
leaves that field's has_X() Unknown, so the unoptimized Ok() bails for
the same reason.  When every arm carries a residual, an out-of-bounds
discriminant can coexist with a valid message whose residuals are all
statically false (so every has_X() is Known-false / absent).

For that case emit a guarded switch: dispatch only when the discriminant
is in bounds, otherwise fall back to per-field has_X().Known() checks
(an Unknown discriminant can never make has_X() Known-true, so the value
check is unnecessary).  Provably-present and bare-armed switches are
unchanged, so every existing golden is byte-for-byte identical.

Adds ResidualConditionalDiscriminant (the bug) and
BareConditionalDiscriminant (the sound-guard case) to condition.emb with
regression tests that pass under both the optimized and unoptimized Ok()
implementations.
@AaronWebster AaronWebster requested a review from robrussell June 4, 2026 20:38
Adds regression structs that distinguish the guard fix from a
presence-wrapper alternative and exercise the optimization chain:

  * DominatedBareDiscriminant: a bare arm on a conditional discriminant
    whose field is size-dominated by an always-present field, so
    IsComplete() stays Known.  Only the discriminant Known() guard
    rejects the existence-indeterminate message; a
    has_<d>().ValueOrDefault() wrapper would wrongly accept it.
  * DisjunctionConditionalDiscriminant: disjunction arms with residuals
    -> guarded switch with coalesced case labels.
  * SingleEntryConditionalDiscriminant: single-entry group demotes to a
    has_X() check, so no discriminant guard is emitted.
  * EnumConditionalDiscriminant: enum-typed conditional discriminant.

Only condition.emb.h regenerates; all other goldens are unchanged.
Tagged-union alternatives are mutually exclusive, so placing `a` and `b`
at the same offset is a more realistic layout (and avoids miscopying
when these structs are reused as templates for new tests).  Only the
physical field layout changes -- the switch structure and Ok() behavior
are identical.
@AaronWebster AaronWebster marked this pull request as ready for review June 5, 2026 02:03
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.

1 participant