Fix #3858: Detect automatic events on the ILAst instead of the C# AST - #3886
Merged
Conversation
Structural recognition of compiler-generated code needs method bodies as ILAst decompiled with a fixed set of settings, so that recognition does not depend on user-visible options. RecordDecompiler had this pipeline as a private helper; hoist it to CSharpDecompiler so other recognizers can share it, deriving the generic context from the method's declaring type instead of a captured type definition. Assisted-by: Claude:claude-fable-5:Claude Code Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
siegfriedpammer
force-pushed
the
fix-3858-automatic-events
branch
3 times, most recently
from
July 18, 2026 17:22
f029d90 to
ca5fb4c
Compare
The syntactic accessor-body patterns in PatternStatementTransform sit downstream of every settings-dependent transform, so each new compiler shape or settings combination silently broke recognition: with AggressiveInlining enabled, static events inline the Delegate.Combine call into CompareExchange positionally, which none of the four patterns matched, while call sites were still rewritten to the event name from metadata alone - producing uncompilable output (CS0079). Recognition now happens in DoDecompile(IEvent) by structurally matching the ILAst of the accessors, decompiled with a fixed set of settings the same way RecordDecompiler analyzes method bodies. This makes detection independent of the user-visible settings by construction. Events that are not recognized fall back to the classic path unchanged, including the existing AST patterns. mcs 2.x compiles the accessors as a compound assignment, evaluating 'this' once via IL 'dup'; the simple-combine matcher accepts that stack-slot alias. Assisted-by: Claude:claude-fable-5:Claude Code
MemberIsHidden decides field hiding from the metadata name association alone, which over-approximates: an event whose accessors fail the ILAst validation is decompiled with explicit accessors, and while a referenced backing field is re-added through the work list, an unreferenced one was silently dropped from the output. The type-definition member loop now consults the same memoized verdict as the event declaration, so the two decisions agree by construction. Assisted-by: Claude:claude-fable-5:Claude Code Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
PatternStatementTransform renamed backing-field identifiers to the event after the fact, keyed on the metadata name association alone: references belonging to an event that is not actually automatic were still renamed, binding them to a custom event that is unusable as a value (the same defect class as #3858), and the rename bypassed the resolver checks, so qualifiers were computed for the hidden field instead of the printed event. ExpressionBuilder.ConvertField now performs the substitution, keyed on the AutoEventDecompiler verdict whose memo moves into DecompileRun so that member hiding, the event declaration, and reference translation all decide from one analysis. Checking the verdict's field identity also keeps same-typed sibling events apart (#3575), and the qualifier logic running against the event drops spurious this./type qualifiers from raise sites. mcs 2.x accesses a sibling automatic event's backing field directly inside custom accessors instead of calling the accessor, so the fixture expects the resulting Delegate.Combine form there. Assisted-by: Claude:claude-fable-5:Claude Code Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
With recognition on the ILAst and reference substitution during translation, the four accessor-body patterns in PatternStatementTransform were only reachable as a fallback, and any divergence between them and the AutoEventDecompiler verdict produced inconsistent output. A compiler shape the ILAst matchers do not know now degrades to explicit accessors with the backing field kept in the output, which stays compilable. Bodyless events (abstract, extern, interface members) previously relied on the patterns' no-body clause to become field-like; since C# cannot express bodyless custom accessors, DoDecompile now chooses the field-like form for them directly. Also deletes the orphaned IsEventBackingFieldName helper; the name association lives in PropertyAndEventBackingFieldLookup. Assisted-by: Claude:claude-fable-5:Claude Code Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
siegfriedpammer
force-pushed
the
fix-3858-automatic-events
branch
from
July 18, 2026 17:32
ca5fb4c to
538f7ae
Compare
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.
Automatic-event recognition moves off the syntactic accessor-body patterns in
PatternStatementTransformonto structural matching of the accessor ILAst, decompiled with a fixed set of settings - the same techniqueRecordDecompileruses for its method bodies. Detection becomes independent of user-visible settings by construction, and a single memoized verdict drives every event-related decision: hiding the backing field, choosing the declaration form, and translating references to the backing field. A recognized automatic event is built in field-like form directly - a custom event declaration is never constructed and collapsed after the fact, and the compiler-generated accessor bodies are never decompiled.Five self-contained commits:
AutoEventDecompilermatches the accessor shapes structurally: the csc4/Roslyn compare-exchange loop, the pre-4.0[MethodImpl(Synchronized)]simple-combine form (including the mcs 2.x variant that evaluatesthisonce via ILdup), and the mcs 5.x loop. On a positive verdict,DoDecompile(IEvent)setsUseCustomEvents = falsesoTypeSystemAstBuilderemits the field-like declaration directly; surviving accessor and backing-field attributes are added from the type system asmethod:/field:sections, dropping the ones the compiler puts there.MemberIsHiddendecides field hiding from the name association alone; when validation rejects the event, an unreferenced backing field used to vanish from the output entirely (referenced ones were re-added through the work list).PatternStatementTransformintoExpressionBuilder.ConvertField, keyed on the verdict. This stops references belonging to a non-automatic event from being rewritten to the event name (the reference-side half of the CS0079 defect class), keeps same-typed sibling events apart via a field-identity check (the Uses of compiler-generated events getting swapped with non-generated events of same type #3575 scenario, now pinned by a fixture), and lets the qualifier logic run against the member actually printed, dropping spuriousthis./type qualifiers from raise sites.DoDecompilenow chooses the field-like form for them directly.Behavior notes:
StaticEvent()instead ofStaticAutomaticEvents.StaticEvent(),Ainstead ofthis.A); fixtures updated accordingly.Tested with the full decompiler suite locally and all CI workflows across the compiler matrix (Roslyn 1.3.2-latest, legacy csc, mcs 2.6.4/5.23). The mcs 2.6.4 gap was in fact caught by the Windows CI matrix and is covered by the dup-alias handling and its fixtures.
This PR was prepared with the help of an AI agent (Claude Code), directed and reviewed by the author.
🤖 Generated with Claude Code