Skip to content

Unify let, let!, use, use!LetOrUse AST representation. - #18825

Merged
T-Gro merged 51 commits into
dotnet:mainfrom
edgarfgp:unify-let-or-use-2
Aug 11, 2025
Merged

Unify let, let!, use, use!LetOrUse AST representation.#18825
T-Gro merged 51 commits into
dotnet:mainfrom
edgarfgp:unify-let-or-use-2

Conversation

@edgarfgp

@edgarfgpedgarfgp commented Aug 6, 2025

Copy link
Copy Markdown
Contributor

Description

This PR unifies the AST representation for binding expressions by consolidating SynExpr.LetOrUse and SynExpr.LetOrUseBang into a single SynExpr.LetOrUse node. This change simplifies the AST structure and makes it more consistent for tools working with syntax trees.

Key Changes

Removed:

  • SynExpr.LetOrUseBang case has been completely removed

Extended:

  • SynExpr.LetOrUse now includes two additional boolean flags:
    • isFromSource: Indicates whether the binding originates from user-written code (true) vs compiler-generated (false)
    • isComputed: Distinguishes computation expression bindings (let!/use! = true) from regular bindings (let/use = false)

Before and After AST Structure

Before (Two Separate Cases)

// Regular let/use bindingstypeSynExpr=| LetOrUse of
isRecursive: bool *
isUse: bool *
bindings: SynBinding list *
body: SynExpr *
range: range *
trivia: SynExprLetOrUseTrivia
// Computation expression bindings (let!/use!)| LetOrUseBang of
bindDebugPoint: DebugPointAtBinding *
isUse: bool *
isFromSource: bool *
pat: SynPat *// Pattern as direct field
rhs: SynExpr *// RHS expression as direct field
andBangs: SynBinding list *// Additional and! bindings
body: SynExpr *
range: range *
trivia: SynExprLetOrUseTrivia

After (Unified Single Case)

typeSynExpr=| LetOrUse of
isRecursive: bool *
isUse: bool *
isFromSource: bool *// NEW: From LetOrUseBang
isComputed: bool *// NEW: Indicates let!/use! (true) vs let/use (false)
bindings: SynBinding list *// Now contains ALL bindings (including first let!/use!)
body: SynExpr *
range: range *
trivia: SynExprLetOrUseTrivia

Example 1: Computation Expression with let! and and!

async{let!bar= getBar()and! baz = getBaz()return bar + baz
}

Before AST:

LetOrUseBang
(Yes (3,4--3,24),false,true,
Named (SynIdent (bar, None),false, None,(3,9--3,12)),// pat: "bar" as direct field
App // rhs: "getBar()" as direct field(NonAtomic,false, Ident getBar,
Const (Unit,(3,22--3,24)),(3,15--3,24)),[SynBinding // andBangs: separate list for "and!"(None, Normal,false,false,[], PreXmlDocEmpty,
SynValData(...),
Named (SynIdent (baz, None),false, None,(4,9--4,12)),
None,
App (NonAtomic,false, Ident getBaz,
Const (Unit,(4,22--4,24)),(4,15--4,24)),...)],
body,// "return bar + baz"...)

After AST:

LetOrUse
(false,false,true,true,// isRec=false, isUse=false, isFromSource=true, isComputed=true[SynBinding // ALL bindings in single list(None, Normal,false,false,[], PreXmlDocEmpty,
SynValData(...),
Named // pat: "bar" now inside SynBinding(SynIdent (bar, None),false, None,(3,9--3,12)),
None,
App // rhs: "getBar()" now inside SynBinding(NonAtomic,false, Ident getBar,
Const (Unit,(3,22--3,24)),(3,15--3,24)),(3,4--6,14), Yes (3,4--3,24),{ LeadingKeyword = Let (3,4--3,8)
InlineKeyword = None
EqualsRange = Some (3,13--3,14)});
SynBinding // "and! baz = getBaz()" in same list(None, Normal,false,false,[], PreXmlDocEmpty,
SynValData(...),
Named (SynIdent (baz, None),false, None,(4,9--4,12)),
None,
App (NonAtomic,false, Ident getBaz,
Const (Unit,(4,22--4,24)),(4,15--4,24)),...)],
body,// "return bar + baz"...)

Example 2: Regular let Binding

doletx=1
printfn "%d" x

Before AST:

