Rewrite
Overview
Rewrite applies a transformation to every node of a given kind reachable from a
Fantomas Oak — through type definitions, member bodies, patterns, types and
attributes. It is handy for cross-cutting passes that aren't tied to where you
wrote the builder: fold constants, migrate a deprecated API everywhere, wrap
calls with instrumentation, convert a union to a record, and so on.
The functions are curried with the target last, so they drop into the usual
pipeline. The WidgetBuilder<Oak> forms return the same builder when nothing
changed (reference equality is preserved on unchanged subtrees).
Function |
Signature |
|---|---|
|
|
|
|
|
|
|
|
#r "../../src/Fabulous.AST/bin/Release/netstandard2.1/publish/Fantomas.Core.dll"
#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 Fantomas.Core.SyntaxOak
open Fantomas.FCS.Text
open type Fabulous.AST.Ast
Rewriting expressions
Rewrite.expr applies your function to every Expr in the tree (bottom-up,
children first). That makes it the right tool for cross-cutting changes that
aren't tied to any single builder call — the kind of thing other ecosystems
reach for AST passes to do. A couple of small helpers for building raw Expr
nodes, used by the examples below:
let ident(name: string) : Expr =
Expr.Constant(Constant.FromText(SingleTextNode(name, Range.Zero)))
let intExpr(value: int) : Expr =
Expr.Constant(Constant.FromText(SingleTextNode(string value, Range.Zero)))
let (|IntLit|_|)(e: Expr) : int option =
match e with
| Expr.Constant(Constant.FromText n) ->
match System.Int32.TryParse n.Text with
| true, v -> Some v
| _ -> None
| _ -> None
Optimize: constant folding
Generators often emit naive, uniform expressions (x * 1 + 0) because that is
the easiest thing to produce from data. A folding pass tidies them up afterwards
— the same idea as an optimizing compiler's constant-folding pass (LLVM, GCC) or
a JavaScript minifier. The generator stays simple; the cleanup is one reusable
pass over the result. Because the walk is bottom-up, nested redundancy collapses
in a single pass.
let foldConstants(e: Expr) : Expr =
match e with
| Expr.InfixApp n ->
match n.Operator.Text, n.LeftHandSide, n.RightHandSide with
| "+", lhs, IntLit 0
| "+", IntLit 0, lhs
| "*", lhs, IntLit 1
| "*", IntLit 1, lhs -> lhs
| "+", IntLit a, IntLit b -> intExpr(a + b)
| "*", IntLit a, IntLit b -> intExpr(a * b)
| _ -> e
| _ -> e
Oak() {
AnonymousModule() {
Value("area", InfixAppExpr(InfixAppExpr(ConstantExpr(Constant "width"), "*", Int(1)), "+", Int(0)))
Value("total", InfixAppExpr(Int(2), "+", Int(3)))
}
}
|> Rewrite.expr foldConstants
|> Gen.mkOak
|> Gen.run
|> printfn "%s"
// produces the following code:
|
Migrate an API (codemod)
Rewrite a non-idiomatic call into its better form wherever it appears — the same
job a jscodeshift React codemod, a
Scalafix rule, or go fix does. Here List.length xs = 0 (which is O(n)) becomes
List.isEmpty xs (O(1)) across the whole tree:
let useIsEmpty(e: Expr) : Expr =
match e with
| Expr.InfixApp n when n.Operator.Text = "=" ->
match n.LeftHandSide, n.RightHandSide with
| Expr.App app, IntLit 0 ->
match app.FunctionExpr with
| Expr.Constant(Constant.FromText f) when f.Text = "List.length" ->
Expr.App(ExprAppNode(ident "List.isEmpty", List.ofSeq app.Arguments, Range.Zero))
| _ -> e
| _ -> e
| _ -> e
Oak() {
AnonymousModule() {
Value(
"isEmpty",
InfixAppExpr(AppExpr(ConstantExpr(Constant "List.length"), [ ConstantExpr(Constant "xs") ]), "=", Int(0))
)
}
}
|> Rewrite.expr useIsEmpty
|> Gen.mkOak
|> Gen.run
|> printfn "%s"
// produces the following code:
|
Instrument calls
Weave a cross-cutting concern into generated code without touching every call
site — the way OpenTelemetry's auto-instrumentation or an AspectJ aspect does.
Here every compute call is wrapped with a timing helper. (No infinite loop: the
walk is a single pass, so the newly-created withTiming node is not revisited.)
let instrument(e: Expr) : Expr =
match e with
| Expr.App app ->
match app.FunctionExpr with
| Expr.Constant(Constant.FromText f) when f.Text = "compute" ->
let wrapped =
Expr.Paren(
ExprParenNode(SingleTextNode("(", Range.Zero), e, SingleTextNode(")", Range.Zero), Range.Zero)
)
Expr.App(ExprAppNode(ident "withTiming", [ wrapped ], Range.Zero))
| _ -> e
| _ -> e
Oak() {
AnonymousModule() { Value("result", AppExpr(ConstantExpr(Constant "compute"), [ ConstantExpr(Constant "input") ])) }
}
|> Rewrite.expr instrument
|> Gen.mkOak
|> Gen.run
|> printfn "%s"
// produces the following code:
|
Rewriting type definitions
Rewrite.typeDefn visits each TypeDefn, so a pass can replace one shape with
another. This converts a single-case union into the equivalent record by reusing
the case's fields (a UnionCaseNode's Fields are already FieldNodes):
let unionToRecord(td: TypeDefn) : TypeDefn =
match td with
| TypeDefn.Union n when n.UnionCases.Length = 1 ->
let itd = n :> ITypeDefn
TypeDefn.Record(
TypeDefnRecordNode(
itd.TypeName,
n.Accessibility,
SingleTextNode("{", Range.Zero),
n.UnionCases.Head.Fields,
SingleTextNode("}", Range.Zero),
itd.Members,
Range.Zero
)
)
| _ -> td
Oak() { AnonymousModule() { Union("Point") { UnionCase("Point", [ Field("X", Int()); Field("Y", Int()) ]) } } }
|> Rewrite.typeDefn unionToRecord
|> Gen.mkOak
|> Gen.run
|> printfn "%s"
// produces the following code:
|
Working with raw Oak nodes
When you already hold a raw Oak (rather than a WidgetBuilder<Oak>), use the
InOak variants. They take the same transformation and return a rewritten Oak:
let oak = Gen.mkOak widget
let optimized = oak |> Rewrite.exprInOak foldConstants
let asRecord = oak |> Rewrite.typeDefnInOak unionToRecord
val string: value: 'T -> string
--------------------
type string = System.String
union case TextSegment.Expr: WidgetBuilder<FillExprNode> * int -> TextSegment
--------------------
module Expr from Fabulous.AST
--------------------
type Expr = | Lazy of ExprLazyNode | Single of ExprSingleNode | Constant of Constant | Null of SingleTextNode | Quote of ExprQuoteNode | Typed of ExprTypedNode | New of ExprNewNode | Tuple of ExprTupleNode | StructTuple of ExprStructTupleNode | ArrayOrList of ExprArrayOrListNode ... static member Node: x: Expr -> Node member HasParentheses: bool
static member Ast.Constant: value: string -> WidgetBuilder<Constant>
--------------------
module Constant from Fabulous.AST
--------------------
type Constant = | FromText of SingleTextNode | Unit of UnitNode | Measure of ConstantMeasureNode static member Node: c: Constant -> NodeBase
module SingleTextNode from Fabulous.AST
--------------------
type SingleTextNode = inherit NodeBase new: idText: string * range: range -> SingleTextNode override Children: Node array member Text: string
--------------------
new: idText: string * range: range -> SingleTextNode
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 ...
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
<summary>Represents a 32-bit signed integer.</summary>
System.Int32.TryParse(s: System.ReadOnlySpan<char>, result: byref<int>) : bool
System.Int32.TryParse(utf8Text: System.ReadOnlySpan<byte>, result: byref<int>) : bool
System.Int32.TryParse(s: string, provider: System.IFormatProvider, result: byref<int>) : bool
System.Int32.TryParse(s: System.ReadOnlySpan<char>, provider: System.IFormatProvider, result: byref<int>) : bool
System.Int32.TryParse(utf8Text: System.ReadOnlySpan<byte>, provider: System.IFormatProvider, result: byref<int>) : bool
System.Int32.TryParse(s: string, style: System.Globalization.NumberStyles, provider: System.IFormatProvider, result: byref<int>) : bool
System.Int32.TryParse(s: System.ReadOnlySpan<char>, style: System.Globalization.NumberStyles, provider: System.IFormatProvider, result: byref<int>) : bool
System.Int32.TryParse(utf8Text: System.ReadOnlySpan<byte>, style: System.Globalization.NumberStyles, provider: System.IFormatProvider, result: byref<int>) : bool
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)
static member Ast.InfixAppExpr: lhs: WidgetBuilder<Expr> * operator: string * rhs: WidgetBuilder<Constant> -> WidgetBuilder<Expr>
static member Ast.InfixAppExpr: lhs: string * operator: string * rhs: string -> WidgetBuilder<Expr>
static member Ast.InfixAppExpr: lhs: string * operator: string * rhs: WidgetBuilder<Constant> -> WidgetBuilder<Expr>
static member Ast.InfixAppExpr: lhs: WidgetBuilder<Constant> * operator: string * rhs: WidgetBuilder<Constant> -> WidgetBuilder<Expr>
static member Ast.InfixAppExpr: lhs: string * operator: string * rhs: WidgetBuilder<Expr> -> WidgetBuilder<Expr>
static member Ast.InfixAppExpr: lhs: WidgetBuilder<Constant> * operator: string * rhs: WidgetBuilder<Expr> -> WidgetBuilder<Expr>
static member Ast.InfixAppExpr: lhs: WidgetBuilder<Expr> * operator: string * rhs: WidgetBuilder<Expr> -> WidgetBuilder<Expr>
static member Ast.ConstantExpr: value: WidgetBuilder<Constant> -> WidgetBuilder<Expr>
static member Ast.Int: unit -> WidgetBuilder<Type>
<summary> Bottom-up rewriter for expressions inside an Oak tree. Visits every <see cref="T:Fantomas.Core.SyntaxOak.Expr" /> reachable from the input (including those nested inside type definitions, member bodies, patterns, types and attributes) and applies the caller-supplied function after each node's children are rebuilt. Reference equality is preserved on unchanged subtrees, so a no-op rewrite returns the same Oak instance. Curried with the target last, so it drops into a pipeline: <c>widget |> Rewrite.expr f |> Gen.run</c>. </summary>
<summary> Rewrites every Expr in the Oak produced by <paramref name="widget" />, returning the same <c>WidgetBuilder</c> when nothing changed. </summary>
<summary> Renders a widget tree to F# source and verifies the result. <c>mkOak</c> turns the root widget into a Fantomas node (recursively building its children), <c>run</c> formats that node to source (optionally with a config), and <c>parse</c> round-trips the source back through the parser to confirm it is syntactically valid. Designed for pipeline use, e.g. <c>widget |> Gen.parse</c> or <c>widget |> Gen.mkOak |> Gen.run |> Gen.parse</c>. </summary>
static member Gen.run: oak: Oak * config: Fantomas.Core.FormatConfig -> string
type ExprAppNode = inherit NodeBase new: functionExpr: Expr * arguments: Expr list * range: range -> ExprAppNode member Arguments: Expr list override Children: Node array member FunctionExpr: Expr
--------------------
new: functionExpr: Expr * arguments: Expr list * range: range -> ExprAppNode
static member Ast.List: unit -> WidgetBuilder<Type>
--------------------
module List from Fabulous.AST
--------------------
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 member IsEmpty: bool member Item: index: int -> 'T with get ...
(+0 other overloads)
static member Ast.AppExpr: name: WidgetBuilder<Constant> -> WidgetBuilder<Expr>
(+0 other overloads)
static member Ast.AppExpr: name: WidgetBuilder<Expr> -> WidgetBuilder<Expr>
(+0 other overloads)
static member Ast.AppExpr: name: string * item: string -> WidgetBuilder<Expr>
(+0 other overloads)
static member Ast.AppExpr: name: WidgetBuilder<Constant> * item: string -> WidgetBuilder<Expr>
(+0 other overloads)
static member Ast.AppExpr: name: WidgetBuilder<Expr> * item: string -> WidgetBuilder<Expr>
(+0 other overloads)
static member Ast.AppExpr: name: string * item: WidgetBuilder<Constant> -> WidgetBuilder<Expr>
(+0 other overloads)
static member Ast.AppExpr: name: WidgetBuilder<Constant> * item: WidgetBuilder<Constant> -> WidgetBuilder<Expr>
(+0 other overloads)
static member Ast.AppExpr: name: WidgetBuilder<Expr> * item: WidgetBuilder<Constant> -> WidgetBuilder<Expr>
(+0 other overloads)
static member Ast.AppExpr: name: string * item: WidgetBuilder<Expr> -> WidgetBuilder<Expr>
(+0 other overloads)
type ExprParenNode = inherit NodeBase new: openingParen: SingleTextNode * expr: Expr * closingParen: SingleTextNode * range: range -> ExprParenNode override Children: Node array member ClosingParen: SingleTextNode member Expr: Expr member OpeningParen: SingleTextNode
--------------------
new: openingParen: SingleTextNode * expr: Expr * closingParen: SingleTextNode * range: range -> ExprParenNode
static member Ast.TypeDefn: name: string -> CollectionBuilder<TypeDefn,MemberDefn>
static member Ast.TypeDefn: name: string * constructor: WidgetBuilder<ImplicitConstructorNode> -> CollectionBuilder<TypeDefn,MemberDefn>
static member Ast.TypeDefn: name: string * constructor: WidgetBuilder<Pattern> -> CollectionBuilder<TypeDefn,MemberDefn>
--------------------
module TypeDefn from Fabulous.AST
<summary> Common attributes shared by all TypeDefn widgets </summary>
--------------------
type TypeDefn = | Enum of TypeDefnEnumNode | Union of TypeDefnUnionNode | Record of TypeDefnRecordNode | None of TypeNameNode | Abbrev of TypeDefnAbbrevNode | Explicit of TypeDefnExplicitNode | Augmentation of TypeDefnAugmentationNode | Delegate of TypeDefnDelegateNode | Regular of TypeDefnRegularNode static member Node: x: TypeDefn -> Node static member TypeDefnNode: x: TypeDefn -> ITypeDefn
type TypeDefnRecordNode = inherit NodeBase interface ITypeDefn new: typeNameNode: TypeNameNode * accessibility: SingleTextNode option * openingBrace: SingleTextNode * fields: FieldNode list * closingBrace: SingleTextNode * members: MemberDefn list * range: range -> TypeDefnRecordNode member Accessibility: SingleTextNode option override Children: Node array member ClosingBrace: SingleTextNode member Fields: FieldNode list member OpeningBrace: SingleTextNode
--------------------
new: typeNameNode: TypeNameNode * accessibility: SingleTextNode option * openingBrace: SingleTextNode * fields: FieldNode list * closingBrace: SingleTextNode * members: MemberDefn list * range: range -> TypeDefnRecordNode
static member Ast.Union: name: string -> CollectionBuilder<TypeDefn,UnionCaseNode>
--------------------
module Union from Fabulous.AST
static member Ast.UnionCase: name: string * fields: (string * WidgetBuilder<Type>) seq -> WidgetBuilder<UnionCaseNode>
static member Ast.UnionCase: name: string * fields: (string * string) seq -> WidgetBuilder<UnionCaseNode>
static member Ast.UnionCase: name: string * field: string -> WidgetBuilder<UnionCaseNode>
static member Ast.UnionCase: name: string * field: WidgetBuilder<FieldNode> -> WidgetBuilder<UnionCaseNode>
static member Ast.UnionCase: name: string * fields: string seq -> WidgetBuilder<UnionCaseNode>
static member Ast.UnionCase: name: string * field: WidgetBuilder<Type> -> WidgetBuilder<UnionCaseNode>
static member Ast.UnionCase: name: string * fields: WidgetBuilder<Type> seq -> WidgetBuilder<UnionCaseNode>
static member Ast.UnionCase: name: string * fields: WidgetBuilder<FieldNode> seq -> WidgetBuilder<UnionCaseNode>
static member Ast.UnionCase: name: string -> WidgetBuilder<UnionCaseNode>
--------------------
module UnionCase from Fabulous.AST
static member Ast.Field: name: string * fieldType: string -> WidgetBuilder<FieldNode>
static member Ast.Field: name: string * fieldType: WidgetBuilder<Type> -> WidgetBuilder<FieldNode>
static member Ast.Field: fieldType: string -> WidgetBuilder<FieldNode>
static member Ast.Field: fieldType: WidgetBuilder<Type> -> WidgetBuilder<FieldNode>
--------------------
module Field from Fabulous.AST
<summary> Rewrites every TypeDefn in the Oak produced by <paramref name="widget" />, returning the same <c>WidgetBuilder</c> when nothing changed. Unlike <see cref="M:Fabulous.AST.Rewrite.expr" /> this visits type definitions themselves, so a pass can replace one shape with another (e.g. a union with a record). </summary>
Fabulous.AST