Skip to content

Fix #3858: Detect automatic events on the ILAst instead of the C# AST - #3886

Merged
siegfriedpammer merged 5 commits into
masterfrom
fix-3858-automatic-events
Jul 18, 2026
Merged

Fix #3858: Detect automatic events on the ILAst instead of the C# AST#3886
siegfriedpammer merged 5 commits into
masterfrom
fix-3858-automatic-events

Conversation

@siegfriedpammer

@siegfriedpammer siegfriedpammer commented Jul 18, 2026

Copy link
Copy Markdown
Member

Automatic-event recognition moves off the syntactic accessor-body patterns in PatternStatementTransform onto structural matching of the accessor ILAst, decompiled with a fixed set of settings - the same technique RecordDecompiler uses 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:

  1. Extract DecompileBodyForAnalysis from RecordDecompiler - pure refactor hoisting the fixed-settings analysis pipeline so other recognizers can share it.
  2. Fix AutomaticEvents doesn't work when AggressiveInlining enabled #3858: Detect automatic events on the ILAst instead of the C# AST - the new AutoEventDecompiler matches 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 evaluates this once via IL dup), and the mcs 5.x loop. On a positive verdict, DoDecompile(IEvent) sets UseCustomEvents = false so TypeSystemAstBuilder emits the field-like declaration directly; surviving accessor and backing-field attributes are added from the type system as method:/field: sections, dropping the ones the compiler puts there.
  3. Emit the backing field when a name-associated event is not automatic - MemberIsHidden decides 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).
  4. Replace automatic-event backing-field references during translation - the reference rewrite moves from the identifier pass in PatternStatementTransform into ExpressionBuilder.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 spurious this./type qualifiers from raise sites.
  5. Remove the syntactic automatic-event patterns - the four accessor patterns, their checks, and the AST-collapse helper they used are deleted (about 300 lines). Bodyless events (abstract, extern, interface members) previously depended on the patterns' no-body clause; since C# cannot express bodyless custom accessors, DoDecompile now chooses the field-like form for them directly.

Behavior notes:

  • A compiler shape the matchers do not know now degrades to explicit accessors with the backing field kept in the output - compilable, instead of the previous wrong-code failure mode.
  • Raise sites lose redundant qualifiers (StaticEvent() instead of StaticAutomaticEvents.StaticEvent(), A instead of this.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

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
siegfriedpammer force-pushed the fix-3858-automatic-events branch 3 times, most recently from f029d90 to ca5fb4c Compare July 18, 2026 17:22
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
siegfriedpammer force-pushed the fix-3858-automatic-events branch from ca5fb4c to 538f7ae Compare July 18, 2026 17:32
@siegfriedpammer
siegfriedpammer merged commit 7554c1b into master Jul 18, 2026
13 checks passed
@siegfriedpammer
siegfriedpammer deleted the fix-3858-automatic-events branch July 18, 2026 19:56
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.

AutomaticEvents doesn't work when AggressiveInlining enabled

1 participant