Skip to content

Fix double Dispose in use bindings with as patterns (#12300)#19858

Open
T-Gro wants to merge 5 commits into
mainfrom
fix/issue-12300
Open

Fix double Dispose in use bindings with as patterns (#12300)#19858
T-Gro wants to merge 5 commits into
mainfrom
fix/issue-12300

Conversation

@T-Gro

@T-Gro T-Gro commented May 29, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #12300use a as b = d (and use b = a where a is already use-bound) previously generated multiple Dispose calls for the same underlying object.

Root Cause

The compiler emitted one Dispose per name introduced by a use pattern. With as patterns (use a as b = d) or rebinding (use b = a), each alias triggered its own cleanup, causing the same IDisposable to be disposed multiple times.

Fix

Two scenarios are addressed in CheckExpressions.fs:

Scenario Ause a as b = d: Emit exactly one Dispose call on the canonical patternInputTmp value (the initial tmp that holds the bound expression) regardless of how many names the as pattern introduces.

Scenario Buse b = a where a is use-bound: Detect when the RHS of a use binding is a simple reference to an already use-bound value and skip the redundant cleanup. The outer use already owns the Dispose.

The detection for Scenario B uses a new eUseBoundValStamps set on the type-checking environment to track which values are known use-bound.

Tests

Added UseBindingDisposalTests.fs with 6 tests covering:

  • use a as b = d disposes once
  • use b = a (rebind of use-bound) disposes once
  • Normal use still disposes
  • Discarded use _ = d still disposes
  • Two independent use bindings each dispose
  • Triple alias use a as b as c = d disposes once

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required


✅ Found changes and release notes in following paths:

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/11.0.100.md

@github-actions github-actions Bot added the AI-Tooling-Check-Bypassed Tooling check: non-fork PR, not diff-analyzed label May 29, 2026
Copilot and others added 5 commits June 2, 2026 12:06
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 T-Gro force-pushed the fix/issue-12300 branch from b45d4c8 to a808498 Compare June 2, 2026 10:27
@T-Gro T-Gro requested a review from abonie June 3, 2026 10:14

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 _ = expr still disposes (wildcard uses patternInputTmp)
  • use b = f(a) where a is use-bound — NOT suppressed (only direct val refs are detected)
  • Nested scopes: stamps are part of TcEnv which scopes correctly
  • let b = a; use c = b — NOT suppressed (intermediate let doesn't add to stamps)

Code Quality

  • Good use of Set<Stamp> (immutable, no concurrency concerns)
  • Performance impact is negligible — the set grows only with use bindings in scope
  • The local stripTyLambdas is appropriate since no equivalent utility exists at this layer (the one in EraseClosures.fs has 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 = a where a is use-bound (single dispose)
  • Normal use (still works)
  • Discarded use _ = d (still disposes)
  • Independent use bindings (each disposes)
  • Triple alias use a as b as c = d (single dispose)

Minor Observations (non-blocking)

  1. The .fs doc 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.

  2. Consider whether the stripTyLambdas + Expr.Val check should also handle Expr.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! 🎉

@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Jun 3, 2026
@T-Gro T-Gro enabled auto-merge (squash) June 4, 2026 12:22
// enclosing `use` will dispose.
let rec stripTyLambdas e =
match e with
| Expr.TyLambda(_, _, body, _, _) -> stripTyLambdas body

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is too contrived, feel free to skip this comment

@github-project-automation github-project-automation Bot moved this from New to In Progress in F# Compiler and Tooling Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-reviewed PR reviewed by AI review council AI-Tooling-Check-Bypassed Tooling check: non-fork PR, not diff-analyzed

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

Values inside 'as' patterns are disposed multiple times

2 participants