F# Compiler Guide


Compiler Services: Using the SyntaxVisitorBase

Syntax tree traversal is a common topic when interacting with the FSharp.Compiler.Service.
As established in Tutorial: Expressions, the ParsedInput can be traversed by a set of recursive functions.
It can be tedious to always construct these functions from scratch.

As an alternative, a SyntaxVisitorBase can be used to traverse the syntax tree. Consider, the following code sample:

let codeSample = """
module Lib

let myFunction paramOne paramTwo =
    ()
"""

Imagine we wish to grab the myFunction name from the headPat in the SynBinding.
Let's introduce a helper function to construct the AST:

#r "FSharp.Compiler.Service.dll"
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Text
open FSharp.Compiler.Syntax

let checker = FSharpChecker.Create()

/// Helper to construct an ParsedInput from a code snippet.
let mkTree codeSample =
    let parseFileResults =
        checker.ParseFile(
            "FileName.fs",
            SourceText.ofString codeSample,
            { FSharpParsingOptions.Default with SourceFiles = [| "FileName.fs" |] }
        )
        |> Async.RunSynchronously

    parseFileResults.ParseTree

And create a visitor to traverse the tree:

let visitor =
    { new SyntaxVisitorBase<string>() with
        override this.VisitPat(path, defaultTraverse, synPat) =
            // First check if the pattern is what we are looking for.
            match synPat with
            | SynPat.LongIdent(longDotId = SynLongIdent(id = [ ident ])) ->
                // Next we can check if the current path of visited nodes, matches our expectations.
                // The path will contain all the ancestors of the current node.
                match path with
                // The parent node of `synPat` should be a `SynBinding`.
                | SyntaxNode.SynBinding _ :: _ ->
                    // We return a `Some` option to indicate we found what we are looking for.
                    Some ident.idText
                // If the parent is something else, we can skip it here.
                | _ -> None
            | _ -> None }

let result = SyntaxTraversal.Traverse(Position.pos0, mkTree codeSample, visitor) // Some "myFunction"

Instead of traversing manually from ParsedInput to SynModuleOrNamespace to SynModuleDecl.Let to SynBinding to SynPat, we leverage the default navigation that happens in SyntaxTraversal.Traverse.
A SyntaxVisitorBase will shortcut all other code paths once a single VisitXYZ override has found anything.

Our code sample of course only had one let binding and thus we didn't need to specify any further logic whether to differentiate between multiple bindings. Let's consider a second example where we know the user's cursor inside an IDE is placed after c and we are interested in the body expression of the let binding.

let secondCodeSample = """
module X

let a = 0
let b = 1
let c = 2
"""

let secondVisitor =
    { new SyntaxVisitorBase<SynExpr>() with
        override this.VisitBinding(path, defaultTraverse, binding) =
            match binding with
            | SynBinding(expr = e) -> Some e }

let cursorPos = Position.mkPos 6 5

let secondResult =
    SyntaxTraversal.Traverse(cursorPos, mkTree secondCodeSample, secondVisitor) // Some (Const (Int32 2, (6,8--6,9)))

Due to our passed cursor position, we did not need to write any code to exclude the expressions of the other let bindings. SyntaxTraversal.Traverse will check whether the current position is inside any syntax node before drilling deeper.

Lastly, some VisitXYZ overrides can contain a defaultTraverse. This helper allows you to continue the default traversal when you currently hit a node that is not of interest. Consider 1 + 2 + 3 + 4, this will be reflected in a nested infix application expression. If the cursor is at the end of the entire expression, we can grab the value of 4 using the following visitor:

let thirdCodeSample = "let sum = 1 + 2 + 3 + 4"