LetOrUse
(false,false,// isRecursive=false, isUse=false[SynBinding
(None, Normal,false,false,[],
PreXmlDoc ((3,4), FSharp.Compiler.Xml.XmlDocCollector),
SynValData(...),
Named (SynIdent (x, None),false, None,(3,8--3,9)),// pat: "x"
None,
Const (Int32 1,(3,12--3,13)),// expr: "1"(3,8--3,13), Yes (3,4--3,13),{ LeadingKeyword = Let (3,4--3,7)
InlineKeyword = None
EqualsRange = Some (3,10--3,11)})],
body,// "printfn "%d" x"...)

After AST:

LetOrUse
(false,false,true,false,// isRec=false, isUse=false, isFromSource=true, isComputed=false[SynBinding // Same binding structure, just with new flags(None, Normal,false,false,[],
PreXmlDoc ((3,4), FSharp.Compiler.Xml.XmlDocCollector),
SynValData(...),
Named (SynIdent (x, None),false, None,(3,8--3,9)),// pat: "x"
None,
Const (Int32 1,(3,12--3,13)),// expr: "1"(3,8--3,13), Yes (3,4--3,13),{ LeadingKeyword = Let (3,4--3,7)
InlineKeyword = None
EqualsRange = Some (3,10--3,11)})],
body,// "printfn "%d" x"...)

Example 3: use! in Computation Expression

async{use! resource = getResource()return resource.Value
}

Before AST:

LetOrUseBang
(Yes (2,4--2,30),true,true,// isUse=true for use!
Named (SynIdent (resource, None),false, None,(2,9--2,17)),// Direct pat field
App (NonAtomic,false, Ident getResource,// Direct rhs field
Const (Unit,(2,28--2,30)),(2,17--2,30)),[],// No and! bindings
body,// "return resource.Value"...)

After AST:

LetOrUse
(false,true,true,true,// isRec=false, isUse=true, isFromSource=true, isComputed=true[SynBinding // Pattern and RHS now wrapped in SynBinding(None, Normal,false,false,[], PreXmlDocEmpty,
SynValData(...),
Named (SynIdent (resource, None),false, None,(2,9--2,17)),
None,
App (NonAtomic,false, Ident getResource,
Const (Unit,(2,28--2,30)),(2,17--2,30)),(2,4--2,30), Yes (2,4--2,30),{ LeadingKeyword = Use (2,4--2,7)
InlineKeyword = None
EqualsRange = Some (2,18--2,19)})],
body,// "return resource.Value"...)

Complete AST Flags Mapping

Source CodeAST Node(isRec, isUse, isFromSource, isComputed)Notes
let x = 42LetOrUse(false, false, true, false)Regular let binding
let rec f x = ...LetOrUse(true, false, true, false)Recursive let binding
use file = ...LetOrUse(false, true, true, false)Regular use binding
let! x = asyncOp()LetOrUse(false, false, true, true)Computation expression let!
use! r = getResource()LetOrUse(false, true, true, true)Computation expression use!
let! a = opA() and! b = opB()LetOrUse(false, false, true, true)Multiple bindings in list
Compiler-generated letLetOrUse(false, false, false, false)isFromSource=false
Compiler-generated let!LetOrUse(false, false, false, true)During CE transformation

Understanding the Boolean Flags

isFromSource

  • true: User-written code that appears in the source file
  • false: Compiler-generated during transformations (pattern match compilation, CE desugaring, etc.)
  • Important for: error reporting, debugging, IDE features, code formatting

isComputed

  • true: Computation expression binding (let! or use!)
  • false: Regular binding (let or use)
  • Determines how the binding is processed during compilation

Migration Guidance for Ecosystem AST Users

1. Pattern Matching Updates

Before:

match expr with| SynExpr.LetOrUse(isRec, isUse, bindings, body, range, trivia)->// Handle regular let/use| SynExpr.LetOrUseBang(spBind, isUse, isFromSource, pat, rhs, andBangs, body, range, trivia)->// Handle let!/use!

After:

match expr with| SynExpr.LetOrUse(isRec, isUse, isFromSource, isComputed, bindings, body, range, trivia)->if isComputed then// This is a let!/use! expressionmatch bindings with| firstBinding :: andBangs ->match firstBinding with| SynBinding(headPat = pat; expr = rhs)->// pat and rhs extracted from first binding// andBangs contains the and! bindings|[]->// error caseelse// This is a regular let/use expression

2. Construction Updates

Before:

// Creating a let! expression
SynExpr.LetOrUseBang(
bindDebugPoint,false,// isUsetrue,// isFromSource
pat,
rhsExpr,
andBangs,
bodyExpr,
range,
trivia
)

After:

