Skip to content

Fix #1603: recognize foreach over enumerators that cannot be disposable - #3916

Merged
siegfriedpammer merged 1 commit into
masterfrom
fix-1603-foreach-without-dispose
Jul 27, 2026
Merged

Fix #1603: recognize foreach over enumerators that cannot be disposable#3916
siegfriedpammer merged 1 commit into
masterfrom
fix-1603-foreach-without-dispose

Conversation

@siegfriedpammer

@siegfriedpammer siegfriedpammer commented Jul 27, 2026

Copy link
Copy Markdown
Member

Fixes #1603.

The compiler emits no using/try-finally at all when the enumerator's static type can never require disposal: a struct or a sealed class that does not implement IDisposable (SerializationInfoEnumerator in the issue's example). Since the decompiler's foreach pattern was only matched against using instructions, such loops stayed while (enumerator.MoveNext()) loops.

This PR recognizes the bare enumerator = x.GetEnumerator(); while (enumerator.MoveNext()) { ... } shape during statement building and reuses the existing foreach transformation core for it (TransformToForeach is split into the UsingInstruction wrapper and a core taking the containers/enumerator variable explicitly).

The transformation is restricted to exactly the cases where recompilation produces the same IL:

  • the enumerator type is a non-ref struct or a sealed class, and does not implement IDisposable (a non-sealed class would be enumerated with a finally { (enumerator as IDisposable)?.Dispose(); } block; ref structs are excluded because of pattern-based disposal);
  • the enumerator variable has a single store and is not used outside the loop;
  • synchronous enumeration only, since async enumerators always implement IAsyncDisposable.

Tests: the two commented-out TODO cases in Pretty/Loops.cs (ForEachOnCustomStructEnumerator and its generic variant) are revived, plus a new sealed-class enumerator case and Issue1603 using the actual SerializationInfo/SerializationEntry types from the issue. All were verified to decompile to while loops before the fix. Full decompiler suite on Linux: 2544 total, 0 failed, 38 skipped. Note the legacy-csc and mcs configurations of the Loops test do not run on Linux, so the Windows CI matrix is the first place they get exercised.

🤖 This PR was prepared by an AI agent (Claude Code) operated by @siegfriedpammer.

The foreach pattern was only matched against using instructions, but
the compiler emits no using/try-finally at all when the enumerator's
static type can never require disposal: a struct or a sealed class
that does not implement IDisposable (SerializationInfoEnumerator in
the issue's example). Such loops stayed while loops.

Recognize the bare 'enumerator = x.GetEnumerator(); while
(enumerator.MoveNext())' shape during statement building and reuse the
existing foreach transformation core for it. The transformation is
restricted to exactly the cases where recompilation would produce the
same IL: the enumerator type rules above (ref structs are excluded
because of pattern-based disposal), a single-store enumerator variable
unused outside the loop, and synchronous enumeration only, since async
enumerators are always IAsyncDisposable.

Assisted-by: Claude:claude-fable-5:Claude Code
@siegfriedpammer
siegfriedpammer merged commit 9bff4ad into master Jul 27, 2026
13 checks passed
@siegfriedpammer
siegfriedpammer deleted the fix-1603-foreach-without-dispose branch July 27, 2026 05:38
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.

foreach expected but while used instead

1 participant