Skip to content

Commit a63ded8

Browse files
OnurGumusclaude
andcommitted
[JS/TS] Add F# quotation support with serializable QuotExpr AST
Add support for F# code quotations (<@ expr @> and <@@ expr @@>) targeting JS/TS. Quotations compile to a serializable QuotExpr discriminated union that works with Thoth.Json and Fable.Remoting for cross-platform serialization between Fable clients and .NET backends. Supported quotation nodes: values (int, float, string, char, bool, unit), lambdas, let bindings, if/then/else, function calls, operators, tuples, unions, records, options, lists, sequential, variable set, field get/set. The % splice operator is supported for composing quotations. New files: - Fable.Core.Quotations.fs: shared F# DU types for .NET deserialization - Quotation.ts: JS/TS runtime with proper Union subclasses - QuotationEmitter.fs: compiler transform from Fable Quote AST to runtime calls - QuotationTests.fs: test suite Other targets (Python, Rust, Dart, Beam, PHP) have stubs with error messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 00f2663 commit a63ded8

24 files changed

Lines changed: 1220 additions & 8 deletions

src/Fable.AST/Fable.fs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,9 @@ type Expr =
834834
| TryCatch of body: Expr * catch: (Ident * Expr) option * finalizer: Expr option * range: SourceLocation option
835835
| IfThenElse of guardExpr: Expr * thenExpr: Expr * elseExpr: Expr * range: SourceLocation option
836836

837+
// Quotations
838+
| Quote of quotedExpr: Expr * isTyped: bool
839+
837840
| Unresolved of expr: UnresolvedExpr * typ: Type * range: SourceLocation option
838841
| Extended of expr: ExtendedSet * range: SourceLocation option
839842

@@ -853,6 +856,7 @@ type Expr =
853856
| Get(_, _, t, _)
854857
| Emit(_, t, _)
855858
| DecisionTreeSuccess(_, _, t) -> t
859+
| Quote _ -> Any
856860
| Set _
857861
| WhileLoop _
858862
| ForLoop _ -> Unit
@@ -874,7 +878,8 @@ type Expr =
874878
| Let _
875879
| LetRec _
876880
| DecisionTree _
877-
| DecisionTreeSuccess _ -> None
881+
| DecisionTreeSuccess _
882+
| Quote _ -> None
878883
| Lambda(_, e, _)
879884
| Delegate(_, e, _, _)
880885
| TypeCast(e, _) -> e.Range

src/Fable.Cli/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Added
11+
12+
* [JS/TS] Add F# quotation support with serializable `QuotExpr` AST (construction, pattern matching, JSON serialization)
13+
1014
## 5.0.0-rc.6 - 2026-03-31
1115

1216
### Fixed

src/Fable.Compiler/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Added
11+
12+
* [JS/TS] Add F# quotation support with serializable `QuotExpr` AST (construction, pattern matching, JSON serialization)
13+
1014
## 5.0.0-rc.12 - 2026-03-31
1115

1216
### Fixed

src/Fable.Core/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Added
11+
12+
* Add `Fable.Core.Quotations` module with `QuotExpr`, `QuotVar`, `QuotLiteral` types for cross-platform quotation serialization
13+
1014
## 5.0.0-rc.1 - 2026-02-26
1115

1216
### Added
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
namespace Fable.Core.Quotations
2+
3+
/// Serializable representation of literal values in quotations.
4+
/// All cases use primitive types for reliable cross-platform JSON serialization.
5+
[<RequireQualifiedAccess>]
6+
type QuotLiteral =
7+
| Bool of bool
8+
| SByte of sbyte
9+
| Byte of byte
10+
| Int16 of int16
11+
| UInt16 of uint16
12+
| Int of int
13+
| UInt32 of uint32
14+
| Int64 of int64
15+
| UInt64 of uint64
16+
| Float32 of float32
17+
| Float of float
18+
| Char of char
19+
| String of string
20+
| Decimal of decimal
21+
| Unit
22+
| Null of typeName: string
23+
24+
/// Serializable representation of an F# quotation variable.
25+
type QuotVar =
26+
{
27+
Name: string
28+
Type: string
29+
IsMutable: bool
30+
}
31+
32+
/// Serializable representation of an F# quotation expression tree.
33+
/// Designed for cross-platform JSON serialization via Thoth.Json or Fable.Remoting.
34+
/// On Fable/JS: constructed by the compiler when you write <@ expr @>.
35+
/// On .NET: deserialize from JSON using the same type definitions.
36+
[<RequireQualifiedAccess>]
37+
type QuotExpr =
38+
| Value of literal: QuotLiteral
39+
| Var of var: QuotVar
40+
| Lambda of var: QuotVar * body: QuotExpr
41+
| Application of func: QuotExpr * arg: QuotExpr
42+
| Let of var: QuotVar * value: QuotExpr * body: QuotExpr
43+
| LetRecursive of bindings: (QuotVar * QuotExpr) array * body: QuotExpr
44+
| IfThenElse of guard: QuotExpr * thenExpr: QuotExpr * elseExpr: QuotExpr
45+
| Call of instance: QuotExpr option * methodName: string * declaringType: string * args: QuotExpr array
46+
| Sequential of first: QuotExpr * second: QuotExpr
47+
| NewTuple of elements: QuotExpr array
48+
| TupleGet of tuple: QuotExpr * index: int
49+
| NewUnionCase of typeName: string * tag: int * fields: QuotExpr array
50+
| UnionCaseTag of expr: QuotExpr
51+
| UnionCaseField of expr: QuotExpr * index: int
52+
| NewRecord of typeName: string * fieldNames: string array * fields: QuotExpr array
53+
| FieldGet of instance: QuotExpr option * fieldName: string
54+
| FieldSet of instance: QuotExpr option * fieldName: string * value: QuotExpr
55+
| VarSet of var: QuotVar * value: QuotExpr
56+
| NewList of elements: QuotExpr array * elementType: string
57+
| NewOption of value: QuotExpr option * elementType: string