(*
AST will look like:

Let
 (false,
  [SynBinding
     (None, Normal, false, false, [],
      PreXmlDoc ((1,0), Fantomas.FCS.Xml.XmlDocCollector),
      SynValData
        (None, SynValInfo ([], SynArgInfo ([], false, None)), None,
         None),
      Named (SynIdent (sum, None), false, None, (1,4--1,7)), None,
      App
        (NonAtomic, false,
         App
           (NonAtomic, true,
            LongIdent
              (false,
               SynLongIdent
                 ([op_Addition], [], [Some (OriginalNotation "+")]),
               None, (1,20--1,21)),
            App
              (NonAtomic, false,
               App
                 (NonAtomic, true,
                  LongIdent
                    (false,
                     SynLongIdent
                       ([op_Addition], [],
                        [Some (OriginalNotation "+")]), None,
                     (1,16--1,17)),
                  App
                    (NonAtomic, false,
                     App
                       (NonAtomic, true,
                        LongIdent
                          (false,
                           SynLongIdent
                             ([op_Addition], [],
                              [Some (OriginalNotation "+")]), None,
                           (1,12--1,13)),
                        Const (Int32 1, (1,10--1,11)), (1,10--1,13)),
                     Const (Int32 2, (1,14--1,15)), (1,10--1,15)),
                  (1,10--1,17)), Const (Int32 3, (1,18--1,19)),
               (1,10--1,19)), (1,10--1,21)),
         Const (Int32 4, (1,22--1,23)), (1,10--1,23)), (1,4--1,7),
      Yes (1,0--1,23), { LeadingKeyword = Let (1,0--1,3)
                         InlineKeyword = None
                         EqualsRange = Some (1,8--1,9) })
*)

let thirdCursorPos = Position.mkPos 1 22

let thirdVisitor =
    { new SyntaxVisitorBase<int>() with
        override this.VisitExpr(path, traverseSynExpr, defaultTraverse, synExpr) =
            match synExpr with
            | SynExpr.Const (constant = SynConst.Int32 v) -> Some v
            // We do want to continue to traverse when nodes like `SynExpr.App` are found.
            | otherExpr -> defaultTraverse otherExpr }

let thirdResult =
    SyntaxTraversal.Traverse(cursorPos, mkTree thirdCodeSample, thirdVisitor) // Some 4

defaultTraverse is especially useful when you do not know upfront what syntax tree you will be walking.
This is a common case when dealing with IDE tooling. You won't know what actual code the end-user is currently processing.

Note: SyntaxVisitorBase is designed to find a single value inside a tree!
This is not an ideal solution when you are interested in all nodes of certain shape.
It will always verify if the given cursor position is still matching the range of the node.
As a fallback the first branch will be explored when you pass Position.pos0.
By design, it is meant to find a single result.

val codeSample: string
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
namespace FSharp.Compiler
namespace FSharp.Compiler.CodeAnalysis
namespace FSharp.Compiler.Text
namespace FSharp.Compiler.Syntax
val checker: FSharpChecker
type FSharpChecker = member CheckFileInProject: parseResults: FSharpParseFileResults * fileName: string * fileVersion: int * sourceText: ISourceText * options: FSharpProjectOptions * ?userOpName: string -> Async<FSharpCheckFileAnswer> member ClearCache: options: FSharpProjectOptions seq * ?userOpName: string -> unit member ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients: unit -> unit member Compile: argv: string array * ?userOpName: string -> Async<FSharpDiagnostic array * int> member FindBackgroundReferencesInFile: fileName: string * options: FSharpProjectOptions * symbol: FSharpSymbol * ?canInvalidateProject: bool * [<Experimental ("This FCS API is experimental and subject to change.")>] ?fastCheck: bool * ?userOpName: string -> Async<range seq> member GetBackgroundCheckResultsForFileInProject: fileName: string * options: FSharpProjectOptions * ?userOpName: string -> Async<FSharpParseFileResults * FSharpCheckFileResults> member GetBackgroundParseResultsForFileInProject: fileName: string * options: FSharpProjectOptions * ?userOpName: string -> Async<FSharpParseFileResults> member GetBackgroundSemanticClassificationForFile: fileName: string * options: FSharpProjectOptions * ?userOpName: string -> Async<SemanticClassificationView option> member GetParsingOptionsFromCommandLineArgs: sourceFiles: string list * argv: string list * ?isInteractive: bool * ?isEditing: bool -> FSharpParsingOptions * FSharpDiagnostic list + 1 overload member GetParsingOptionsFromProjectOptions: options: FSharpProjectOptions -> FSharpParsingOptions * FSharpDiagnostic list ...
<summary> Used to parse and check F# source code. </summary>
static member FSharpChecker.Create: ?projectCacheSize: int * ?keepAssemblyContents: bool * ?keepAllBackgroundResolutions: bool * ?legacyReferenceResolver: LegacyReferenceResolver * ?tryGetMetadataSnapshot: FSharp.Compiler.AbstractIL.ILBinaryReader.ILReaderTryGetMetadataSnapshot * ?suggestNamesForErrors: bool * ?keepAllBackgroundSymbolUses: bool * ?enableBackgroundItemKeyStoreAndSemanticClassification: bool * ?enablePartialTypeChecking: bool * ?parallelReferenceResolution: bool * ?captureIdentifiersWhenParsing: bool * [<Experimental ("This parameter is experimental and likely to be removed in the future.")>] ?documentSource: DocumentSource * [<Experimental ("This parameter is experimental and likely to be removed in the future.")>] ?useSyntaxTreeCache: bool -> FSharpChecker
val mkTree: codeSample: string -> ParsedInput
 Helper to construct an ParsedInput from a code snippet.
val parseFileResults: FSharpParseFileResults
member FSharpChecker.ParseFile: fileName: string * sourceText: ISourceText * options: FSharpParsingOptions * ?cache: bool * ?userOpName: string -> Async<FSharpParseFileResults>
module SourceText from FSharp.Compiler.Text
<summary> Functions related to ISourceText objects </summary>
val ofString: string -> ISourceText
<summary> Creates an ISourceText object from the given string </summary>
type FSharpParsingOptions = { SourceFiles: string array ApplyLineDirectives: bool ConditionalDefines: string list DiagnosticOptions: FSharpDiagnosticOptions LangVersionText: string IsInteractive: bool IndentationAwareSyntax: bool option StrictIndentation: bool option CompilingFSharpCore: bool IsExe: bool } static member Default: FSharpParsingOptions
<summary> Options used to determine active --define conditionals and other options relevant to parsing files in a project </summary>
property FSharpParsingOptions.Default: FSharpParsingOptions with get
Multiple items
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit) static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate) static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async<bool> static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload static member AwaitWaitHandle: waitHandle: WaitHandle * ?millisecondsTimeout: int -> Async<bool> static member CancelDefaultToken: unit -> unit static member Catch: computation: Async<'T> -> Async<Choice<'T,exn>> static member Choice: computations: Async<'T option> seq -> Async<'T option> static member FromBeginEnd: beginAction: (AsyncCallback * obj -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> ...

--------------------
type Async<'T>
static member Async.RunSynchronously: computation: Async<'T> * ?timeout: int * ?cancellationToken: System.Threading.CancellationToken -> 'T
property FSharpParseFileResults.ParseTree: ParsedInput with get
<summary> The syntax tree resulting from the parse </summary>
val visitor: SyntaxVisitorBase<string>
Multiple items
type SyntaxVisitorBase<'T> = new: unit -> SyntaxVisitorBase<'T> abstract VisitAttributeApplication: path: SyntaxVisitorPath * attributes: SynAttributeList -> 'T option + 1 overload abstract VisitBinding: path: SyntaxVisitorPath * defaultTraverse: (SynBinding -> 'T option) * synBinding: SynBinding -> 'T option + 1 overload abstract VisitComponentInfo: path: SyntaxVisitorPath * synComponentInfo: SynComponentInfo -> 'T option + 1 overload abstract VisitEnumDefn: path: SyntaxVisitorPath * cases: SynEnumCase list * range -> 'T option + 1 overload abstract VisitExpr: path: SyntaxVisitorPath * traverseSynExpr: (SynExpr -> 'T option) * defaultTraverse: (SynExpr -> 'T option) * synExpr: SynExpr -> 'T option + 1 overload abstract VisitHashDirective: path: SyntaxVisitorPath * hashDirective: ParsedHashDirective * range: range -> 'T option + 1 overload abstract VisitImplicitInherit: path: SyntaxVisitorPath * defaultTraverse: (SynExpr -> 'T option) * inheritedType: SynType * synArgs: SynExpr * range: range -> 'T option + 1 overload abstract VisitInheritSynMemberDefn: path: SyntaxVisitorPath * componentInfo: SynComponentInfo * typeDefnKind: SynTypeDefnKind * synType: SynType * members: SynMemberDefns * range: range -> 'T option + 1 overload abstract VisitInterfaceSynMemberDefnType: path: SyntaxVisitorPath * synType: SynType -> 'T option + 1 overload ...

