Skip to content

Allow let! and use! binding with type annotation without parentheses. #18508

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

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c4b40cb
new parser production rule.
edgarfgp Apr 25, 2025
0b489a1
Add let! and use! syntax tree tests
edgarfgp Apr 26, 2025
be13fe0
Update existing syntax tree tests
edgarfgp Apr 26, 2025
4e00653
try update mkSynMemberDefnGetSet
edgarfgp Apr 26, 2025
b223cd4
format code
edgarfgp Apr 26, 2025
7c592a8
try different parser rule
edgarfgp Apr 26, 2025
e3f7b8c
Add new test
edgarfgp Apr 26, 2025
43e9ea2
Revert "new parser production rule."
edgarfgp Apr 26, 2025
b26be8b
Revert "try update mkSynMemberDefnGetSet"
edgarfgp Apr 26, 2025
cbe3d48
Revert "Update existing syntax tree tests"
edgarfgp Apr 26, 2025
acb8742
update ce let! and use! test
edgarfgp Apr 26, 2025
00e687c
Merge branch 'main' into fix-10697
edgarfgp Apr 28, 2025
3034ffb
more tests
edgarfgp Apr 28, 2025
99cc8d4
reduce diff
edgarfgp Apr 28, 2025
dd416dc
release notes
edgarfgp Apr 28, 2025
5eb3102
Merge branch 'main' into fix-10697
edgarfgp Apr 30, 2025
e54cb12
more tests
edgarfgp Apr 30, 2025
9e65095
Merge branch 'main' into fix-10697
edgarfgp Apr 30, 2025
f7837a4
Merge branch 'main' into fix-10697
edgarfgp May 1, 2025
e608ce6
try headBindingPattern
edgarfgp May 1, 2025
50114d1
Update tests
edgarfgp May 1, 2025
b16bbb5
more tests
edgarfgp May 1, 2025
84ea9ba
opt_topReturnTypeWithTypeConstraints
edgarfgp May 2, 2025
2b09352
Merge branch 'main' into fix-10697
edgarfgp May 2, 2025
420a92d
Merge branch 'main' into fix-10697
edgarfgp May 5, 2025
afcfc17
Merge branch 'main' into fix-10697
edgarfgp May 6, 2025
4b22d90
Merge branch 'main' into fix-10697
edgarfgp May 7, 2025
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 @@ -27,6 +27,7 @@
* Fix missing `null` highlighting in tooltips ([PR #18457](https://github.com/dotnet/fsharp/pull/18457))
* Fix range of SynPat.Named doesn't include accessibility ([PR #18526](https://github.com/dotnet/fsharp/pull/18526))
* Allow `_` in `use!` bindings values (lift FS1228 restriction) ([PR #18487](https://github.com/dotnet/fsharp/pull/18487))
* Allow `let!` and `use!` binding with type annotation without parentheses. ([PR #18508](https://github.com/dotnet/fsharp/pull/18508))
* Make `[<CallerMemberName; Struct>]` combination work([PR #18444](https://github.com/dotnet/fsharp/pull/18444/))
* Fix code completion considers types from own namespace non-imported ([PR #18518](https://github.com/dotnet/fsharp/issues/18518))
* Code completion: fix getting qualifier expression in do statements in type decls ([PR #18524](https://github.com/dotnet/fsharp/pull/18524))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1802,6 +1802,7 @@ let rec TryTranslateComputationExpression
match pat with
| SynPat.Named(ident = SynIdent(id, _); isThisVal = false) -> id, pat
| SynPat.LongIdent(longDotId = SynLongIdent(id = [ id ])) -> id, pat
| SynPat.Typed(pat = pat) -> extractIdentifierFromPattern 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 "_"
Expand Down
31 changes: 31 additions & 0 deletions src/Compiler/pars.fsy
Original file line number Diff line number Diff line change
Expand Up @@ -4466,6 +4466,37 @@ declExpr:
let trivia: SynExprLetOrUseBangTrivia = { LetOrUseBangKeyword = rhs parseState 1 ; EqualsRange = Some mEquals }
SynExpr.LetOrUseBang(spBind, ($1 = "use"), true, $2, $4, $7, $8, m, trivia) }

| BINDER headBindingPattern opt_topReturnTypeWithTypeConstraints EQUALS typedSequentialExprBlock IN opt_OBLOCKSEP moreBinders typedSequentialExprBlock %prec expr_let
{ // Handle type annotations on patterns in let!/use! bindings
// Examples: let! x: int = async { return 1 }
// use! _: IDisposable = async { return new MemoryStream() }
let spBind = DebugPointAtBinding.Yes(rhs2 parseState 1 7)
let pat =
match $3 with
| None -> $2
| Some (_, SynReturnInfo((ty, _), _)) ->
SynPat.Typed($2, ty, unionRanges $2.Range ty.Range)
let mEquals = rhs parseState 4
let m = unionRanges (rhs parseState 1) $9.Range
let trivia: SynExprLetOrUseBangTrivia = { LetOrUseBangKeyword = rhs parseState 1 ; EqualsRange = Some mEquals }
SynExpr.LetOrUseBang(spBind, ($1 = "use"), true, pat, $5, $8, $9, m, trivia) }

| OBINDER headBindingPattern opt_topReturnTypeWithTypeConstraints EQUALS typedSequentialExprBlock hardwhiteDefnBindingsTerminator opt_OBLOCKSEP moreBinders typedSequentialExprBlock %prec expr_let
{ // Handle type annotations on patterns in let!/use! bindings (offside-sensitive version)
// This rule maintains consistent handling of binding constructs across different syntactic contexts
let report, mIn, _ = $6
report (if $1 = "use" then "use!" else "let!") (rhs parseState 1) // report unterminated error
let spBind = DebugPointAtBinding.Yes(unionRanges (rhs parseState 1) $5.Range)
let pat =
match $3 with
| None -> $2
| Some (_, SynReturnInfo((ty, _), _)) ->
SynPat.Typed($2, ty, unionRanges $2.Range ty.Range)
let mEquals = rhs parseState 4
let m = unionRanges (rhs parseState 1) $9.Range
let trivia: SynExprLetOrUseBangTrivia = { LetOrUseBangKeyword = rhs parseState 1 ; EqualsRange = Some mEquals }
SynExpr.LetOrUseBang(spBind, ($1 = "use"), true, pat, $5, $8, $9, m, trivia) }

| OBINDER headBindingPattern EQUALS typedSequentialExprBlock hardwhiteDefnBindingsTerminator opt_OBLOCKSEP error %prec expr_let
{ // error recovery that allows intellisense when writing incomplete computation expressions
let spBind = DebugPointAtBinding.Yes(unionRanges (rhs parseState 1) $4.Range)
Expand Down
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:IDisposable = new Disposable(1)
use! __:IDisposable = new Disposable(2)
use! (res1: IDisposable) = new Disposable(3)
use! _: IDisposable = new Disposable(4)
use! (_: IDisposable) = 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()
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ module UseBangBindingsVersion9 =
|> withDiagnostics [
(Error 1228, Line 47, Col 14, Line 47, Col 15, "'use!' bindings must be of the form 'use! <var> = <expr>'")
]

[<Theory; Directory(__SOURCE_DIRECTORY__, Includes=[|"UseBang05.fs"|])>]
let ``UseBangBindings - UseBang05_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! <var> = <expr>'")
]

module UseBangBindingsPreview =
[<Theory; Directory(__SOURCE_DIRECTORY__, Includes=[|"UseBang01.fs"|])>]
Expand Down Expand Up @@ -83,4 +94,13 @@ module UseBangBindingsPreview =
|> withLangVersionPreview
|> compileAndRun
|> shouldSucceed

[<Theory; Directory(__SOURCE_DIRECTORY__, Includes=[|"UseBang05.fs"|])>]
let ``UseBangBindings - UseBang05_fs - Preview LangVersion`` compilation =
compilation
|> asExe
|> withLangVersionPreview
|> compileAndRun
|> shouldSucceed


Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// #DeclarationElements #LetBindings
//<Expects status="success"></Expects>
open System

let answer =
use x:IDisposable = new System.IO.MemoryStream()
42
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
open System

type private Disposable() =
[<DefaultValue>] static val mutable private disposedTimes: int
[<DefaultValue>] 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 x: IDisposable = 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
open System

type private Disposable() =
[<DefaultValue>] static val mutable private disposedTimes: int
[<DefaultValue>] 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 _:IDisposable = 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
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,25 @@ module UseBindings =
|> asExe
|> withLangVersion60
|> compileAndRun
|> shouldSucceed
|> shouldSucceed

[<Theory; Directory(__SOURCE_DIRECTORY__, Includes=[|"UseBindingDiscard03.fs"|])>]
let ``UseBindings - UseBindingDiscard03_fs - Current LangVersion`` compilation =
compilation
|> asExe
|> compileAndRun
|> shouldSucceed

[<Theory; Directory(__SOURCE_DIRECTORY__, Includes=[|"UseBinding01.fs"|])>]
let ``UseBindings - UseBinding01_fs - Current LangVersion`` compilation =
compilation
|> asFsx
|> compile
|> shouldSucceed

[<Theory; Directory(__SOURCE_DIRECTORY__, Includes=[|"UseBinding02.fs"|])>]
let ``UseBindings - UseBinding02_fs - Current LangVersion`` compilation =
compilation
|> asFsx
|> compile
|> shouldSucceed
6 changes: 6 additions & 0 deletions tests/service/data/SyntaxTree/SynType/Typed LetBang 01.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Module

async {
let! res: int = async { return 1 }
return res
}
36 changes: 36 additions & 0 deletions tests/service/data/SyntaxTree/SynType/Typed LetBang 01.fs.bsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
ImplFile
(ParsedImplFileInput
("/root/SynType/Typed LetBang 01.fs", false, QualifiedNameOfFile Module, [],
[],
[SynModuleOrNamespace
([Module], false, NamedModule,
[Expr
(App
(NonAtomic, false, Ident async,
ComputationExpr
(false,
LetOrUseBang
(Yes (4,4--4,38), false, true,
Typed
(Named (SynIdent (res, None), false, None, (4,9--4,12)),
LongIdent (SynLongIdent ([int], [], [None])),
(4,9--4,17)),
App
(NonAtomic, false, Ident async,
ComputationExpr
(false,
YieldOrReturn
((false, true), Const (Int32 1, (4,35--4,36)),
(4,28--4,36),
{ YieldOrReturnKeyword = (4,28--4,34) }),
(4,26--4,38)), (4,20--4,38)), [],
YieldOrReturn
((false, true), Ident res, (5,4--5,14),
{ YieldOrReturnKeyword = (5,4--5,10) }), (4,4--5,14),
{ LetOrUseBangKeyword = (4,4--4,8)
EqualsRange = Some (4,18--4,19) }), (3,6--6,1)),
(3,0--6,1)), (3,0--6,1))],
PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector), [], None,
(1,0--6,1), { LeadingKeyword = Module (1,0--1,6) })], (true, true),
{ ConditionalDirectives = []
CodeComments = [] }, set []))
6 changes: 6 additions & 0 deletions tests/service/data/SyntaxTree/SynType/Typed LetBang 02.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Module

async {
let! (res: int) = async { return 1 }
return res
}
38 changes: 38 additions & 0 deletions tests/service/data/SyntaxTree/SynType/Typed LetBang 02.fs.bsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
ImplFile
(ParsedImplFileInput
("/root/SynType/Typed LetBang 02.fs", false, QualifiedNameOfFile Module, [],
[],
[SynModuleOrNamespace
([Module], false, NamedModule,
[Expr
(App
(NonAtomic, false, Ident async,
ComputationExpr
(false,
LetOrUseBang
(Yes (4,4--4,40), false, true,
Paren
(Typed
(Named
(SynIdent (res, None), false, None, (4,10--4,13)),
LongIdent (SynLongIdent ([int], [], [None])),
(4,10--4,18)), (4,9--4,19)),
App
(NonAtomic, false, Ident async,
ComputationExpr
(false,
YieldOrReturn
((false, true), Const (Int32 1, (4,37--4,38)),
(4,30--4,38),
{ YieldOrReturnKeyword = (4,30--4,36) }),
(4,28--4,40)), (4,22--4,40)), [],
YieldOrReturn
((false, true), Ident res, (5,4--5,14),
{ YieldOrReturnKeyword = (5,4--5,10) }), (4,4--5,14),
{ LetOrUseBangKeyword = (4,4--4,8)
EqualsRange = Some (4,20--4,21) }), (3,6--6,1)),
(3,0--6,1)), (3,0--6,1))],
PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector), [], None,
(1,0--6,1), { LeadingKeyword = Module (1,0--1,6) })], (true, true),
{ ConditionalDirectives = []
CodeComments = [] }, set []))
7 changes: 7 additions & 0 deletions tests/service/data/SyntaxTree/SynType/Typed LetBang 03.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module Module

async {
let (a: int, b: int) = 1, 3
let! (c: int, d: int) = async { return 1, 3 }
return a + b + c + d
}
Loading