Skip to content

Allow _ in use! bindings values #18487

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/9.0.300.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
* Fixed [#18433](https://github.com/dotnet/fsharp/issues/18433), a rare case of an internal error in xml comment processing. ([PR #18436](https://github.com/dotnet/fsharp/pull/18436))
* Fix confusing type inference error in task expression ([Issue #13789](https://github.com/dotnet/fsharp/issues/13789), [PR #18450](https://github.com/dotnet/fsharp/pull/18450))
* Fix missing `null` highlighting in tooltips ([PR #18457](https://github.com/dotnet/fsharp/pull/18457))
* Allow `_` in `use!` bindings values (lift FS1228 restriction) ([PR #18487](https://github.com/dotnet/fsharp/pull/18487))
* Make `[<CallerMemberName; Struct>]` combination work([PR #18444](https://github.com/dotnet/fsharp/pull/18444/))

### Added
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes/.Language/preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Deprecate places where `seq` can be omitted. ([Language suggestion #1033](https://github.com/fsharp/fslang-suggestions/issues/1033), [PR #17772](https://github.com/dotnet/fsharp/pull/17772))
* Added type conversions cache, only enabled for compiler runs ([PR#17668](https://github.com/dotnet/fsharp/pull/17668))
* Support ValueOption + Struct attribute as optional parameter for methods ([Language suggestion #1136](https://github.com/fsharp/fslang-suggestions/issues/1136), [PR #18098](https://github.com/dotnet/fsharp/pull/18098))
* Allow `_` in `use!` bindings values (lift FS1228 restriction) ([PR #18487](https://github.com/dotnet/fsharp/pull/18487))
* Warn when `unit` is passed to an `obj`-typed argument ([PR #18330](https://github.com/dotnet/fsharp/pull/18330))

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/// with generalization at appropriate points.
module internal FSharp.Compiler.CheckComputationExpressions

open FSharp.Compiler.TcGlobals
open Internal.Utilities.Library
open FSharp.Compiler.AccessibilityLogic
open FSharp.Compiler.AttributeChecking
Expand Down Expand Up @@ -1783,12 +1784,33 @@ let rec TryTranslateComputationExpression
if ceenv.isQuery then
error (Error(FSComp.SR.tcBindMayNotBeUsedInQueries (), mBind))

match pat, andBangs with
| (SynPat.Named(ident = SynIdent(id, _); isThisVal = false) | SynPat.LongIdent(longDotId = SynLongIdent(id = [ id ]))), [] ->
match andBangs with
| [] ->
// Valid pattern case - handle with Using + Bind
requireBuilderMethod "Using" mBind cenv ceenv.env ceenv.ad ceenv.builderTy mBind
requireBuilderMethod "Bind" mBind cenv ceenv.env ceenv.ad ceenv.builderTy mBind

let supportsUseBangBindingValueDiscard =
ceenv.cenv.g.langVersion.SupportsFeature LanguageFeature.UseBangBindingValueDiscard

// use! x = ...
// use! (x) = ...
// use! (__) = ...
// use! _ = ...
// use! (_) = ...
let rec extractIdentifierFromPattern pat =
match pat with
| SynPat.Named(ident = SynIdent(id, _); isThisVal = false) -> id, pat
| SynPat.LongIdent(longDotId = SynLongIdent(id = [ id ])) -> id, pat
| SynPat.Wild(m) when supportsUseBangBindingValueDiscard ->
// To properly call the Using(disposable) CE member, we need to convert the wildcard to a SynPat.Named
let tmpIdent = mkSynId m "_"
tmpIdent, SynPat.Named(SynIdent(tmpIdent, None), false, None, m)
| SynPat.Paren(pat = pat) -> extractIdentifierFromPattern pat
| _ -> error (Error(FSComp.SR.tcInvalidUseBangBinding (), pat.Range))

let ident, pat = extractIdentifierFromPattern pat

let bindExpr =
let consumeExpr =
SynExpr.MatchLambda(
Expand All @@ -1809,14 +1831,14 @@ let rec TryTranslateComputationExpression
)

let consumeExpr =
mkSynCall "Using" mBind [ SynExpr.Ident id; consumeExpr ] ceenv.builderValName
mkSynCall "Using" mBind [ SynExpr.Ident ident; consumeExpr ] ceenv.builderValName

let consumeExpr =
SynExpr.MatchLambda(
false,
mBind,
[
SynMatchClause(pat, None, consumeExpr, id.idRange, DebugPointAtTarget.No, SynMatchClauseTrivia.Zero)
SynMatchClause(pat, None, consumeExpr, ident.idRange, DebugPointAtTarget.No, SynMatchClauseTrivia.Zero)
],
DebugPointAtBinding.NoneAtInvisible,
mBind
Expand All @@ -1829,8 +1851,7 @@ let rec TryTranslateComputationExpression
|> addBindDebugPoint spBind

Some(translatedCtxt bindExpr)
| _pat, [] -> error (Error(FSComp.SR.tcInvalidUseBangBinding (), mBind))
| _pat, _ands ->
| _ ->
// Has andBangs
let m =
match andBangs with
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/FSComp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1798,3 +1798,4 @@ featureDontWarnOnUppercaseIdentifiersInBindingPatterns,"Don't warn on uppercase
featureDeprecatePlacesWhereSeqCanBeOmitted,"Deprecate places where 'seq' can be omitted"
featureSupportValueOptionsAsOptionalParameters,"Support ValueOption as valid type for optional member parameters"
featureSupportWarnWhenUnitPassedToObjArg,"Warn when unit is passed to a member accepting `obj` argument, e.g. `Method(o:obj)` will warn if called via `Method()`."
featureUseBangBindingValueDiscard,"Allows use! _ = ... in computation expressions"
3 changes: 3 additions & 0 deletions src/Compiler/Facilities/LanguageFeatures.fs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ type LanguageFeature =
| DeprecatePlacesWhereSeqCanBeOmitted
| SupportValueOptionsAsOptionalParameters
| WarnWhenUnitPassedToObjArg
| UseBangBindingValueDiscard

/// LanguageVersion management
type LanguageVersion(versionText) =
Expand Down Expand Up @@ -229,6 +230,7 @@ type LanguageVersion(versionText) =
LanguageFeature.DeprecatePlacesWhereSeqCanBeOmitted, previewVersion
LanguageFeature.SupportValueOptionsAsOptionalParameters, previewVersion
LanguageFeature.WarnWhenUnitPassedToObjArg, previewVersion
LanguageFeature.UseBangBindingValueDiscard, previewVersion
]

static let defaultLanguageVersion = LanguageVersion("default")
Expand Down Expand Up @@ -391,6 +393,7 @@ type LanguageVersion(versionText) =
| LanguageFeature.DeprecatePlacesWhereSeqCanBeOmitted -> FSComp.SR.featureDeprecatePlacesWhereSeqCanBeOmitted ()
| LanguageFeature.SupportValueOptionsAsOptionalParameters -> FSComp.SR.featureSupportValueOptionsAsOptionalParameters ()
| LanguageFeature.WarnWhenUnitPassedToObjArg -> FSComp.SR.featureSupportWarnWhenUnitPassedToObjArg ()
| LanguageFeature.UseBangBindingValueDiscard -> FSComp.SR.featureUseBangBindingValueDiscard ()

/// Get a version string associated with the given feature.
static member GetFeatureVersionString feature =
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Facilities/LanguageFeatures.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type LanguageFeature =
| DeprecatePlacesWhereSeqCanBeOmitted
| SupportValueOptionsAsOptionalParameters
| WarnWhenUnitPassedToObjArg
| UseBangBindingValueDiscard

/// LanguageVersion management
type LanguageVersion =
Expand Down
22 changes: 19 additions & 3 deletions src/Compiler/pars.fsy
Original file line number Diff line number Diff line change
Expand Up @@ -3755,13 +3755,23 @@ atomicPattern:
{ SynPat.ArrayOrList(true, $2, lhs parseState) }

| UNDERSCORE
{ SynPat.Wild(lhs parseState) }
{ (* Underscore pattern ('_') is represented as SynPat.Wild
This wild pattern is used in all binding forms:
- let _ = ...
- use _ = ...
- let! _ = ...
- use! _ = ...
This ensures consistent representation of wildcard bindings in the AST *)
SynPat.Wild(lhs parseState) }

| QMARK ident
{ SynPat.OptionalVal($2, lhs parseState) }

| atomicPatternLongIdent %prec prec_atompat_pathop
{ let vis, lidwd = $1
{ (* This rule handles identifiers in patterns like 'use! __' *)
(* For simple identifiers (like '__'), it creates a SynPat.Named AST node *)
(* For complex paths (A.B.C) or uppercase ids, it calls mkSynPatMaybeVar *)
let vis, lidwd = $1
if not (isNilOrSingleton lidwd.LongIdent) || String.isLeadingIdentifierCharacterUpperCase (List.head lidwd.LongIdent).idText then
mkSynPatMaybeVar lidwd vis (lhs parseState)
else
Expand Down Expand Up @@ -4431,7 +4441,13 @@ declExpr:
SynExpr.YieldOrReturnFrom(($1, not $1), arbExpr ("yield!", mYieldAll), mYieldAll, trivia) }

| BINDER headBindingPattern EQUALS typedSequentialExprBlock IN opt_OBLOCKSEP moreBinders typedSequentialExprBlock %prec expr_let
{ let spBind = DebugPointAtBinding.Yes(rhs2 parseState 1 5)
{ (* This rule handles the 'use!' and 'let!' binding expressions in computation expressions *)
(* The BINDER token represents keywords like 'use!' or 'let!' *)
(* headBindingPattern represents patterns in the binding:
- Underscore ('_') patterns are preserved as SynPat.Wild
- Named patterns ('__') are represented as SynPat.Named
- Identifiers (like 'value') are represented as SynPat.LongIdent *)
let spBind = DebugPointAtBinding.Yes(rhs2 parseState 1 5)
let mEquals = rhs parseState 3
let m = unionRanges (rhs parseState 1) $8.Range
let trivia: SynExprLetOrUseBangTrivia = { LetOrUseBangKeyword = rhs parseState 1 ; EqualsRange = Some mEquals }
Expand Down
5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.pl.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.pt-BR.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.ru.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.tr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.zh-Hans.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Compiler/xlf/FSComp.txt.zh-Hant.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
open System

open System

type Disposable(id: int) =
static let mutable disposedIds = Set.empty<int>
static let mutable constructedIds = Set.empty<int>

do constructedIds <- constructedIds.Add(id)

member _.Id = id

static member GetDisposed() = disposedIds
static member GetConstructed() = constructedIds
static member Reset() =
disposedIds <- Set.empty
constructedIds <- Set.empty

interface IDisposable with
member this.Dispose() = disposedIds <- disposedIds.Add(this.Id)

type DisposableBuilder() =
member _.Using(resource: #IDisposable, f) =
async {
use res = resource
return! f res
}

member _.Bind(disposable: Disposable, f) = async.Bind(async.Return(disposable), f)
member _.Return(x) = async.Return x
member _.ReturnFrom(x) = x
member _.Bind(task, f) = async.Bind(task, f)

let counterDisposable = DisposableBuilder()

let testBindingPatterns() =
Disposable.Reset()

counterDisposable {
use! res = new Disposable(1)
use! __ = new Disposable(2)
use! (res1) = new Disposable(3)
use! _ = new Disposable(4)
use! (_) = new Disposable(5)
return ()
} |> Async.RunSynchronously

let constructed = Disposable.GetConstructed()
let disposed = Disposable.GetDisposed()
let undisposed = constructed - disposed

if not undisposed.IsEmpty then
printfn $"Undisposed instances: %A{undisposed}"
failwithf "Not all disposables were properly disposed"
else
printfn $"Success! All %d{constructed.Count} disposables were properly disposed"

testBindingPatterns()
Loading