// Creating a let! expressionletfirstBinding= SynBinding(
accessibility = None,
kind = SynBindingKind.Normal,
isInline =false,
isMutable =false,
attributes =[],
xmlDoc = PreXmlDoc.Empty,
valData = SynInfo.emptySynValData,
headPat = pat,// Pattern moved here
returnInfo = None,
expr = rhsExpr,// RHS moved here
range = range,
debugPoint = bindDebugPoint,// Debug point moved here
trivia = bindingTrivia
)
SynExpr.LetOrUse(false,// isRecursivefalse,// isUsetrue,// isFromSourcetrue,// isComputed (indicates let!)
firstBinding :: andBangs,// All bindings in single list
bodyExpr,
range,
trivia
)

3. Common Migration Patterns

Checking for computation expressions:

// Beforematch expr with| SynExpr.LetOrUseBang _->true|_->false// Aftermatch expr with| SynExpr.LetOrUse(isComputed =true)->true|_->false

Extracting pattern and expression from let!:

// Before| SynExpr.LetOrUseBang(_,_,_, pat, rhs,_,_,_,_)->
processBinding pat rhs
// After| SynExpr.LetOrUse(isComputed =true; bindings = binding ::_)->match binding with| SynBinding(headPat = pat; expr = rhs)->
processBinding pat rhs
|_->// error

Processing and! bindings:

// Before| SynExpr.LetOrUseBang(_,_,_, firstPat, firstRhs, andBangs,_,_,_)->
processFirst firstPat firstRhs
for andBang in andBangs do
processAndBang andBang
// After| SynExpr.LetOrUse(isComputed =true; bindings = bindings)->match bindings with| first :: rest ->
processBinding first
for andBang in rest do
processAndBang andBang
|[]->// error

Breaking Changes

This is a breaking change for any code that pattern matches on or constructs SynExpr values. All tools and libraries that work with the AST will need to be updated, including:

  • Code formatters (Fantomas)
  • Analyzers
  • Refactoring tools
  • IDE features
  • Custom compiler extensions

Checklist

  • Test cases added
  • Release notes entry updated

edgarfgpand others added 30 commits April 23, 2025 15:27
Ensure that regular let/use bindings (isComputed=false) are matched before let!/use! bindings (isComputed=true) to prevent computation expressions from being treated as regular bindings.
Comment threadsrc/Compiler/SyntaxTree/SyntaxTree.fsi Outdated
Comment threadsrc/Compiler/SyntaxTree/SyntaxTree.fsi
Comment threadsrc/Compiler/SyntaxTree/SyntaxTree.fsi

@T-GroT-Gro left a comment

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.

Since this changes the public API, I think this deserves some form of impact analysis for this change, and some guidance around the change.

https://github.com/search?q=LetOrUseBang+language%3AF%23&type=code&l=F%23

@github-project-automationgithub-project-automationBot moved this from New to In Progress in F# Compiler and ToolingAug 8, 2025
Comment threadsrc/Compiler/SyntaxTree/ParseHelpers.fs
@edgarfgpedgarfgp reopened this Aug 10, 2025

@T-GroT-Gro left a comment

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.

Please consider writing a footnote on migration towards AST users.
Can be a footnote inside the release-notes file, pointed to from the main release notes entry.

@T-Gro
T-Gro enabled auto-merge (squash) August 11, 2025 09:40
# Conflicts:
#	tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl
#	tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl
auto-merge was automatically disabled August 11, 2025 10:11

Head branch was pushed to by a user without write access

@edgarfgp

Copy link
Copy Markdown
ContributorAuthor

Please consider writing a footnote on migration towards AST users. Can be a footnote inside the release-notes file, pointed to from the main release notes entry.

Done :)

@T-Gro
T-Gro merged commit d82b8df into dotnet:mainAug 11, 2025
36 checks passed
Comment threadsrc/Compiler/pars.fsy
let isUse = ($1 = "use")

mkLetExpression(true, mKeyword, None, m, $8, None, Some(pat, $4, $7, mEquals, isUse)) }
mkLetExpression(true, None, m, $8, None, Some(pat, $4, $7, mKeyword, mEquals, isUse)) }

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.

@edgarfgp It seems the separate rules should not be needed anymore for the computed variants of the bindings too. Could you try unifying them with the the normal ones?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure thing!!!

@nojaf

Copy link
Copy Markdown
Contributor

I know I'm too late to all of this, but is would have been so useful if SynLeadingKeyword just had the right information in regards to let! and and!.

Now I need to check two things to ensure let! and and! in

opt {let!abc= def ()and! foo =()()}

are indeed let! and not LeadingKeyword = Let (4,4--4,8).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

5 participants

@edgarfgp@nojaf@auduchinok@T-Gro