Skip to content

Add with keyword to Syntax tree #12400

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 15 commits into from
Nov 19, 2021
Merged

Add with keyword to Syntax tree #12400

merged 15 commits into from
Nov 19, 2021

Conversation

nojaf
Copy link
Contributor

@nojaf nojaf commented Nov 16, 2021

This PR adds the with keyword range to various nodes in the Syntax tree.
//cc @dsyme

@auduchinok
Copy link
Member

I wonder if we could move ranges with keywords and punctuation like this into separate aggregating fields?
It'd simplify using AST (since there would be less fields to reason about) and for most FCS users it'll be easier to fix their code after new data is added to that additional field. We previosly didn't keep that much token-related data, but now it's becoming a bit too many fields in many common AST node types.

@nojaf
Copy link
Contributor Author

nojaf commented Nov 16, 2021

separate aggregating fields

How would that look like? If it is something entirely outside of the tree then that already has little value as I would need to piece these things together again somehow. Unless you mean something else.

FCS users it'll be easier to fix their code

You don't need any changes if you use named pattern matching.
Example:

match expr with
| SynExpr.TryWith (tryExpr=innerComp; withCases=clauses; range=mTryToLast; tryDebugPoint=spTry) -> ...

I'm updating all the code in the compiler like this as I go and for one commit I changed the tree and parser and no other changes were necessary.

@dsyme
Copy link
Contributor

dsyme commented Nov 16, 2021

Great work @nojaf

How would that look like? If it is something entirely outside of the tree ..

I think @auduchinok means a systematic typed "trivia" field, as a version resilient class, in each node. We could in principle though not as part of this PR of course :)

@auduchinok
Copy link
Member

auduchinok commented Nov 16, 2021

I think @auduchinok means a systematic typed "trivia" field, as a version resilient class, in each node. We could in principle though not as part of this PR of course :)

Yes, just to move it out of the main node types, so there're less fields to pattern match and update on FCS changes.

@auduchinok
Copy link
Member

auduchinok commented Nov 16, 2021

You don't need any changes if you use named pattern matching.

I think this shouldn't be a requirement for FCS clients.

I'm updating all the code in the compiler like this as I go and for one commit I changed the tree and parser and no other changes were necessary.

It'd likely be better if it was possible without that many changes. 🙂

@nojaf
Copy link
Contributor Author

nojaf commented Nov 16, 2021

Ok, thanks for elaborating. For better or worse, I'm getting a lot of ideas actually by this 🙈.
Interesting train of thought.

@nojaf
Copy link
Contributor Author

nojaf commented Nov 18, 2021

@dsyme this is ready for review.

Copy link
Contributor

@dsyme dsyme left a comment

Choose a reason for hiding this comment

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

This is really great! Just a few nits, nothing much

Thanks

Renamed Range to range.
Add comment above PropertyKeyword.
@nojaf
Copy link
Contributor Author

nojaf commented Nov 18, 2021

This is really great! Just a few nits, nothing much

Thanks, never ever doubt about raising nitpicks. All good, always, any time ☺!

@@ -3499,7 +3520,7 @@ declExpr:
{ let mMatch = rhs parseState 1
let mWith, (clauses, mLast) = $3
let spBind = DebugPointAtBinding.Yes(unionRanges mMatch mWith)
SynExpr.Match (spBind, $2, clauses, unionRanges mMatch mLast) }
SynExpr.Match (mMatch, spBind, $2, mWith, clauses, unionRanges mMatch mLast) }
Copy link
Member

@auduchinok auduchinok Nov 18, 2021

Choose a reason for hiding this comment

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

@nojaf What problem does a separate match range field solve here? The keyword starts at the same range as the whole match expression and is a single token of a known width. Given each added range is rather a big struct, it seems wasteful to add ranges that can be calculated from the nodes.

Maybe we could add some helpers that would calculate keyword ranges from their positions instead of storing them in trees? We could use computed properties and only store positions in Trivia/Appendix types for keywords where storing position is required.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Feels like we are talking in circles here:
image

I already expressed why ranges are preferred over calculations with positions.
@dsyme what do you think?

Copy link
Member

@auduchinok auduchinok Nov 19, 2021

Choose a reason for hiding this comment

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

This time I'm suggesting this calculation to be the details of implementation in FCS, while keeping range properties that Fantomas could work with. 🙂

Calculating a range from position doesn't look too bad perf-wise and ranges are structs, so creating them shouldn't add GC pressure. On the other hand, the two keywords in match expr use 8 ints to store data that could use 2 ints. And if we're going to add this for all keywords, it becomes a bit too much.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, I'm slowly getting on board with this.
Although, is there a way to get a position inside the parser?
Places where I now use rhs parseState 1, is there something similar that just gets the start position of 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@auduchinok would that then look something like:

#r "nuget: FSharp.Compiler.Service"

open FSharp.Compiler.Text
open FSharp.Compiler.Text.Range
open FSharp.Compiler.Text.Position

type NodeTrivia =
    { StartPos: pos
      FileName: string }

    member this.Range =
        mkRange this.FileName this.StartPos (mkPos this.StartPos.Line (this.StartPos.Column + 4))

let myTrivia =
    { StartPos = mkPos 1 0
      FileName = "MyFile" }

myTrivia.Range

?

Copy link
Member

@auduchinok auduchinok Nov 19, 2021

Choose a reason for hiding this comment

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

@nojaf Yes, the idea for getting a range is roughly like this. I'm also trying to find a better way to keep the keyword position only and to get the file name from the overall node range. Something like this could work:

let getKeywordRange (containingRange: range) startPos length =
    mkRange containingRange.FileName startPos (mkPos startPos.Line (startPos.Column + length))

[<Struct>]
type SynExprMatchTrivia =
    { WithPos: pos }

    member this.GetMatchKeywordRange(exprRange: range) =
        getKeywordRange exprRange exprRange.Start 5

    member this.GetWithKeywordRange(exprRange: range) =
        getKeywordRange exprRange this.WithPos 4

Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer a range than a position. The position on its own is of no use, we would always reconstruct the range. These particular nodes are of no intrinsic significance when it comes to memory performance

@nojaf nojaf closed this Nov 19, 2021
@nojaf nojaf reopened this Nov 19, 2021
@nojaf nojaf requested a review from dsyme November 19, 2021 10:35

// 'match! expr with pats ...' --> build.Bind(e1, (function pats ...))
| SynExpr.MatchBang (spMatch, expr, clauses, m) ->
| SynExpr.MatchBang (mMatch, spMatch, expr, _withKeyword, clauses, _m) ->
Copy link
Contributor

Choose a reason for hiding this comment

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

_mWith to keep it consistent

let propertyNameBindingBuilder, _ = $2
let optPropertyType = $3
let isMutable = false
(fun visNoLongerUsed memFlagsBuilder attrs rangeStart ->
let mutable hasGet = false
let mutable hasSet = false
let xmlDoc = grabXmlDocAtRangeStart(parseState, attrs, rangeStart)

let tryMkSynMemberDefnMember
Copy link
Contributor

Choose a reason for hiding this comment

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

We should lift all this code out into SyntaxTreeOps.fs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is quite tricky to just port.
reportParseErrorAt and raiseParseErrorAt are being used inside it.
Could we do this another time perhaps?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes sure, I meant to say that

@dsyme
Copy link
Contributor

dsyme commented Nov 19, 2021

Nice work, just a couple of very minor nits

@dsyme dsyme merged commit 76e65ac into dotnet:main Nov 19, 2021
@nojaf nojaf deleted the with-keyword branch November 19, 2021 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants