Fix null-conditional field-like event raise NRE with no subscribers#2797
Merged
Conversation
A null-conditional raise `Evt?(args)` of a field-like event threw NullReferenceException when the event had no subscribers instead of being a no-op, breaking the canonical `Evt?(...)` / `Evt?.Invoke(...)` guard for both nominal `EventHandler` and structural `(object?, EventArgs) -> void` events. PR #2792 (fix/2726) canonicalizes conventional structural event shapes and nominal `EventHandler` events to the CLR `System.EventHandler`/`EventHandler<T>` delegates, so their invocation now routes through the CLR-delegate call path in `OverloadResolver.BindCallExpression`. Unlike the FunctionTypeSymbol and named-delegate paths (which guard the void raise via `BuildIndirectDelegateCall`), that path emitted an unconditional `callvirt Invoke`, dereferencing a null backing delegate on a fresh event and regressing the structural raise that worked before #2792. Restore the short-circuit: when a null-conditional invocation targets a void-returning CLR delegate, bind the `Invoke` against a captured receiver and wrap it in a BoundNullConditionalAccessExpression so a null backing delegate is a safe no-op, matching C# `Evt?.Invoke(args)`. This covers both nominal and structural field-like events without any Oahu-specific logic. Add runtime + ILVerify coverage for zero-subscriber and subscribed nominal and structural events, unsubscribe-then-raise, and the generic `EventHandler<T>` shape. Fixes #2796
2505817596
pushed a commit
to 2505817596/gsharp-net472
that referenced
this pull request
Jul 24, 2026
…avidObando#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 DavidObando#2797 (issue DavidObando#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, DavidObando#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 DavidObando#2798
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.
Summary
A null-conditional raise
Evt?(args)of a field-like event threwNullReferenceExceptionwhen the event had no subscribers instead of being a no-op. This broke the canonicalEvt?(...)/Evt?.Invoke(...)short-circuit for both nominalEventHandlerand structural(object?, EventArgs) -> voidevents, and blocked 3 migrated Oahu tests atOahu.Core.SettingsBase.OnChange.Root cause (regression of #2792)
PR #2792 (
fix/2726) canonicalizes conventional structural event shapes and nominalEventHandlerevents to the CLRSystem.EventHandler/EventHandler<T>delegates. Their invocation therefore now routes through the CLR-delegate call path inOverloadResolver.BindCallExpression(dispatch viaInvoke). Unlike theFunctionTypeSymboland named-delegate paths — which already guard the void raise viaBuildIndirectDelegateCall— that path emitted an unconditionalcallvirt Invoke, dereferencing a null backing delegate on a fresh event.Commit bisect (from the issue): structural
evt_struct_nullraise works at2621fa0a(#2791) and breaks atc84ca771(#2792); both nominal and structural fail at the cycle-16 tip.Fix
In the CLR-delegate call path, when a null-conditional invocation targets a void-returning CLR delegate, bind
Invokeagainst a captured receiver and wrap it in aBoundNullConditionalAccessExpression, so a null backing delegate short-circuits to a safe no-op — matching C#Evt?.Invoke(args). This covers both nominal and structural field-like events with no Oahu-specific logic. Value-returning conditional invocations keep their existing lowering.Emitted IL for the raise now becomes the standard null guard:
Tests
New
Issue2796NullConditionalEventRaiseEmitTests(runtime + ILVerify via the sharedIlVerifier.Verifyhelper):EventHandlerzero-subscriber raise no-ops; with a subscriber invokes(object?, EventArgs) -> voidzero-subscriber raise no-ops; with a subscriber invokesSettingsBase.OnChangefailure mode)EventHandler<T>zero-subscriber no-op and subscribed invokeValidation
Compiler.Tests: 3495 passed, 0 failedCore.Tests: 6704 passed, 1 skippedevt_raise.gs,evt_struct_null.gsno-op & exit 0;evt_nonnull.gshits=1) pass and ILVerify cleanOahu.Cli.Tests342/342 pass (was 339/342)MSBUILDDISABLENODEREUSE=1Fixes #2796