src/Fable.Core/Fable.Core.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<Compile Include="Fable.Core.PyInterop.fs" />
2121
<Compile Include="Fable.Core.RustInterop.fs" />
2222
<Compile Include="Fable.Core.BeamInterop.fs" />
23+
<Compile Include="Fable.Core.Quotations.fs" />
2324
<Compile Include="Fable.Core.Extensions.fs" />
2425
<Content Include="CHANGELOG.md" />
2526
</ItemGroup>

src/Fable.Transforms/Beam/Fable2Beam.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,10 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er
11751175
]
11761176
)
11771177

1178+
| Quote _ ->
1179+
com.WarnOnlyOnce("Quotations are not supported for Beam target", ?range = expr.Range)
1180+
Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "undefined"))
1181+
11781182
| Extended(kind, _range) ->
11791183
match kind with
11801184
| Throw(Some exprArg, _typ) ->

src/Fable.Transforms/Dart/Fable2Dart.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1986,6 +1986,10 @@ module Util =
19861986
addError com [] r "Unexpected unresolved expression"
19871987
[], None
19881988

1989+
| Fable.Quote _ ->
1990+
addError com [] expr.Range "Quotations are not supported for Dart"
1991+
[], None
1992+
19891993
| Fable.ObjectExpr(_, t, _) ->
19901994
match returnStrategy with
19911995
// Constructors usually have a useless object expression on top

src/Fable.Transforms/FSharp2Fable.fs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1456,10 +1456,11 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs
14561456
$"Cannot compile ILFieldGet(%A{ownerTyp}, %s{fieldName})"
14571457
|> addErrorAndReturnNull com ctx.InlinePath (makeRangeFrom fsExpr)
14581458

1459-
| FSharpExprPatterns.Quote _ ->
1460-
return
1461-
"Quotes are not currently supported by Fable"
1462-
|> addErrorAndReturnNull com ctx.InlinePath (makeRangeFrom fsExpr)
1459+
| FSharpExprPatterns.Quote quotedExpr ->
1460+
let! body = transformExpr com ctx [] quotedExpr
1461+
let exprType = fsExpr.Type
1462+
let isTyped = exprType.GenericArguments.Count > 0
1463+
return Fable.Quote(body, isTyped)
14631464

14641465
| FSharpExprPatterns.AddressOf expr ->
14651466
let r = makeRangeFrom fsExpr
@@ -2485,6 +2486,8 @@ let resolveInlineExpr (com: IFableCompiler) ctx info expr =
24852486
)
24862487
| Fable.Debugger -> e
24872488

2489+
| Fable.Quote(quotedExpr, isTyped) -> Fable.Quote(resolveInlineExpr com ctx info quotedExpr, isTyped)
2490+
24882491
| Fable.Unresolved(e, t, r) ->
24892492
match e with
24902493
| Fable.UnresolvedTraitCall(sourceTypes, traitName, isInstance, argTypes, argExprs) ->

src/Fable.Transforms/Fable.Transforms.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<Compile Include="Beam/Beam.AST.fs" />
2828
<Compile Include="Beam/Replacements.fs" />
2929
<Compile Include="Replacements.fs" />
30+
<Compile Include="QuotationEmitter.fs" />
3031
<Compile Include="Replacements.Api.fs" />
3132
<Compile Include="FSharp2Fable.fsi" />
3233
<Compile Include="FSharp2Fable.fs" />

0 commit comments

Comments
 (0)