Fabulous.AST
Contents
- What is AST?
- Why Use AST for Code Generation?
- Code Generation Approaches
- Getting Started
- Documentation Structure
What is AST?
AST
stands for Abstract Syntax Tree. It is a tree representation of the abstract syntactic structure of source code written in a programming language.
It is used by compilers to analyze, transform, and generate code.
Why Use AST for Code Generation?
You can generate code by just using strings and string interpolation. But there are several reasons why you should not do that:
- It's error-prone and hard to maintain.
- If the code you are generating is complex, then it's even harder to generate it using string interpolation.
- You will have to write extra code to handle edge cases, ensure the generated code is valid, and manage formatting and indentation.
ASTs offer several benefits for code generation:
- The code generated is compliant with the F# syntax and will most likely be valid and compilable.
- It provides a more structured way to generate code.
- It allows you to manipulate the code programmatically.
- It enables you to generate more complex code in a more maintainable way.
Code Generation Approaches
String Interpolation
This is a simple example of generating code using StringBuilder
and string interpolation:
open System.Text
let code = StringBuilder()
code.AppendLine("module MyModule =")
code.AppendLine(" let x = 12")
code |> string |> printfn "%s"
// produces the following code:
|
Quote from fantomas: For mercy's sake don't use string concatenation when generating F# code, use Fantomas instead. It is battle tested and proven technology!
Compiler AST
The official F# compiler AST:
- It is very verbose and hard to read.
- It is difficult to manipulate or analyze programmatically.
- Contains a lot of information that is not relevant to the code itself.
You can see a live example using Fantomas tools
open Fantomas.FCS.Syntax
open Fantomas.FCS.SyntaxTrivia
open Fantomas.FCS.Text
open Fantomas.FCS.Xml
#r "../src/Fabulous.AST/bin/Release/netstandard2.1/publish/Fantomas.FCS.dll"
#r "../src/Fabulous.AST/bin/Release/netstandard2.1/publish/Fantomas.Core.dll"
ParsedInput.ImplFile(
ParsedImplFileInput.ParsedImplFileInput(
fileName = "tmp.fsx",
isScript = true,
qualifiedNameOfFile = QualifiedNameOfFile.QualifiedNameOfFile(Ident("Tmp$fsx", Range.Zero)),
scopedPragmas = [],
hashDirectives = [],
contents =
[ SynModuleOrNamespace.SynModuleOrNamespace(
longId = [ Ident("Tmp", Range.Zero) ],
isRecursive = false,
kind = SynModuleOrNamespaceKind.AnonModule,
decls =
[ SynModuleDecl.Let(
isRecursive = false,
bindings =
[ SynBinding.SynBinding(
accessibility = None,
kind = SynBindingKind.Normal,
isInline = false,
isMutable = false,
attributes = [],
xmlDoc = PreXmlDoc.Empty,
valData =
SynValData.SynValData(
memberFlags = None,
valInfo =
SynValInfo.SynValInfo(
curriedArgInfos = [],
returnInfo =
SynArgInfo.SynArgInfo(
attributes = [],
optional = false,
ident = None
)
),
thisIdOpt = None
),
headPat =
SynPat.Named(
ident = SynIdent.SynIdent(ident = Ident("x", Range.Zero), trivia = None),
isThisVal = false,
accessibility = None,
range = Range.Zero
),
returnInfo = None,
expr = SynExpr.Const(constant = SynConst.Int32(12), range = Range.Zero),
range = Range.Zero,
debugPoint = DebugPointAtBinding.Yes(Range.Zero),
trivia =
{ LeadingKeyword = SynLeadingKeyword.Let(Range.Zero)
InlineKeyword = None
EqualsRange = Some(Range.Zero) }
) ],
range = Range.Zero
) ],
xmlDoc = PreXmlDoc.Empty,
attribs = [],
accessibility = None,
range = Range.Zero,
trivia = { LeadingKeyword = SynModuleOrNamespaceLeadingKeyword.None }
) ],
flags = (false, false),
trivia =
{ ConditionalDirectives = []
CodeComments = [] },
identifiers = set []
)
)
// produces the following code:
let x = 12
Fantomas Oak AST
It is a simplified version of the official AST that is used by Fantomas to format F# code.
- It is more concise and easier to read.
- It is somewhat easier to manipulate or analyze programmatically.
- It is more human-readable, as it contains only the relevant information about the code itself.
- However, it still requires providing many optional values even for simple code.
You can see a live example using the online tool
#r "../src/Fabulous.AST/bin/Release/netstandard2.1/publish/Fantomas.FCS.dll"
#r "../src/Fabulous.AST/bin/Release/netstandard2.1/publish/Fantomas.Core.dll"
open Fantomas.FCS.Text
open Fantomas.Core
open Fantomas.Core.SyntaxOak
Oak(
[],
[ ModuleOrNamespaceNode(
None,
[ BindingNode(
None,
None,
MultipleTextsNode([ SingleTextNode("let", Range.Zero) ], Range.Zero),
false,
None,
None,
Choice1Of2(IdentListNode([ IdentifierOrDot.Ident(SingleTextNode("x", Range.Zero)) ], Range.Zero)),
None,
[],
None,
SingleTextNode("=", Range.Zero),
Expr.Constant(Constant.FromText(SingleTextNode("12", Range.Zero))),
Range.Zero
)
|> ModuleDecl.TopLevelBinding ],
Range.Zero
) ],
Range.Zero
)
|> CodeFormatter.FormatOakAsync
|> Async.RunSynchronously
|> printfn "%s"
// produces the following code:
|
Fabulous.AST DSL
Fabulous.AST provides a more user-friendly API to generate code using ASTs. It's built on top of Fantomas Oak AST and offers a more concise and easier-to-use API for code generation. It dramatically reduces the boilerplate code required to generate F# code.
You can configure code formatting using FormatConfig
:
type FormatConfig =
{ IndentSize: Num
MaxLineLength: Num
EndOfLine: EndOfLineStyle
InsertFinalNewline: bool
SpaceBeforeParameter: bool
SpaceBeforeLowercaseInvocation: bool
SpaceBeforeUppercaseInvocation: bool
SpaceBeforeClassConstructor: bool
SpaceBeforeMember: bool
SpaceBeforeColon: bool }
Here's the same example using Fabulous.AST:
#r "../src/Fabulous.AST/bin/Release/netstandard2.1/publish/Fabulous.AST.dll"
#r "../src/Fabulous.AST/bin/Release/netstandard2.1/publish/Fantomas.FCS.dll"
open Fabulous.AST
open type Fabulous.AST.Ast
Oak() { AnonymousModule() { Value("x", "12") } }
|> Gen.mkOak
|> Gen.run
|> printfn "%s"
// produces the following code:
|
Getting Started
To start using Fabulous.AST, first install the NuGet package:
dotnet add package Fabulous.AST
Then, in your code:
open Fabulous.AST
open type Fabulous.AST.Ast
// Generate a simple module with a value
let code =
Oak() {
AnonymousModule() {
Value("x", Int(42))
}
}
|> Gen.mkOak
|> Gen.run
printfn "%s" code
Documentation Structure
Fabulous.AST documentation is organized by F# language features. Each page focuses on one specific aspect of F# code generation:
- Modules and Namespaces: How to create and organize code into modules and namespaces
- Records: Creating and working with record types
- Discriminated Unions: Generating sum types with different cases
- Functions: Defining and manipulating functions
- Classes: Creating classes with members and constructors
- Expressions: Building various expression types
- Patterns: Working with pattern matching
- Type Definitions: Creating different kinds of types
- Units of Measure: Defining and using units of measure
Each documentation page follows a consistent structure:
- Overview: Brief explanation of the concept
- Basic Usage: Simple examples of the most common usage
- API Reference: Available widget constructors and modifiers
- Examples: Practical examples showing different use cases
- Advanced Topics: More complex scenarios and customizations
What Widgets to Use to Generate Code?
Fabulous.AST maps to the Fantomas Oak AST nodes. You can use the online tool to examine the AST nodes and then use the corresponding widgets to generate the code.
For example, the following Oak AST node:
Oak (1,0-1,10)
ModuleOrNamespaceNode (1,0-1,10)
BindingNode (1,0-1,10)
MultipleTextsNode (1,0-1,3)
let (1,0-1,3)
IdentListNode (1,4-1,5)
x (1,4-1,5)
= (1,6-1,7)
12 (1,8-1,10)
Translates to the following Fabulous.AST code:
Oak() {
AnonymousModule() {
Value("x", "12")
}
}
We've reduced the boilerplate code from 70 lines to just 5 lines of code, making it much easier to read and understand.
type StringBuilder = interface ISerializable new: unit -> unit + 5 overloads member Append: value: bool -> StringBuilder + 25 overloads member AppendFormat: provider: IFormatProvider * format: string * arg0: obj -> StringBuilder + 12 overloads member AppendJoin: separator: char * [<ParamArray>] values: obj array -> StringBuilder + 5 overloads member AppendLine: unit -> StringBuilder + 3 overloads member Clear: unit -> StringBuilder member CopyTo: sourceIndex: int * destination: char array * destinationIndex: int * count: int -> unit + 1 overload member EnsureCapacity: capacity: int -> int member Equals: span: ReadOnlySpan<char> -> bool + 1 overload ...
<summary>Represents a mutable string of characters. This class cannot be inherited.</summary>
--------------------
StringBuilder() : StringBuilder
StringBuilder(capacity: int) : StringBuilder
StringBuilder(value: string) : StringBuilder
StringBuilder(capacity: int, maxCapacity: int) : StringBuilder
StringBuilder(value: string, capacity: int) : StringBuilder
StringBuilder(value: string, startIndex: int, length: int, capacity: int) : StringBuilder
StringBuilder.AppendLine(handler: byref<StringBuilder.AppendInterpolatedStringHandler>) : StringBuilder
StringBuilder.AppendLine(value: string) : StringBuilder
StringBuilder.AppendLine(provider: System.IFormatProvider, handler: byref<StringBuilder.AppendInterpolatedStringHandler>) : StringBuilder
val string: value: 'T -> string
--------------------
type string = System.String
union case ParsedImplFileInput.ParsedImplFileInput: fileName: string * isScript: bool * qualifiedNameOfFile: QualifiedNameOfFile * scopedPragmas: ScopedPragma list * hashDirectives: ParsedHashDirective list * contents: SynModuleOrNamespace list * flags: bool * bool * trivia: ParsedImplFileInputTrivia * identifiers: Set<string> -> ParsedImplFileInput
--------------------
type ParsedImplFileInput = | ParsedImplFileInput of fileName: string * isScript: bool * qualifiedNameOfFile: QualifiedNameOfFile * scopedPragmas: ScopedPragma list * hashDirectives: ParsedHashDirective list * contents: SynModuleOrNamespace list * flags: bool * bool * trivia: ParsedImplFileInputTrivia * identifiers: Set<string> member Contents: SynModuleOrNamespace list member FileName: string member HashDirectives: ParsedHashDirective list member IsExe: bool member IsLastCompiland: bool member IsScript: bool member QualifiedName: QualifiedNameOfFile member ScopedPragmas: ScopedPragma list member Trivia: ParsedImplFileInputTrivia
union case QualifiedNameOfFile.QualifiedNameOfFile: Ident -> QualifiedNameOfFile
--------------------
type QualifiedNameOfFile = | QualifiedNameOfFile of Ident member Id: Ident member Range: range member Text: string
[<Struct>] type Ident = new: text: string * range: range -> Ident member idRange: range member idText: string
--------------------
Ident ()
new: text: string * range: range -> Ident
module Range from Fantomas.FCS.Text
--------------------
[<Struct>] type Range = member End: pos member EndColumn: int member EndLine: int member EndRange: range member FileName: string member IsSynthetic: bool member Start: pos member StartColumn: int member StartLine: int member StartRange: range ...
union case SynModuleOrNamespace.SynModuleOrNamespace: longId: LongIdent * isRecursive: bool * kind: SynModuleOrNamespaceKind * decls: SynModuleDecl list * xmlDoc: PreXmlDoc * attribs: SynAttributes * accessibility: SynAccess option * range: range * trivia: SynModuleOrNamespaceTrivia -> SynModuleOrNamespace
--------------------
type SynModuleOrNamespace = | SynModuleOrNamespace of longId: LongIdent * isRecursive: bool * kind: SynModuleOrNamespaceKind * decls: SynModuleDecl list * xmlDoc: PreXmlDoc * attribs: SynAttributes * accessibility: SynAccess option * range: range * trivia: SynModuleOrNamespaceTrivia member Range: range
union case SynBinding.SynBinding: 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 -> 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
union case SynValData.SynValData: memberFlags: SynMemberFlags option * valInfo: SynValInfo * thisIdOpt: Ident option -> SynValData
--------------------
type SynValData = | SynValData of memberFlags: SynMemberFlags option * valInfo: SynValInfo * thisIdOpt: Ident option member SynValInfo: SynValInfo
union case SynValInfo.SynValInfo: curriedArgInfos: SynArgInfo list list * returnInfo: SynArgInfo -> SynValInfo
--------------------
type SynValInfo = | SynValInfo of curriedArgInfos: SynArgInfo list list * returnInfo: SynArgInfo member ArgNames: string list member CurriedArgInfos: SynArgInfo list list
union case SynArgInfo.SynArgInfo: attributes: SynAttributes * optional: bool * ident: Ident option -> SynArgInfo
--------------------
type SynArgInfo = | SynArgInfo of attributes: SynAttributes * optional: bool * ident: Ident option member Attributes: SynAttributes member Ident: Ident option
union case SynIdent.SynIdent: ident: Ident * trivia: IdentTrivia option -> SynIdent
--------------------
type SynIdent = | SynIdent of ident: Ident * trivia: IdentTrivia option member Range: range
type Oak = inherit NodeBase new: parsedHashDirectives: ParsedHashDirectiveNode list * modulesOrNamespaces: ModuleOrNamespaceNode list * m: range -> Oak override Children: Node array member ModulesOrNamespaces: ModuleOrNamespaceNode list member ParsedHashDirectives: ParsedHashDirectiveNode list
--------------------
new: parsedHashDirectives: ParsedHashDirectiveNode list * modulesOrNamespaces: ModuleOrNamespaceNode list * m: range -> Oak
type ModuleOrNamespaceNode = inherit NodeBase new: header: ModuleOrNamespaceHeaderNode option * decls: ModuleDecl list * range: range -> ModuleOrNamespaceNode override Children: Node array member Declarations: ModuleDecl list member Header: ModuleOrNamespaceHeaderNode option member IsNamed: bool
--------------------
new: header: ModuleOrNamespaceHeaderNode option * decls: ModuleDecl list * range: range -> ModuleOrNamespaceNode
type BindingNode = inherit NodeBase new: xmlDoc: XmlDocNode option * attributes: MultipleAttributeListNode option * leadingKeyword: MultipleTextsNode * isMutable: bool * inlineNode: SingleTextNode option * accessibility: SingleTextNode option * functionName: Choice<IdentListNode,Pattern> * genericTypeParameters: TyparDecls option * parameters: Pattern list * returnType: BindingReturnInfoNode option * equals: SingleTextNode * expr: Expr * range: range -> BindingNode member Accessibility: SingleTextNode option member Attributes: MultipleAttributeListNode option override Children: Node array member Equals: SingleTextNode member Expr: Expr member FunctionName: Choice<IdentListNode,Pattern> member GenericTypeParameters: TyparDecls option member Inline: SingleTextNode option ...
--------------------
new: xmlDoc: XmlDocNode option * attributes: MultipleAttributeListNode option * leadingKeyword: MultipleTextsNode * isMutable: bool * inlineNode: SingleTextNode option * accessibility: SingleTextNode option * functionName: Choice<IdentListNode,Pattern> * genericTypeParameters: TyparDecls option * parameters: Pattern list * returnType: BindingReturnInfoNode option * equals: SingleTextNode * expr: Expr * range: range -> BindingNode
type MultipleTextsNode = inherit NodeBase new: content: SingleTextNode list * range: range -> MultipleTextsNode override Children: Node array member Content: SingleTextNode list
--------------------
new: content: SingleTextNode list * range: range -> MultipleTextsNode
type SingleTextNode = inherit NodeBase new: idText: string * range: range -> SingleTextNode override Children: Node array member Text: string
--------------------
new: idText: string * range: range -> SingleTextNode
type IdentListNode = inherit NodeBase new: content: IdentifierOrDot list * range: range -> IdentListNode override Children: Node array member Content: IdentifierOrDot list member IsEmpty: bool static member Empty: IdentListNode
--------------------
new: content: IdentifierOrDot list * range: range -> IdentListNode
static member CodeFormatter.FormatOakAsync: oak: Oak * config: FormatConfig -> Async<string>
module Async from Fantomas.Core
--------------------
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 Ast.Oak: unit -> CollectionBuilder<Oak,'marker>
--------------------
module Oak from Fabulous.AST
--------------------
type Oak = inherit NodeBase new: parsedHashDirectives: ParsedHashDirectiveNode list * modulesOrNamespaces: ModuleOrNamespaceNode list * m: range -> Oak override Children: Node array member ModulesOrNamespaces: ModuleOrNamespaceNode list member ParsedHashDirectives: ParsedHashDirectiveNode list
--------------------
new: parsedHashDirectives: ParsedHashDirectiveNode list * modulesOrNamespaces: ModuleOrNamespaceNode list * m: range -> Oak
(+0 other overloads)
static member Ast.Value: name: string * value: string * returnType: WidgetBuilder<Type> -> WidgetBuilder<BindingNode>
(+0 other overloads)
static member Ast.Value: name: string * value: string -> WidgetBuilder<BindingNode>
(+0 other overloads)
static member Ast.Value: name: string * value: WidgetBuilder<Constant> * returnType: string -> WidgetBuilder<BindingNode>
(+0 other overloads)
static member Ast.Value: name: string * value: WidgetBuilder<Constant> * returnType: WidgetBuilder<Type> -> WidgetBuilder<BindingNode>
(+0 other overloads)
static member Ast.Value: name: string * value: WidgetBuilder<Constant> -> WidgetBuilder<BindingNode>
(+0 other overloads)
static member Ast.Value: name: string * value: WidgetBuilder<Expr> * returnType: string -> WidgetBuilder<BindingNode>
(+0 other overloads)
static member Ast.Value: name: string * value: WidgetBuilder<Expr> * returnType: WidgetBuilder<Type> -> WidgetBuilder<BindingNode>
(+0 other overloads)
static member Ast.Value: name: string * value: WidgetBuilder<Expr> -> WidgetBuilder<BindingNode>
(+0 other overloads)
static member Ast.Value: name: WidgetBuilder<Constant> * value: WidgetBuilder<Constant> * returnType: string -> WidgetBuilder<BindingNode>
(+0 other overloads)
<summary> It takes the root of the widget tree and create the corresponding Fantomas node, and recursively creating all children nodes </summary>