diff --git a/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md index 16f5721e22a..7224d401d6e 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md @@ -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 `[]` combination work([PR #18444](https://github.com/dotnet/fsharp/pull/18444/)) ### Added diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md index 905e086a163..d1576a84eca 100644 --- a/docs/release-notes/.Language/preview.md +++ b/docs/release-notes/.Language/preview.md @@ -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 diff --git a/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs b/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs index 011b6f0a5ac..f861665a5fc 100644 --- a/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs @@ -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 @@ -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( @@ -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 @@ -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 diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index 9bfa12ce963..aab4de2a554 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -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" diff --git a/src/Compiler/Facilities/LanguageFeatures.fs b/src/Compiler/Facilities/LanguageFeatures.fs index 7a9a14b8602..874b709d658 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fs +++ b/src/Compiler/Facilities/LanguageFeatures.fs @@ -99,6 +99,7 @@ type LanguageFeature = | DeprecatePlacesWhereSeqCanBeOmitted | SupportValueOptionsAsOptionalParameters | WarnWhenUnitPassedToObjArg + | UseBangBindingValueDiscard /// LanguageVersion management type LanguageVersion(versionText) = @@ -229,6 +230,7 @@ type LanguageVersion(versionText) = LanguageFeature.DeprecatePlacesWhereSeqCanBeOmitted, previewVersion LanguageFeature.SupportValueOptionsAsOptionalParameters, previewVersion LanguageFeature.WarnWhenUnitPassedToObjArg, previewVersion + LanguageFeature.UseBangBindingValueDiscard, previewVersion ] static let defaultLanguageVersion = LanguageVersion("default") @@ -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 = diff --git a/src/Compiler/Facilities/LanguageFeatures.fsi b/src/Compiler/Facilities/LanguageFeatures.fsi index 410a8b193c9..f50357f3fe0 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fsi +++ b/src/Compiler/Facilities/LanguageFeatures.fsi @@ -90,6 +90,7 @@ type LanguageFeature = | DeprecatePlacesWhereSeqCanBeOmitted | SupportValueOptionsAsOptionalParameters | WarnWhenUnitPassedToObjArg + | UseBangBindingValueDiscard /// LanguageVersion management type LanguageVersion = diff --git a/src/Compiler/pars.fsy b/src/Compiler/pars.fsy index 7cac0ad0dc0..e6e3a657592 100644 --- a/src/Compiler/pars.fsy +++ b/src/Compiler/pars.fsy @@ -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 @@ -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 } diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 7d64601c303..adb2b01d334 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -657,6 +657,11 @@ Interoperabilita mezi neřízeným obecným omezením jazyka C# a F# (emitovat další modreq) + + Allows use! _ = ... in computation expressions + Allows use! _ = ... in computation expressions + + Use type conversion cache during compilation Use type conversion cache during compilation diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index 67f4270377e..7deaf880012 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -657,6 +657,11 @@ Interop zwischen nicht verwalteter generischer Einschränkung in C# und F# (zusätzlicher ModReq ausgeben) + + Allows use! _ = ... in computation expressions + Allows use! _ = ... in computation expressions + + Use type conversion cache during compilation Use type conversion cache during compilation diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index 9478e3d2c55..e8090e5579e 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -657,6 +657,11 @@ Interoperabilidad entre la restricción genérica no administrada de C# y F# (emitir modreq adicional) + + Allows use! _ = ... in computation expressions + Allows use! _ = ... in computation expressions + + Use type conversion cache during compilation Use type conversion cache during compilation diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 57fade5d250..25faaecb3a4 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -657,6 +657,11 @@ Interopérabilité entre les contraintes génériques non gérées de C# et F# (émettre un modreq supplémentaire) + + Allows use! _ = ... in computation expressions + Allows use! _ = ... in computation expressions + + Use type conversion cache during compilation Use type conversion cache during compilation diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index 14d670e8455..30d68a3330b 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -657,6 +657,11 @@ Interoperabilità tra il vincolo generico non gestito di C# e di F# (crea un modreq aggiuntivo) + + Allows use! _ = ... in computation expressions + Allows use! _ = ... in computation expressions + + Use type conversion cache during compilation Use type conversion cache during compilation diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index 78acb0fd944..9f2709fa559 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -657,6 +657,11 @@ C# と F# のアンマネージド ジェネリック制約の間の相互運用 (追加の modreq を出力) + + Allows use! _ = ... in computation expressions + Allows use! _ = ... in computation expressions + + Use type conversion cache during compilation Use type conversion cache during compilation diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index 604ac431e4a..2c80d361ef6 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -657,6 +657,11 @@ C#과 F#의 관리되지 않는 제네릭 제약 조건 간의 Interop(추가 modreq 내보내기) + + Allows use! _ = ... in computation expressions + Allows use! _ = ... in computation expressions + + Use type conversion cache during compilation Use type conversion cache during compilation diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index fdee3d82f7d..0bd80d75a72 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -657,6 +657,11 @@ Międzyoperacyjnie między niezarządzanym ograniczeniem ogólnym języka C# i F# (emituj dodatkowe modreq) + + Allows use! _ = ... in computation expressions + Allows use! _ = ... in computation expressions + + Use type conversion cache during compilation Use type conversion cache during compilation diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 2f86c57d960..c0e79885fed 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -657,6 +657,11 @@ Interoperabilidade entre a restrição genérica não gerenciada de C# e F# (emitir modreq adicional) + + Allows use! _ = ... in computation expressions + Allows use! _ = ... in computation expressions + + Use type conversion cache during compilation Use type conversion cache during compilation diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index fefd5255a0b..3a4610e93df 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -657,6 +657,11 @@ Взаимодействие между универсальным ограничением "unmanaged" C# и F#(создание дополнительного modreq) + + Allows use! _ = ... in computation expressions + Allows use! _ = ... in computation expressions + + Use type conversion cache during compilation Use type conversion cache during compilation diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index 5325f0ab09f..6d1bee2476b 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -657,6 +657,11 @@ C# ile F#' arasında yönetilmeyen genel kısıtlama (ek modreq yayın) + + Allows use! _ = ... in computation expressions + Allows use! _ = ... in computation expressions + + Use type conversion cache during compilation Use type conversion cache during compilation diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index 2e8b957d810..a60e3188af7 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -657,6 +657,11 @@ C# 和 F# 的非托管泛型约束之间的互操作(发出额外的 modreq) + + Allows use! _ = ... in computation expressions + Allows use! _ = ... in computation expressions + + Use type conversion cache during compilation Use type conversion cache during compilation diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index f0924b3d30f..3253c4e45f7 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -657,6 +657,11 @@ C# 與 F# 的非受控泛型條件約束之間的 Interop (發出額外的 modreq) + + Allows use! _ = ... in computation expressions + Allows use! _ = ... in computation expressions + + Use type conversion cache during compilation Use type conversion cache during compilation diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBang01.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBang01.fs new file mode 100644 index 00000000000..4b6335c3789 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBang01.fs @@ -0,0 +1,58 @@ +open System + +open System + +type Disposable(id: int) = + static let mutable disposedIds = Set.empty + static let mutable constructedIds = Set.empty + + 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() \ No newline at end of file diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBang02.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBang02.fs new file mode 100644 index 00000000000..d57f0b8c14d --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBang02.fs @@ -0,0 +1,57 @@ +open System + +open System + +type Disposable(id: int) = + static let mutable disposedIds = Set.empty + static let mutable constructedIds = Set.empty + + 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! _ = new Disposable(1) + use! _ = new Disposable(2) + use! (_) = new Disposable(3) + use! (_) = new Disposable(4) + 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() \ No newline at end of file diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBang03.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBang03.fs new file mode 100644 index 00000000000..57c814c561a --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBang03.fs @@ -0,0 +1,71 @@ +open System + +type Disposable(id: int) = + static let mutable disposedIds = Map.empty + static let mutable constructedIds = Set.empty + + do constructedIds <- constructedIds.Add(id) + + member _.Id = id + + static member GetDisposed() = disposedIds + static member GetConstructed() = constructedIds + static member Reset() = + disposedIds <- Map.empty + constructedIds <- Set.empty + + interface IDisposable with + member this.Dispose() = + let currentCount = + match Map.tryFind this.Id disposedIds with + | Some count -> count + | None -> 0 + disposedIds <- Map.add this.Id (currentCount + 1) disposedIds + +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 disposedSet = Set.ofSeq (Map.keys disposed) + let undisposed = constructed - disposedSet + + if not undisposed.IsEmpty then + printfn $"Undisposed instances: %A{undisposed}" + failwithf "Not all disposables were properly disposed" + + let multipleDisposed = + disposed + |> Map.filter (fun _ count -> count > 1) + + if not multipleDisposed.IsEmpty then + printfn $"Objects disposed multiple times: %A{multipleDisposed}" + failwithf "Some disposables were disposed multiple times" + + printfn $"Success! All %d{constructed.Count} disposables were properly disposed exactly once" + +testBindingPatterns() \ No newline at end of file diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBang04.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBang04.fs new file mode 100644 index 00000000000..3c32e29bc46 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBang04.fs @@ -0,0 +1,91 @@ +open System + +type Disposable(id: int) = + static let mutable disposedIds = Map.empty + static let mutable constructedIds = Set.empty + + do constructedIds <- constructedIds.Add(id) + + member _.Id = id + + static member GetDisposed() = disposedIds + static member GetConstructed() = constructedIds + static member Reset() = + disposedIds <- Map.empty + constructedIds <- Set.empty + + interface IDisposable with + member this.Dispose() = + let currentCount = + match Map.tryFind this.Id disposedIds with + | Some count -> count + | None -> 0 + disposedIds <- Map.add this.Id (currentCount + 1) disposedIds + +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) + + use! x = new Disposable(6) + use! x = new Disposable(7) + + use! (_) = new Disposable(8) + use! (_) = new Disposable(9) + + return () + } |> Async.RunSynchronously + + let constructed = Disposable.GetConstructed() + let disposed = Disposable.GetDisposed() + + let disposedSet = Set.ofSeq (Map.keys disposed) + let undisposed = constructed - disposedSet + + if not undisposed.IsEmpty then + printfn $"Undisposed instances: %A{undisposed}" + failwithf "Not all disposables were properly disposed" + + // Verify each object was disposed exactly once + let incorrectlyDisposed = + disposed + |> Map.partition (fun _ count -> count = 1) + |> snd + + if not incorrectlyDisposed.IsEmpty then + printfn $"Objects with incorrect disposal count: %A{incorrectlyDisposed}" + failwithf "Some disposables were not disposed exactly once" + + let idChecks = + [1..9] |> List.map (fun id -> + match Map.tryFind id disposed with + | Some 1 -> true + | _ -> false + ) + + if not ((List.forall id) idChecks) then + failwithf "Not all disposable IDs were properly handled" + + printfn $"Success! All %d{constructed.Count} disposables were properly disposed exactly once, including repeated patterns" + +testBindingPatterns() \ No newline at end of file diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBangBindings.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBangBindings.fs new file mode 100644 index 00000000000..ba0f31bd885 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBangBindings.fs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Language + +open FSharp.Test +open Xunit +open FSharp.Test.Compiler + +module UseBangBindingsVersion9 = + [] + let ``UseBangBindings - UseBang01_fs - Current LangVersion`` compilation = + compilation + |> asFsx + |> withLangVersion90 + |> typecheck + |> shouldFail + |> withDiagnostics [ + (Error 1228, Line 43, Col 14, Line 43, Col 15, "'use!' bindings must be of the form 'use! = '") + ] + + [] + let ``UseBangBindings - UseBang02_fs - Current LangVersion`` compilation = + compilation + |> asFsx + |> withLangVersion90 + |> typecheck + |> shouldFail + |> withDiagnostics [ + (Error 1228, Line 40, Col 14, Line 40, Col 15, "'use!' bindings must be of the form 'use! = '") + ] + + [] + let ``UseBangBindings - UseBang03_fs - Current LangVersion`` compilation = + compilation + |> asFsx + |> withLangVersion90 + |> typecheck + |> shouldFail + |> withDiagnostics [ + (Error 1228, Line 46, Col 14, Line 46, Col 15, "'use!' bindings must be of the form 'use! = '") + ] + + [] + let ``UseBangBindings - UseBang04_fs - Current LangVersion`` compilation = + compilation + |> asFsx + |> withLangVersion90 + |> typecheck + |> shouldFail + |> withDiagnostics [ + (Error 1228, Line 47, Col 14, Line 47, Col 15, "'use!' bindings must be of the form 'use! = '") + ] + +module UseBangBindingsPreview = + [] + let ``UseBangBindings - UseBang01_fs - Preview LangVersion`` compilation = + compilation + |> asExe + |> withLangVersionPreview + |> compileAndRun + |> shouldSucceed + + [] + let ``UseBangBindings - UseBang02_fs - Preview LangVersion`` compilation = + compilation + |> asExe + |> withLangVersionPreview + |> compileAndRun + |> shouldSucceed + + [] + let ``UseBangBindings - UseBang03_fs - Preview LangVersion`` compilation = + compilation + |> asExe + |> withLangVersionPreview + |> compileAndRun + |> shouldSucceed + + [] + let ``UseBangBindings - UseBang04_fs - Preview LangVersion`` compilation = + compilation + |> asExe + |> withLangVersionPreview + |> compileAndRun + |> shouldSucceed + diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBindingDiscard02.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBindingDiscard02.fs new file mode 100644 index 00000000000..9a31ba2763b --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBindingDiscard02.fs @@ -0,0 +1,26 @@ +open System + +type private Disposable() = + [] static val mutable private disposedTimes: int + [] static val mutable private constructedTimes: int + + do Disposable.constructedTimes <- Disposable.constructedTimes + 1 + + static member DisposeCallCount() = Disposable.disposedTimes + static member ConstructorCallCount() = Disposable.constructedTimes + + interface System.IDisposable with + member _.Dispose() = + Disposable.disposedTimes <- Disposable.disposedTimes + 1 + +let _scope = + use _ = new Disposable() + () + +let disposeCalls = Disposable.DisposeCallCount() +if disposeCalls <> 1 then + failwith "was not disposed or disposed too many times" + +let ctorCalls = Disposable.ConstructorCallCount() +if ctorCalls <> 1 then + failwithf "unexpected constructor call count: %i" ctorCalls diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBindings.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBindings.fs index 8196a9fe286..74137286a8a 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBindings.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/UseBindings/UseBindings.fs @@ -27,35 +27,9 @@ module UseBindings = |> withErrorCode 3350 |> withDiagnosticMessageMatches "Feature 'discard pattern in use binding' is not available.*" - [] - let ``Dispose called for discarded value of use binding`` () = - Fsx """ -type private Disposable() = - [] static val mutable private disposedTimes: int - [] static val mutable private constructedTimes: int - - do Disposable.constructedTimes <- Disposable.constructedTimes + 1 - - static member DisposeCallCount() = Disposable.disposedTimes - static member ConstructorCallCount() = Disposable.constructedTimes - - interface System.IDisposable with - member _.Dispose() = - Disposable.disposedTimes <- Disposable.disposedTimes + 1 - -let _scope = - use _ = new Disposable() - () - -let disposeCalls = Disposable.DisposeCallCount() -if disposeCalls <> 1 then - failwith "was not disposed or disposed too many times" - -let ctorCalls = Disposable.ConstructorCallCount() -if ctorCalls <> 1 then - failwithf "unexpected constructor call count: %i" ctorCalls - - """ + [] + let ``Dispose called for discarded value of use binding`` compilation = + compilation |> asExe |> withLangVersion60 |> compileAndRun diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 49b459e2796..98eb8a9336c 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -75,6 +75,7 @@ +