Skip to content

Fix InvalidProgramException for Nullable<T> tuple nil comparisons (#3626) - #3628

Merged
DavidObando merged 1 commit into
mainfrom
oats/fix-invalidprogramexception-fo-8921ba93
Aug 29, 2026
Merged

Fix InvalidProgramException for Nullable<T> tuple nil comparisons (#3626)#3628
DavidObando merged 1 commit into
mainfrom
oats/fix-invalidprogramexception-fo-8921ba93

Conversation

@oats-publisher

Copy link
Copy Markdown
Contributor

This fixes issue #3626. The reported symptom was InvalidProgramException in an
async, non-awaited if let over an imported named tuple, but the actual root
cause is more general: any tupleNullable != nil / == nil comparison emits
invalid IL (ldfld <Nullable<Tuple>>; ldnull; ceq, verified by ilverify as a
StackUnexpected error) whenever the tuple's ClrType is symbolic/null —
which happens whenever an element carries a nullable-reference-type
annotation (e.g. string?), regardless of sync/async, same-compilation vs.
imported, or named vs. unnamed tuple.

Root cause: LiftedBinaryOperatorCollector.IsLiftedValueTypeBinary in
SlotPlanner.cs decided whether a T? == nil comparison needs the lifted
HasValue-based emission by probing UnderlyingType.ClrType.IsValueType
directly, instead of using the canonical NullableLifting.IsValueTypeNullable
predicate (which already special-cases TupleTypeSymbol for exactly this
reason). When the probe missed, the comparison fell through to a generic
ldnull; ceq path — invalid for a value-type Nullable<T>. The follow-on
EmitLiftedNullableBinary/GetNullableHasValueRef/GetNullableValueRef
emit helpers had the same class of bug: they dereferenced
UnderlyingType.ClrType (or gated on the narrower
IsUserValueTypeNullable) instead of the broader
NullableLifting.RequiresSymbolicNullableGetValue, which already routes
symbolic tuples through the existing symbolic get_HasValue/get_Value
MemberRef construction used elsewhere (null-conditional receiver probe,
(v!!) unwrap).

Fix: swap the ad hoc ClrType-based checks for the existing canonical
NullableLifting predicates in three places:

  • SlotPlanner.cs: IsLiftedValueTypeBinary now uses
    NullableLifting.IsValueTypeNullable.
  • MethodBodyEmitter.Operators.cs: EmitLiftedNullableBinary's nil-compare
    form now resolves get_HasValue through GetNullableHasValueRef instead
    of dereferencing ClrType directly; GetNullableHasValueRef/
    GetNullableValueRef now gate on RequiresSymbolicNullableGetValue
    instead of IsUserValueTypeNullable.

Tests: added test/Compiler.Tests/Emit/Issue3626NullableTupleNilComparisonTests.cs
covering (1) a same-compilation sync if let with both the match and nil
branches, (2) the original async/non-awaited/imported/named-tuple repro, and
(3) the same with an unnamed tuple (confirming names are not load-bearing).
All three compile+ilverify+run and fail without the fix. Also ran the
existing Nullable (600), Async (272), Tuple (89), and Lifted/IfLet (79)
scoped test suites — all pass with no regressions.

@DavidObando
DavidObando merged commit a6606c1 into main Aug 29, 2026
36 checks passed
@DavidObando
DavidObando deleted the oats/fix-invalidprogramexception-fo-8921ba93 branch August 29, 2026 04:45
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