-
Notifications
You must be signed in to change notification settings - Fork 795
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
Add higher-order-function-based API for working with untyped AST #16462
Conversation
This looks really promising! What comes to mind here is that the current Visitor doesn't traverse expressions by default and I would assume the new functions don't have this limitation. It would also be great if you could update https://fsharp.github.io/fsharp-compiler-docs/fcs/syntax-visitor.html. Feel free to swap out or remove any of the existing text there. We should educate FCS consumers about these new functions. Great work @brianrourkeboll! |
The idea for these new functions is that every node be traversed in canonical order unless or until otherwise specified. That seems like a good default for, e.g., analyzers that will end up wanting to look at a good portion of the AST anyway via The existing default behavior of If that is truly a very common or important use-case, we could consider exposing some way of specifying desired traversal behavior in these new functions to avoid being excessively wasteful in such scenarios—perhaps a flags enum or something? [<Flags>]
type SyntaxNodes =
| None = 0
| SynModuleOrNamespace = 1
| SynModule = 2
| SynBinding = 4
| SynExpr = 8
| …
| All = … val fold:
folder: ('State -> SyntaxVisitorPath -> SyntaxNode -> 'State) ->
nodesOfInterest: SyntaxNodes ->
state: 'State ->
parsedInput: ParsedInput ->
'State Used like: (
SyntaxNodes.SynModuleOrNamespace ||| SyntaxNodes.SynModule ||| SyntaxNodes.SynBinding,
[],
parsedInput
)
|||> ParsedInput.fold (fun acc path node -> …) or (
SyntaxNodes.All ^^^ SyntaxNodes.SynExpr,
[],
parsedInput
)
|||> ParsedInput.fold (fun acc path node -> …) or (
SyntaxNodes.All ^^^ SyntaxNodes.SynExpr,
parsedInput
)
||> ParsedInput.tryPickDownTo pos (fun path node -> …)
Yes: if others think that this new API looks useful enough to move forward with, I'd probably try to migrate existing usages of |
❗ Release notes required
|
* Put `Ast.foldWhile` back in. * Add `Ast.exists`. * Add `Ast.tryNode`.
* Replace uses of `SyntaxTraversal.Traverse` in `FSharpParseFileResults` with the appropriate function from the `Ast` module: `exists`, `tryPick`, `tryNode`.
* Before, no signature help was offered in a case like this: ```fsharp [1..10] |> List.fold (fun acc _ -> acc) ‸ |> List.filter (fun x -> x > 3) ``` The service will now offer help for the `state` parameter when the cursor ‸ is in that location.
* `FSharpParseFileResults.TryRangeOfFunctionOrMethodBeingApplied` was previously returning the range of the (zero-width) `SynExpr.ArbitraryAfterError`. It now returns the range of the `*` (`op_Multiply`) instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm, thanks, we can
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This review is a testament to the exceptional work done on this project. It's clear that a lot of thought and effort has been put into it, and the result is impressive.
I have some suggestions that might enhance the public API of this PR, although these are minor compared to the overall quality of the work. It seems more intuitive to continue using ParsedInput
as the standard input across all new functions, given its familiarity. While the type Ast = SyntaxNode list
serves its purpose internally, perhaps it's best not to expose it publicly.
Regarding the naming of the abbreviation, I personally feel that it might be more beneficial to consider a different naming convention or perhaps remove it altogether.
In the ParsedInputExtensions
module, making the Contents
member private could be a good move, as its utility seems limited outside the scope of the new code:
module ParsedInputExtensions =
type ParsedInput with
/// <summary>
/// Gets the parsed file's contents as forest of <see cref="T:FSharp.Compiler.Syntax.SyntaxNode"/>s.
/// </summary>
member Contents: Ast
The introduction of module Ast =
is interesting, but I wonder if it's necessary. It might be more cohesive to integrate these functions into the existing module public SyntaxTraversal
, especially if ast: Ast
is changed to parsedInput: ParsedInput
.
Concerning the function signatures, aligning them with the functions-first rule, as commonly seen in List
, Seq
, Array
, etc., would maintain consistency. For example:
val exists: position: pos -> predicate: (SyntaxVisitorPath -> SyntaxNode -> bool) -> ast: Ast -> bool
// could be revised to
val exists: predicate: (SyntaxVisitorPath -> SyntaxNode -> bool) -> position: pos -> parsedInput: ParsedInput -> bool
Finally, it may be worthwhile to revisit the F# Compiler Service's Syntax Visitor documentation. While a complete overhaul might be too extensive at this stage, adding references to these new functions could provide valuable context and guidance for users.
Overall, these points are minor compared to the excellent quality of the work,
and I believe they could further polish an already impressive project.
@nojaf I appreciate the thoughtful feedback! TL;DRI'm willing to switch back to
|
Thanks, @brianrourkeboll, for putting your thoughts together on this. I remain skeptical about the additional arguments presented. ComposabilityRegarding the possibility of adding functions in the future, it's worth noting that the current primary users of SyntaxVisitors—namely FSAC, VS tooling, and analyzers—interact at the file level with AST EditingAs for the suggestion of editing the AST, I remain unconvinced of its utility in our tooling scenarios. Considering our main consumers, they typically suggest changes in a text format. While manipulating the AST directly might seem appealing, the critical step of converting these changes back to text is non-trivial and is not a process our consumers are equipped to handle efficiently. Parameter OrderMy stance on parameter order is somewhat flexible, but there's a logical argument for placing val exists: predicate: (SyntaxVisitorPath -> SyntaxNode -> bool) -> position: pos -> parsedInput: ParsedInput -> bool The structure facilitates a natural application flow. Start by applying a generic function applicable to any file: let typeExists = exists (fun path node -> true)
let typeExistsInFile = typeExists cursorPosition parsedInput Then specify the concrete cursor position and file input. This rationale underpins my preference for arranging the predicate first. |
My 2Kč on this:
|
* Use `ParsedInput` as the main AST type. * Move the `position` parameter rightward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Astonishing work @brianrourkeboll!
I'm working on updating the docs and hope to push that today. |
Thanks, looking forward to have is in :) |
* Add basic examples for `ParsedInput` module functions. * Merge the existing `SyntaxVisitorBase` docs into the new file.
The docs are just golden like your API work 🥇 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Brian! Amazing work - keep rocking :)
* Name resolution: keep type vars in subsequent checks (#16456) * Keep typars produced in name resolution * Better debug errors * Unwrap measure type vars * Undo check declarations change * Fix reported range * Undo occurrence change * Skip path typars * Add test * More freshen typar APIs properly * Fantomas * Cleanup * Add release notes * 123 --------- Co-authored-by: Vlad Zarytovskii <[email protected]> * Build benchmarks in CI (#16518) * Remove profiling startpoint project * Add bench build job * Up * up * up --------- Co-authored-by: Kevin Ransom (msft) <[email protected]> * More ValueOption in compiler: part 1 (#16323) * More ValueOption in compiler: part 1 * release notes * Update CheckComputationExpressions.fs * release notes * `[Experimental]` `[WIP]` Transparent Compiler (#15179) * Track CheckDeclarations.CheckModuleSignature activity. (#16534) * Add Computation Expression Benchmarks (#16541) * add benchmarks for various usages of CEs * refactor * move CE source files to dedicated ce folder * Update Roslyn to a version which uses Immutable v7 (#16545) * revert #16326 (addition of XliffTasks reference) (#16548) * updated devcontainer image (#16551) * Add higher-order-function-based API for working with untyped AST (#16462) * Add module-based API for working with untyped AST * Fantomas * tryPickUntil → tryPickDownTo * Don't need that * Thread path while walking * Update comment * Simplify * Expose `Ast.fold` and `Ast.tryPick`. * Expose `SyntaxNode.(|Attributes|)`. * Ensure a few more syntax node cases get hit. * Update FCS release notes * Update surface area * Add back `foldWhile`; add `exists`, `tryNode` * Put `Ast.foldWhile` back in. * Add `Ast.exists`. * Add `Ast.tryNode`. * `SyntaxTraversal.Traverse` → `Ast.tryPick`… * Replace uses of `SyntaxTraversal.Traverse` in `FSharpParseFileResults` with the appropriate function from the `Ast` module: `exists`, `tryPick`, `tryNode`. * Update surface area * Need that * Just to be safe * Add `Ast.tryPickLast` * Handle multiple args mid-pipeline * Before, no signature help was offered in a case like this: ```fsharp [1..10] |> List.fold (fun acc _ -> acc) ‸ |> List.filter (fun x -> x > 3) ``` The service will now offer help for the `state` parameter when the cursor ‸ is in that location. * `*` instead of error * `FSharpParseFileResults.TryRangeOfFunctionOrMethodBeingApplied` was previously returning the range of the (zero-width) `SynExpr.ArbitraryAfterError`. It now returns the range of the `*` (`op_Multiply`) instead. * Update surface area * Fmt * Missed in merge * Add VS release notes entry * # → ### * Add ryPick tests * Add a few more tests * \n * Bump release notes * Fmt * `Ast` → `ParsedInput` * Use `ParsedInput` as the main AST type. * Move the `position` parameter rightward. * Update surface area * Less `function` * Update untyped AST docs * Add basic examples for `ParsedInput` module functions. * Merge the existing `SyntaxVisitorBase` docs into the new file. * Clean up doc comments --------- Co-authored-by: Vlad Zarytovskii <[email protected]> * Move paren entries to appropriate releases (#16561) * [main] Update dependencies from dotnet/source-build-reference-packages (#16532) * Update dependencies from https://github.com/dotnet/source-build-reference-packages build 20240115.2 Microsoft.SourceBuild.Intermediate.source-build-reference-packages From Version 9.0.0-alpha.1.24059.3 -> To Version 9.0.0-alpha.1.24065.2 * Update dependencies from https://github.com/dotnet/source-build-reference-packages build 20240116.1 Microsoft.SourceBuild.Intermediate.source-build-reference-packages From Version 9.0.0-alpha.1.24059.3 -> To Version 9.0.0-alpha.1.24066.1 * Update dependencies from https://github.com/dotnet/source-build-reference-packages build 20240117.1 Microsoft.SourceBuild.Intermediate.source-build-reference-packages From Version 9.0.0-alpha.1.24059.3 -> To Version 9.0.0-alpha.1.24067.1 * Update dependencies from https://github.com/dotnet/source-build-reference-packages build 20240117.1 Microsoft.SourceBuild.Intermediate.source-build-reference-packages From Version 9.0.0-alpha.1.24059.3 -> To Version 9.0.0-alpha.1.24067.1 * Update dependencies from https://github.com/dotnet/source-build-reference-packages build 20240117.1 Microsoft.SourceBuild.Intermediate.source-build-reference-packages From Version 9.0.0-alpha.1.24059.3 -> To Version 9.0.0-alpha.1.24067.1 * Update dependencies from https://github.com/dotnet/source-build-reference-packages build 20240117.1 Microsoft.SourceBuild.Intermediate.source-build-reference-packages From Version 9.0.0-alpha.1.24059.3 -> To Version 9.0.0-alpha.1.24067.1 --------- Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com> Co-authored-by: Vlad Zarytovskii <[email protected]> * Attempt to make links from single identifier module names. (#16550) * Add scenarios where parentheses are around module name. * Address problem tighter to nameof usage. * Restore missing commit and inline nameof ident check. * Add release note entry. * rewrite SizeOfValueInfo in Optimizer.fs to be tail-recursive (#16559) * rewrite SizeOfValueInfo in Optimizer.fs to be tail-recursive * use Brians rewrite into one local function * stringbuilder is not threadsafe (#16557) * Array postfix notation in fsharp core api (#16564) * changed array types to postfix form in all signatures * changed array types to postfix form in the implementation files * Revert 16348 (#16536) * Improve AsyncMemoize tests * relax test condition * Revert "Cancellable: set token from node/async in features code (#16348)" This reverts commit d4e3b26. * remove UsingToken * remove UsingToken * test improvement * relax test condition * use thread-safe collections when collecting events from AsyncMemoize * fix flaky test * release note * Small code reshuffle for diff minimization (#16569) * Moving code around * Small code reshuffle for diff minimization * wat * Refactor parens API (#16461) * Refactor parens API * Remove `UnnecessaryParentheses.getUnnecessaryParentheses`. * Expose `SynExpr.shouldBeParenthesizedInContext`. * Expose `SynPat.shouldBeParenthesizedInContext`. * Expose `SyntaxTraversal.TraverseAll`. * Fantomas * Use `ParsedInput.fold` * Tests * Update surface area * Clean up sigs & comments * Update release notes * Remove redundant async * Remove stubs (no longer needed) * Preserve original stacktrace in state machines if available (#16568) * Preserve original stacktrace in state machines if available * Update release notes * Automated command ran: fantomas Co-authored-by: vzarytovskii <[email protected]> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * check reportErrors and feature support at top level (#16549) * Align DU case augmentation with previous behavior in EraseUnions (#16571) * Align DU case augment with previous behavior in EraseUnions * Update 8.0.300.md * modify tests * Refresh debug surface area (#16573) * Remove superfluous rec keywords and untangle some functions (#16544) * remove some superfluous rec keywords and untangle two functions that aren't mutually recursive. * Don't throw on invalid input in Graph construction (#16575) * More ValueOption in compiler: part 2 (#16567) * More ValueOption in complier: part 2 * Update release notes * extra optimization * extra optimization 2 * fantomas * Update dependencies from https://github.com/dotnet/arcade build 20240123.2 (#16579) Microsoft.DotNet.Arcade.Sdk From Version 8.0.0-beta.24060.4 -> To Version 8.0.0-beta.24073.2 Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com> * [main] Update dependencies from dotnet/source-build-reference-packages (#16574) * Update dependencies from https://github.com/dotnet/source-build-reference-packages build 20240122.5 Microsoft.SourceBuild.Intermediate.source-build-reference-packages From Version 9.0.0-alpha.1.24067.1 -> To Version 9.0.0-alpha.1.24072.5 * Update dependencies from https://github.com/dotnet/source-build-reference-packages build 20240123.1 Microsoft.SourceBuild.Intermediate.source-build-reference-packages From Version 9.0.0-alpha.1.24067.1 -> To Version 9.0.0-alpha.1.24073.1 --------- Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com> Co-authored-by: Tomas Grosup <[email protected]> * Improve AsyncMemoize tests (#16580) --------- Co-authored-by: Eugene Auduchinok <[email protected]> Co-authored-by: Vlad Zarytovskii <[email protected]> Co-authored-by: Petr <[email protected]> Co-authored-by: Kevin Ransom (msft) <[email protected]> Co-authored-by: Petr Pokorny <[email protected]> Co-authored-by: Florian Verdonck <[email protected]> Co-authored-by: dawe <[email protected]> Co-authored-by: Tomas Grosup <[email protected]> Co-authored-by: Martin <[email protected]> Co-authored-by: Brian Rourke Boll <[email protected]> Co-authored-by: dotnet-maestro[bot] <42748379+dotnet-maestro[bot]@users.noreply.github.com> Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com> Co-authored-by: Jakub Majocha <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Ignore me, I am dumb and didn't realize this was in a new namespace. |
Description
As part of my refactoring of the parenthesization API (#16461), I needed to publicly expose some way to traverse the entire untyped AST. One option was to make public a version of the
SyntaxTraversal.traverseAll
function that I added in #16079 taking aSyntaxVisitorBase<'T>
implementation. This PR introduces (either instead or in addition) an alternative API based on higher-order functions that I think has advantages in ergonomics and composability.I think a focus on exposing composable, orthogonal APIs for FCS primitives, from
ParsedInput
orSyntaxNode
/SyntaxNodes
/Ast
toSynExpr
andSynPat
, could help greatly simplify current and future usage.I'd be interested in feedback on this from current internal and external consumers of the untyped AST, including @baronfel, @nojaf, etc.
Edit: I went ahead and tried to update much of FSAC's usage of the existing syntax visitor API to the new one, and I think the results are encouraging: https://gist.github.com/brianrourkeboll/96bedaaed7dd7c22ddbe2047ee937633
Edit: I also went ahead and updated the uses of
SyntaxTraversal.Traverse
inFSharpParseFileResults
to use the new API. That surfaced the need forParsedInput.exists
,ParsedInput.tryNode
, andParsedInput.tryPickLast
. It also happened to lead me to make a small improvement to the signature help functionality:Before, no signature help was offered in a situation like this, where a function in the middle of a pipeline already had a complex curried parameter (e.g., a lambda) applied to it:
The service will now offer help for the
state
parameter when thecursor ‸ is in that location:
Provisional API surface area (updated)
Original proposed API
Notes
ServiceParseTreeWalk.fsi
for doc comments with additional descriptions.tryPick
effectively corresponds to the existingSyntaxTraversal.Traverse
API. It could also be namedtryPickDownTo
,tryPickUntil
, ortryPickThrough
.position
parameter, because the common case is that a position of interest is already known when they are called, and so unnecessary traversal of extra nodes can be minimized. I have considered adding some kind of suffix to these functions (tryPickDownTo
/tryPickAt
,tryNodeAt
,existsAt
, etc.) in case we might also want to add variants without aposition
parameter (for when a position of interest is not already known), but I don't have a compelling use-case for such variants yet.Ast.mapExpr
,Ast.mapPat
… This could be very useful in, e.g., certain bulk codefix scenarios.Examples
Original examples
Use
ParsedInput.fold
to traverse the entire tree and accumulate results.Use
ParsedInput.foldWhile
to accumulate results, but short-circuit where desired.Use
ParsedInput.walk
to traverse the tree in a specific way, e.g., lopping off entire branches, changing the order in which a given node's children are traversed, etc.Use
ParsedInput.tryPick
to dive to the specific node you want (based on position, path, case, whatever) and return some value derived from the node, like the node and its context, or the node's range, or anything else.Checklist
FSharpParseFileResults
. These transitively callParsedInput.tryPick
,ParsedInput.tryPickLast
,ParsedInput.exists
,ParsedInput.tryNode
,ParsedInput.foldWhile
under the hood.ParsedInput.tryPick
(corresponding to the existing ones forSyntaxTraversal.Traverse
).ParsedInput.tryPickLast
,ParsedInput.exists
,ParsedInput.tryNode
,ParsedInput.fold
,ParsedInput.foldWhile
.