Fix value-returning field-like event null-conditional invocation NRE#2799
Conversation
…2798) A null-conditional invocation `Evt?(args)` of a subscriber-less field-like event whose delegate returns a value threw NullReferenceException instead of producing a null/optional result. PR #2797 (issue #2796) fixed the void-returning raise on both the nominal CLR-delegate path and the structural FunctionTypeSymbol path, but both guarded only the void result and emitted an unconditional `callvirt Invoke` for value-returning delegates, dereferencing a null backing delegate on a fresh event with no subscribers. Generalize the established nullable-delegate invocation result-slot lowering (the same lowering TryBindNullableDelegateInvocation uses for nullable delegate properties, #2772) to both paths: - OverloadResolver.CallBinding.cs: the nominal CLR `Func<...>` / `EventHandler` delegate path now wraps a value-returning `Invoke` in a BoundNullConditionalAccessExpression whose result type is the nullable wrapping of the delegate's return type, allocating a `$nres_` result slot for value-typed returns. A null receiver yields null/default; the receiver is evaluated once; a non-null delegate invokes normally. - OverloadResolver.Invocations.cs (BuildIndirectDelegateCall): the structural `(T) -> R` event path applies the same lowering for value-returning fnType. This is a general emitter/binder fix with no Oahu-specific logic. Covers nominal Func and structural function delegates, reference- and value-returning shapes, zero-subscriber / subscribed / unsubscribe-then-call cases, side-effectful argument single-evaluation and short-circuit, natural `??` fallback consumption (including value-type null coalescing), runtime behavior, metadata, and ILVerify. Value-type null coalescing over the null-conditional result verifies clean, so no separate emitter defect is exposed. Fixes #2798
…eiver kinds (#2799 follow-up) BuildIndirectDelegateCall only guarded the null-conditional invocation form `f?(args)` of a non-nullable structural function-typed receiver when the variable was the implicit backing field of a field-like event (ImplicitFieldVariableSymbol). Every other receiver kind reaching that path — a bare static field (ImplicitStaticFieldVariableSymbol), a bare instance/static property (ImplicitPropertyVariableSymbol / ImplicitStaticPropertyVariableSymbol), and a plain local, parameter, or top-level global (LocalVariableSymbol / ParameterSymbol / GlobalVariableSymbol) — fell through to an unguarded BoundIndirectCallExpression, silently dropping the `?`: the result was bound as the non-optional return type (so `r == nil` failed to bind and value-type `??` misbehaved) and a null receiver NRE-d instead of short-circuiting. Fix: - OverloadResolver.Invocations.cs (BuildIndirectDelegateCall): compute the receiver load and its static type uniformly for every variable kind, then apply the null-conditional guard across all of them. The receiver is evaluated exactly once into a capture; value-returning invocations adopt the established result-slot lowering (nullable result type, $nres_ slot for value-type returns), void invocations are a plain no-op. Extracted the shared result construction into BuildNullConditionalDelegateResult, reused by the CLR-delegate path too. - OverloadResolver.CallBinding.cs (nominal CLR-delegate path): fix a distinct, tightly-related pre-existing defect — the receiver was computed as a bare BoundVariableExpression and never used TryBuildImplicitMemberLoad, so a nominal Func<...> value exposed as a bare static field or instance/static property ICE-d with GS9998 ("no local slot") on ANY invocation (conditional or not). The receiver-load fix mirrors the structural path. All seven VariableSymbol kinds that can reach these paths are now handled; no reachable delegate receiver kind silently drops `?` or ICEs. General binder fix, no Oahu-specific logic. New Issue2799NullConditionalStructuralDelegateReceiverEmitTests (runtime + ILVerify): local, parameter, instance property, static field, static property, and global receivers; value- and void-returning shapes; null short-circuit and non-null invoke where null is language-reachable (uninitialized function-typed field CLR default); side-effectful property receiver single-evaluation; bare `== nil` optional-result semantics; plus nominal CLR Func property/static-member receivers (conditional and non-conditional).
Follow-up (review): generalize null-conditional structural delegate invocation across all receiver kindsThe final review found a tightly-related deferred gap, now completed on this branch. Gap
Proof (before code change)
Fix
All seven TestsNew Validation
|
Summary
A null-conditional invocation
Evt?(args)of a subscriber-less field-like event whose delegate returns a value threwNullReferenceExceptioninstead of producing a null/optional result. PR #2797 (issue #2796) fixed the void-returning raise on both the nominal CLR-delegate path and the structuralFunctionTypeSymbolpath, but both guarded only thevoidresult and emitted an unconditionalcallvirt Invokefor value-returning delegates — dereferencing a null backing delegate on a fresh event with no subscribers.Repros (both threw NRE with no subscribers)
Root cause
OverloadResolver.CallBinding.cs: the nominal CLR-delegate null-conditional block guarded onlyvoidresults, falling through to a plaincallvirt Invokefor value-returning delegates.OverloadResolver.Invocations.cs(BuildIndirectDelegateCall): guarded onlyvoidFunctionTypeSymbolresults.Fix
Generalize the established nullable-delegate invocation result-slot lowering (the same lowering
TryBindNullableDelegateInvocationuses for nullable delegate properties, #2772) to both paths. A value-returning conditional invocation is wrapped in aBoundNullConditionalAccessExpressionwhose result type is the nullable wrapping of the delegate's return type, allocating a$nres_result slot for value-typed returns. A null receiver yieldsnull/default, the receiver is evaluated once, and a non-null delegate invokes normally. This is a general binder/emitter fix with no Oahu-specific logic.Emitted lowering now matches C#
Evt?.Invoke(args): value-type returns materializedefault(Nullable<T>)on the null branch and wrapTon the not-null branch; reference-type returns leaveldnull.Tests
New
Issue2798NullConditionalValueEventRaiseEmitTests(runtime + ILVerify via the sharedIlVerifier.Verifyhelper):Func<int,int>value-returning: zero-subscriber → null (?? -1⇒ -1); subscribed → invokes (10)(int32) -> int32value-returning: zero-subscriber → null fallback; subscribed → invokesFunc<string,string>and structural(string) -> stringreference-returning: zero-subscriber → null (?? "none"); subscribed → invokes??):r == niltrue with no subscribers, false once subscribedSettingsBase.OnChangefailure mode, value-returning variant)Validation
Compiler.Tests: 3504 passed, 0 failedCore.Tests: 6704 passed, 1 skipped, 0 failedNullable<int> ?? -1) over the null-conditional result verifies and runs clean, so no separate emitter defect is exposed by the value-type fallbackMSBUILDDISABLENODEREUSE=1Fixes #2798