--------------------
new: unit -> SyntaxVisitorBase<'T>
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
val this: SyntaxVisitorBase<string>
override SyntaxVisitorBase.VisitPat: path: SyntaxVisitorPath * defaultTraverse: (SynPat -> 'T option) * synPat: SynPat -> 'T option
val path: SyntaxVisitorPath
val defaultTraverse: (SynPat -> string option)
val synPat: SynPat
type SynPat = | Const of constant: SynConst * range: range | Wild of range: range | Named of ident: SynIdent * isThisVal: bool * accessibility: SynAccess option * range: range | Typed of pat: SynPat * targetType: SynType * range: range | Attrib of pat: SynPat * attributes: SynAttributes * range: range | Or of lhsPat: SynPat * rhsPat: SynPat * range: range * trivia: SynPatOrTrivia | ListCons of lhsPat: SynPat * rhsPat: SynPat * range: range * trivia: SynPatListConsTrivia | Ands of pats: SynPat list * range: range | As of lhsPat: SynPat * rhsPat: SynPat * range: range | LongIdent of longDotId: SynLongIdent * extraId: Ident option * typarDecls: SynValTyparDecls option * argPats: SynArgPats * accessibility: SynAccess option * range: range ... member Range: range
<summary> Represents a syntax tree for an F# pattern </summary>
union case SynPat.LongIdent: longDotId: SynLongIdent * extraId: Ident option * typarDecls: SynValTyparDecls option * argPats: SynArgPats * accessibility: SynAccess option * range: range -> SynPat
<summary> A long identifier pattern possibly with argument patterns </summary>
Multiple items
union case SynLongIdent.SynLongIdent: id: LongIdent * dotRanges: range list * trivia: FSharp.Compiler.SyntaxTrivia.IdentTrivia option list -> SynLongIdent

--------------------
type SynLongIdent = | SynLongIdent of id: LongIdent * dotRanges: range list * trivia: IdentTrivia option list member Dots: range list member IdentsWithTrivia: SynIdent list member LongIdent: LongIdent member Range: range member RangeWithoutAnyExtraDot: range member ThereIsAnExtraDotAtTheEnd: bool member Trivia: IdentTrivia list
<summary> Represents a long identifier with possible '.' at end. Typically dotRanges.Length = lid.Length-1, but they may be same if (incomplete) code ends in a dot, e.g. "Foo.Bar." The dots mostly matter for parsing, and are typically ignored by the typechecker, but if dotRanges.Length = lid.Length, then the parser must have reported an error, so the typechecker is allowed more freedom about typechecking these expressions. LongIdent can be empty list - it is used to denote that name of some AST element is absent (i.e. empty type name in inherit) </summary>
val id: x: 'T -> 'T
val ident: Ident
type SyntaxNode = | SynPat of SynPat | SynType of SynType | SynExpr of SynExpr | SynModule of SynModuleDecl | SynModuleOrNamespace of SynModuleOrNamespace | SynTypeDefn of SynTypeDefn | SynMemberDefn of SynMemberDefn | SynMatchClause of SynMatchClause | SynBinding of SynBinding | SynModuleOrNamespaceSig of SynModuleOrNamespaceSig ...
<summary> Used to track route during traversal of syntax using SyntaxTraversal.Traverse </summary>
union case SyntaxNode.SynBinding: SynBinding -> SyntaxNode
union case Option.Some: Value: 'T -> Option<'T>
property Ident.idText: string with get
union case Option.None: Option<'T>
val result: string option
module SyntaxTraversal from FSharp.Compiler.Syntax
val Traverse: pos: pos * parseTree: ParsedInput * visitor: SyntaxVisitorBase<'T> -> 'T option
Multiple items
module Position from FSharp.Compiler.Text

--------------------
[<Struct>] type Position = member Column: int member Line: int
<summary> Represents a position in a file </summary>
val pos0: pos
<summary> The zero position </summary>
val secondCodeSample: string
val secondVisitor: SyntaxVisitorBase<SynExpr>
type SynExpr = | Paren of expr: SynExpr * leftParenRange: range * rightParenRange: range option * range: range | Quote of operator: SynExpr * isRaw: bool * quotedExpr: SynExpr * isFromQueryExpression: bool * range: range | Const of constant: SynConst * range: range | Typed of expr: SynExpr * targetType: SynType * range: range | Tuple of isStruct: bool * exprs: SynExpr list * commaRanges: range list * range: range | AnonRecd of isStruct: bool * copyInfo: (SynExpr * BlockSeparator) option * recordFields: (SynLongIdent * range option * SynExpr) list * range: range * trivia: SynExprAnonRecdTrivia | ArrayOrList of isArray: bool * exprs: SynExpr list * range: range | Record of baseInfo: (SynType * SynExpr * range * BlockSeparator option * range) option * copyInfo: (SynExpr * BlockSeparator) option * recordFields: SynExprRecordField list * range: range | New of isProtected: bool * targetType: SynType * expr: SynExpr * range: range | ObjExpr of objType: SynType * argOptions: (SynExpr * Ident option) option * withKeyword: range option * bindings: SynBinding list * members: SynMemberDefns * extraImpls: SynInterfaceImpl list * newExprRange: range * range: range ... member IsArbExprAndThusAlreadyReportedError: bool member Range: range member RangeOfFirstPortion: range member RangeWithoutAnyExtraDot: range
<summary> Represents a syntax tree for F# expressions </summary>
val this: SyntaxVisitorBase<SynExpr>
override SyntaxVisitorBase.VisitBinding: path: SyntaxVisitorPath * defaultTraverse: (SynBinding -> 'T option) * synBinding: SynBinding -> 'T option
val defaultTraverse: (SynBinding -> SynExpr option)
val binding: SynBinding
Multiple items
union case SynBinding.SynBinding: accessibility: SynAccess option * kind: SynBindingKind * isInline: bool * isMutable: bool * attributes: SynAttributes * xmlDoc: FSharp.Compiler.Xml.PreXmlDoc * valData: SynValData * headPat: SynPat * returnInfo: SynBindingReturnInfo option * expr: SynExpr * range: range * debugPoint: DebugPointAtBinding * trivia: FSharp.Compiler.SyntaxTrivia.SynBindingTrivia -> SynBinding

--------------------
type SynBinding = | SynBinding of accessibility: SynAccess option * kind: SynBindingKind * isInline: bool * isMutable: bool * attributes: SynAttributes * xmlDoc: PreXmlDoc * valData: SynValData * headPat: SynPat * returnInfo: SynBindingReturnInfo option * expr: SynExpr * range: range * debugPoint: DebugPointAtBinding * trivia: SynBindingTrivia member RangeOfBindingWithRhs: range member RangeOfBindingWithoutRhs: range member RangeOfHeadPattern: range
<summary> Represents a binding for a 'let' or 'member' declaration </summary>
val e: SynExpr
val cursorPos: pos
val mkPos: line: int -> column: int -> pos
<summary> Create a position for the given line and column </summary>
val secondResult: SynExpr option
val thirdCodeSample: string
val thirdCursorPos: pos
val thirdVisitor: SyntaxVisitorBase<int>
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
val this: SyntaxVisitorBase<int>
override SyntaxVisitorBase.VisitExpr: path: SyntaxVisitorPath * traverseSynExpr: (SynExpr -> 'T option) * defaultTraverse: (SynExpr -> 'T option) * synExpr: SynExpr -> 'T option
val traverseSynExpr: (SynExpr -> int option)
val defaultTraverse: (SynExpr -> int option)
val synExpr: SynExpr
union case SynExpr.Const: constant: SynConst * range: range -> SynExpr
<summary> F# syntax: 1, 1.3, () etc. </summary>
type SynConst = | Unit | Bool of bool | SByte of sbyte | Byte of byte | Int16 of int16 | UInt16 of uint16 | Int32 of int32 | UInt32 of uint32 | Int64 of int64 | UInt64 of uint64 ... member Range: dflt: range -> range
<summary> The unchecked abstract syntax tree of constants in F# types and expressions. </summary>
union case SynConst.Int32: int32 -> SynConst
<summary> F# syntax: 13, 0x4000, 0o0777 </summary>
val v: int32
val otherExpr: SynExpr
val thirdResult: int option