Compiler Services: Processing typed expression tree
This tutorial demonstrates how to get the checked, typed expressions tree (TAST) for F# code and how to walk over the tree.
This can be used for creating tools such as source code analyzers and refactoring tools. You can also combine the information with the API available from symbols.
NOTE: The FSharp.Compiler.Service API is subject to change when later versions of the nuget package are published
Getting checked expressions
To access the type-checked, resolved expressions, you need to create an instance of InteractiveChecker
.
To use the interactive checker, reference FSharp.Compiler.Service.dll
and open the
relevant namespaces:
#r "FSharp.Compiler.Service.dll"
open System
open System.IO
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.EditorServices
open FSharp.Compiler.Symbols
open FSharp.Compiler.Text
Checking code
We first parse and check some code as in the symbols tutorial. One difference is that we set keepAssemblyContents to true.
// Create an interactive checker instance
let checker = FSharpChecker.Create(keepAssemblyContents=true)
let parseAndCheckSingleFile (input) =
let file = Path.ChangeExtension(System.IO.Path.GetTempFileName(), "fsx")
File.WriteAllText(file, input)
// Get context representing a stand-alone (script) file
let projOptions, _errors =
checker.GetProjectOptionsFromScript(file, SourceText.ofString input, assumeDotNetFramework=false)
|> Async.RunSynchronously
checker.ParseAndCheckProject(projOptions)
|> Async.RunSynchronously
Getting the expressions
After type checking a file, you can access the declarations and contents of the assembly, including expressions:
let input2 =
"""
module MyLibrary
open System
let foo(x, y) =
let msg = String.Concat("Hello", " ", "world")
if msg.Length > 10 then
10
else
20
type MyClass() =
member x.MyMethod() = 1
"""
let checkProjectResults =
parseAndCheckSingleFile(input2)
checkProjectResults.Diagnostics // should be empty
Checked assemblies are made up of a series of checked implementation files. The "file" granularity matters in F# because initialization actions are triggered at the granularity of files. In this case there is only one implementation file in the project:
let checkedFile = checkProjectResults.AssemblyContents.ImplementationFiles.[0]
Checked assemblies are made up of a series of checked implementation files. The "file" granularity matters in F# because initialization actions are triggered at the granularity of files. In this case there is only one implementation file in the project:
let rec printDecl prefix d =
match d with
| FSharpImplementationFileDeclaration.Entity (e, subDecls) ->
printfn "%sEntity %s was declared and contains %d sub-declarations" prefix e.CompiledName subDecls.Length
for subDecl in subDecls do
printDecl (prefix+" ") subDecl
| FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(v, vs, e) ->
printfn "%sMember or value %s was declared" prefix v.CompiledName
| FSharpImplementationFileDeclaration.InitAction(e) ->
printfn "%sA top-level expression was declared" prefix
for d in checkedFile.Declarations do
printDecl "" d
// Entity MyLibrary was declared and contains 4 sub-declarations
// Member or value foo was declared
// Entity MyClass was declared and contains 0 sub-declarations
// Member or value .ctor was declared
// Member or value MyMethod was declared
As can be seen, the only declaration in the implementation file is that of the module MyLibrary, which contains fours sub-declarations.
As an aside, one peculiarity here is that the member declarations (e.g. the "MyMethod" member) are returned as part of the containing module entity, not as part of their class. Note that the class constructor is returned as a separate declaration. The class type definition has been "split" into a constructor and the other declarations.
let myLibraryEntity, myLibraryDecls =
match checkedFile.Declarations.[0] with
| FSharpImplementationFileDeclaration.Entity (e, subDecls) -> (e, subDecls)
| _ -> failwith "unexpected"
What about the expressions, for example the body of function "foo"? Let's find it:
let (fooSymbol, fooArgs, fooExpression) =
match myLibraryDecls.[0] with
| FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(v, vs, e) -> (v, vs, e)
| _ -> failwith "unexpected"
Here 'fooSymbol' is a symbol associated with the declaration of 'foo', 'fooArgs' represents the formal arguments to the 'foo' function, and 'fooExpression' is an expression for the implementation of the 'foo' function.
Once you have an expression, you can work with it much like an F# quotation. For example, you can find its declaration range and its type:
fooExpression.Type // shows that the return type of the body expression is 'int'
fooExpression.Range // shows the declaration range of the expression implementing 'foo'
Walking over expressions
Expressions are analyzed using active patterns, much like F# quotations. Here is a generic expression visitor:
let rec visitExpr f (e:FSharpExpr) =
f e
match e with
| FSharpExprPatterns.AddressOf(lvalueExpr) ->
visitExpr f lvalueExpr
| FSharpExprPatterns.AddressSet(lvalueExpr, rvalueExpr) ->
visitExpr f lvalueExpr; visitExpr f rvalueExpr
| FSharpExprPatterns.Application(funcExpr, typeArgs, argExprs) ->
visitExpr f funcExpr; visitExprs f argExprs
| FSharpExprPatterns.Call(objExprOpt, memberOrFunc, typeArgs1, typeArgs2, argExprs) ->
visitObjArg f objExprOpt; visitExprs f argExprs
| FSharpExprPatterns.Coerce(targetType, inpExpr) ->
visitExpr f inpExpr
| FSharpExprPatterns.FastIntegerForLoop(startExpr, limitExpr, consumeExpr, isUp, _, _) ->
visitExpr f startExpr; visitExpr f limitExpr; visitExpr f consumeExpr
| FSharpExprPatterns.ILAsm(asmCode, typeArgs, argExprs) ->
visitExprs f argExprs
| FSharpExprPatterns.ILFieldGet (objExprOpt, fieldType, fieldName) ->
visitObjArg f objExprOpt
| FSharpExprPatterns.ILFieldSet (objExprOpt, fieldType, fieldName, valueExpr) ->
visitObjArg f objExprOpt
| FSharpExprPatterns.IfThenElse (guardExpr, thenExpr, elseExpr) ->
visitExpr f guardExpr; visitExpr f thenExpr; visitExpr f elseExpr
| FSharpExprPatterns.Lambda(lambdaVar, bodyExpr) ->
visitExpr f bodyExpr
| FSharpExprPatterns.Let((bindingVar, bindingExpr, dbg), bodyExpr) ->
visitExpr f bindingExpr; visitExpr f bodyExpr
| FSharpExprPatterns.LetRec(recursiveBindings, bodyExpr) ->
for _,bindingExpr,_ in recursiveBindings do visitExpr f bindingExpr
visitExpr f bodyExpr
| FSharpExprPatterns.NewArray(arrayType, argExprs) ->
visitExprs f argExprs
| FSharpExprPatterns.NewDelegate(delegateType, delegateBodyExpr) ->
visitExpr f delegateBodyExpr
| FSharpExprPatterns.NewObject(objType, typeArgs, argExprs) ->
visitExprs f argExprs
| FSharpExprPatterns.NewRecord(recordType, argExprs) ->
visitExprs f argExprs
| FSharpExprPatterns.NewAnonRecord(recordType, argExprs) ->
visitExprs f argExprs
| FSharpExprPatterns.NewTuple(tupleType, argExprs) ->
visitExprs f argExprs
| FSharpExprPatterns.NewUnionCase(unionType, unionCase, argExprs) ->
visitExprs f argExprs
| FSharpExprPatterns.Quote(quotedExpr) ->
visitExpr f quotedExpr
| FSharpExprPatterns.FSharpFieldGet(objExprOpt, recordOrClassType, fieldInfo) ->
visitObjArg f objExprOpt
| FSharpExprPatterns.AnonRecordGet(objExpr, recordOrClassType, fieldInfo) ->
visitExpr f objExpr
| FSharpExprPatterns.FSharpFieldSet(objExprOpt, recordOrClassType, fieldInfo, argExpr) ->
visitObjArg f objExprOpt; visitExpr f argExpr
| FSharpExprPatterns.Sequential(firstExpr, secondExpr) ->
visitExpr f firstExpr; visitExpr f secondExpr
| FSharpExprPatterns.TryFinally(bodyExpr, finalizeExpr, dbgTry, dbgFinally) ->
visitExpr f bodyExpr; visitExpr f finalizeExpr
| FSharpExprPatterns.TryWith(bodyExpr, _, _, catchVar, catchExpr, dbgTry, dbgWith) ->
visitExpr f bodyExpr; visitExpr f catchExpr
| FSharpExprPatterns.TupleGet(tupleType, tupleElemIndex, tupleExpr) ->
visitExpr f tupleExpr
| FSharpExprPatterns.DecisionTree(decisionExpr, decisionTargets) ->
visitExpr f decisionExpr; List.iter (snd >> visitExpr f) decisionTargets
| FSharpExprPatterns.DecisionTreeSuccess (decisionTargetIdx, decisionTargetExprs) ->
visitExprs f decisionTargetExprs
| FSharpExprPatterns.TypeLambda(genericParam, bodyExpr) ->
visitExpr f bodyExpr
| FSharpExprPatterns.TypeTest(ty, inpExpr) ->
visitExpr f inpExpr
| FSharpExprPatterns.UnionCaseSet(unionExpr, unionType, unionCase, unionCaseField, valueExpr) ->
visitExpr f unionExpr; visitExpr f valueExpr
| FSharpExprPatterns.UnionCaseGet(unionExpr, unionType, unionCase, unionCaseField) ->
visitExpr f unionExpr
| FSharpExprPatterns.UnionCaseTest(unionExpr, unionType, unionCase) ->
visitExpr f unionExpr
| FSharpExprPatterns.UnionCaseTag(unionExpr, unionType) ->
visitExpr f unionExpr
| FSharpExprPatterns.ObjectExpr(objType, baseCallExpr, overrides, interfaceImplementations) ->
visitExpr f baseCallExpr
List.iter (visitObjMember f) overrides
List.iter (snd >> List.iter (visitObjMember f)) interfaceImplementations
| FSharpExprPatterns.TraitCall(sourceTypes, traitName, typeArgs, typeInstantiation, argTypes, argExprs) ->
visitExprs f argExprs
| FSharpExprPatterns.ValueSet(valToSet, valueExpr) ->
visitExpr f valueExpr
| FSharpExprPatterns.WhileLoop(guardExpr, bodyExpr, dbg) ->
visitExpr f guardExpr; visitExpr f bodyExpr
| FSharpExprPatterns.BaseValue baseType -> ()
| FSharpExprPatterns.DefaultValue defaultType -> ()
| FSharpExprPatterns.ThisValue thisType -> ()
| FSharpExprPatterns.Const(constValueObj, constType) -> ()
| FSharpExprPatterns.Value(valueToGet) -> ()
| _ -> failwith (sprintf "unrecognized %+A" e)
and visitExprs f exprs =
List.iter (visitExpr f) exprs
and visitObjArg f objOpt =
Option.iter (visitExpr f) objOpt
and visitObjMember f memb =
visitExpr f memb.Body
Let's use this expression walker:
fooExpression |> visitExpr (fun e -> printfn "Visiting %A" e)
// Prints:
//
// Visiting Let...
// Visiting Call...
// Visiting Const ("Hello", ...)
// Visiting Const (" ", ...)
// Visiting Const ("world", ...)
// Visiting IfThenElse...
// Visiting Call...
// Visiting Call...
// Visiting Value ...
// Visiting Const ...
// Visiting Const ...
// Visiting Const ...
Note that
- The visitExpr function is recursive (for nested expressions).
- Pattern matching is removed from the tree, into a form called 'decision trees'.
Summary
In this tutorial, we looked at the basics of working with checked declarations and expressions.
In practice, it is also useful to combine the information here with some information you can obtain from the symbols tutorial.
namespace FSharp
--------------------
namespace Microsoft.FSharp
namespace FSharp
--------------------
namespace Microsoft.FSharp
--------------------
type FSharpAttribute = member Format: context: FSharpDisplayContext -> string member IsAttribute: unit -> bool member AttributeType: FSharpEntity with get member ConstructorArguments: IList<FSharpType * obj> with get member IsUnresolved: bool with get member NamedArguments: IList<FSharpType * string * bool * obj> with get member Range: range with get
<summary> Represents a custom attribute attached to F# source code or a compiler .NET component </summary>
<summary> Used to parse and check F# source code. </summary>
<summary>Performs operations on <see cref="T:System.String" /> instances that contain file or directory path information. These operations are performed in a cross-platform manner.</summary>
<summary>Provides static methods for the creation, copying, deletion, moving, and opening of a single file, and aids in the creation of <see cref="T:System.IO.FileStream" /> objects.</summary>
File.WriteAllText(path: string, contents: string) : unit
File.WriteAllText(path: string, contents: ReadOnlySpan<char>, encoding: Text.Encoding) : unit
File.WriteAllText(path: string, contents: string, encoding: Text.Encoding) : unit
<summary> Functions related to ISourceText objects </summary>
<summary> Creates an ISourceText object from the given string </summary>
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * objnull -> 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 * objnull -> 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>
member FSharpChecker.ParseAndCheckProject: options: FSharpProjectOptions * ?userOpName: string -> Async<FSharpCheckProjectResults>
<summary> The errors returned by processing the project </summary>
<summary> Get a view of the overall contents of the assembly. Only valid to use if HasCriticalErrors is false. </summary>
<summary> The contents of the implementation files in the assembly </summary>
<summary> Represents a declaration in an implementation file, as seen by the F# language </summary>
<summary> Represents the declaration of a type </summary>
<summary> Get the compiled name of the type or module, possibly with `n mangling. This is identical to LogicalName unless the CompiledName attribute is used. </summary>
<summary> Represents the declaration of a member, function or value, including the parameters and body of the member </summary>
<summary> Get the member name in compiled code </summary>
<summary> Represents the declaration of a static initialization action </summary>
<summary> Get the declarations that make up this implementation file </summary>
<summary> The type of the expression </summary>
<summary> The range of the expression </summary>
<summary> Represents a checked and reduced expression, as seen by the F# language. The active patterns in 'FSharp.Compiler.SourceCodeServices' can be used to analyze information about the expression. Pattern matching is reduced to decision trees and conditional tests. Some other constructs may be represented in reduced form. </summary>
<summary> A collection of active patterns to analyze expressions </summary>
<summary> Matches expressions which take the address of a location </summary>
<summary> Matches expressions which set the contents of an address </summary>
<summary> Matches expressions which are the application of function values </summary>
<summary> Matches expressions which are calls to members or module-defined functions. When calling curried functions and members the arguments are collapsed to a single collection of arguments, as done in the compiled version of these. </summary>
<summary> Matches expressions which coerce the type of a value </summary>
<summary> Matches fast-integer loops (up or down) </summary>
<summary> Matches expressions which are IL assembly code </summary>
<summary> Matches expressions which fetch a field from a .NET type </summary>
<summary> Matches expressions which set a field in a .NET type </summary>
<summary> Matches expressions which are conditionals </summary>
<summary> Matches expressions which are lambda abstractions </summary>
<summary> Matches expressions which are let definitions </summary>
<summary> Matches expressions which are let-rec definitions </summary>
<summary> Matches array expressions </summary>
<summary> Matches expressions which create an instance of a delegate type </summary>
<summary> Matches expressions which are calls to object constructors </summary>
<summary> Matches record expressions </summary>
<summary> Matches anonymous record expressions </summary>
<summary> Matches tuple expressions </summary>
<summary> Matches expressions which create an object corresponding to a union case </summary>
<summary> Matches expressions which are quotation literals </summary>
<summary> Matches expressions which get a field from a record or class </summary>
<summary> Matches expressions getting a field from an anonymous record. The integer represents the index into the sorted fields of the anonymous record. </summary>
<summary> Matches expressions which set a field in a record or class </summary>
<summary> Matches sequential expressions </summary>
<summary> Matches try/finally expressions </summary>
<summary> Matches try/with expressions </summary>
<summary> Matches expressions which get a value from a tuple </summary>
<summary> Matches expressions with a decision expression, each branch of which ends in DecisionTreeSuccess passing control and values to one of the targets. </summary>
module List from Microsoft.FSharp.Collections
--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T with get member IsEmpty: bool with get member Item: index: int -> 'T with get ...
<summary> Special expressions at the end of a conditional decision structure in the decision expression node of a DecisionTree . The given expressions are passed as values to the decision tree target. </summary>
<summary> Matches expressions which are type abstractions </summary>
<summary> Matches expressions which test the runtime type of a value </summary>
<summary> Matches expressions which set a field from a union case (only used in FSharp.Core itself) </summary>
<summary> Matches expressions which get a field from a union case </summary>
<summary> Matches expressions which test if an expression corresponds to a particular union case </summary>
<summary> Matches expressions which gets the tag for a union case </summary>
<summary> Matches object expressions, returning the base type, the base call, the overrides and the interface implementations </summary>
<summary> Matches expressions for an unresolved call to a trait </summary>
<summary> Matches expressions which set the contents of a mutable variable </summary>
<summary> Matches while loops </summary>
<summary> Matches expressions which are uses of the 'base' value </summary>
<summary> Matches default-value expressions, including null expressions </summary>
<summary> Matches expressions which are uses of the 'this' value </summary>
<summary> Matches constant expressions, including signed and unsigned integers, strings, characters, booleans, arrays of bytes and arrays of unit16. </summary>
<summary> Matches expressions which are uses of values </summary>
<summary> The expression that forms the body of the method </summary>