Fix double Dispose in use bindings with as patterns (#12300)#19858
Fix double Dispose in use bindings with as patterns (#12300)#19858T-Gro wants to merge 5 commits into
Conversation
❗ Release notes required
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Emit exactly one Dispose call per `use` binding, regardless of how many names the pattern introduces, by cleaning up only `patternInputTmp`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…2300) Track stamps of use-bound locals on TcEnv and skip cleanup emission in TcLetBinding when the binding's right-hand side is a reference to one of them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
T-Gro
left a comment
There was a problem hiding this comment.
Review: Fix double Dispose in use bindings with as patterns (#12300)
Verdict: Approve ✅
Summary
This PR correctly fixes issue #12300 where use bindings with as patterns (e.g., use a as b = d) or rebinding an existing use-bound value (use b = a) caused double Dispose() calls. The fix is minimal, well-scoped, and handles both scenarios cleanly.
Analysis
Scenario A (as-pattern): The original code called Dispose on every val introduced by the pattern via List.foldBack. Since use a as b = d introduces both a and b pointing to the same object, this meant double disposal. The fix correctly narrows to a single Dispose on patternInputTmp — the canonical value holding the bound expression.
Scenario B (rebinding): use b = a where a is already use-bound. The fix introduces eUseBoundValStamps to track use-bound vals and checks if the RHS is a direct reference to one. If so, the inner use skips its cleanup, deferring to the outer use. This is correct — the env (not envInner) is used for the alias check, so it only sees vals from enclosing scopes, while the current binding's vals are added to envInner for downstream visibility.
Edge cases handled correctly:
use _ = exprstill disposes (wildcard usespatternInputTmp)use b = f(a)whereais use-bound — NOT suppressed (only direct val refs are detected)- Nested scopes: stamps are part of
TcEnvwhich scopes correctly let b = a; use c = b— NOT suppressed (intermediateletdoesn't add to stamps)
Code Quality
- Good use of
Set<Stamp>(immutable, no concurrency concerns) - Performance impact is negligible — the set grows only with
usebindings in scope - The local
stripTyLambdasis appropriate since no equivalent utility exists at this layer (the one inEraseClosures.fshas a different signature/purpose) - Comments clearly explain the rationale and reference the issue
Tests
Comprehensive coverage of:
use a as b = d(single dispose)use b = awhereais use-bound (single dispose)- Normal
use(still works) - Discarded
use _ = d(still disposes) - Independent
usebindings (each disposes) - Triple alias
use a as b as c = d(single dispose)
Minor Observations (non-blocking)
-
The
.fsdoc comment says/// See .fsi.— this is fine as a cross-reference pattern but slightly unusual in this codebase where most fields have inline docs in both files. -
Consider whether the
stripTyLambdas+Expr.Valcheck should also handleExpr.App(Expr.Val(...), ...)patterns for upcasts (e.g.,use b = (a :> IDisposable)). Currently this would NOT suppress the duplicate dispose. This may be an acceptable limitation for a follow-up if it's ever reported.
Overall, this is a clean, correct fix with excellent test coverage. Nice work! 🎉
| // enclosing `use` will dispose. | ||
| let rec stripTyLambdas e = | ||
| match e with | ||
| | Expr.TyLambda(_, _, body, _, _) -> stripTyLambdas body |
There was a problem hiding this comment.
Should perhaps add a case like
| Expr.Op(TOp.Coerce, _, [inner], _) -> stripCoerceAndTyLambdas inner
otherwise something like (idk how common this is)
use b = (a :> System.IDisposable)
will still "double Dispose"
There was a problem hiding this comment.
If this is too contrived, feel free to skip this comment
Summary
Fixes #12300 —
use a as b = d(anduse b = awhereais alreadyuse-bound) previously generated multipleDisposecalls for the same underlying object.Root Cause
The compiler emitted one
Disposeper name introduced by ausepattern. Withaspatterns (use a as b = d) or rebinding (use b = a), each alias triggered its own cleanup, causing the sameIDisposableto be disposed multiple times.Fix
Two scenarios are addressed in
CheckExpressions.fs:Scenario A —
use a as b = d: Emit exactly oneDisposecall on the canonicalpatternInputTmpvalue (the initial tmp that holds the bound expression) regardless of how many names theaspattern introduces.Scenario B —
use b = awhereais use-bound: Detect when the RHS of ausebinding is a simple reference to an already use-bound value and skip the redundant cleanup. The outerusealready owns the Dispose.The detection for Scenario B uses a new
eUseBoundValStampsset on the type-checking environment to track which values are knownuse-bound.Tests
Added
UseBindingDisposalTests.fswith 6 tests covering:use a as b = ddisposes onceuse b = a(rebind of use-bound) disposes onceusestill disposesuse _ = dstill disposesusebindings each disposeuse a as b as c = ddisposes once