Skip to content

[JS/TS] Add F# quotation support with serializable QuotExpr AST#4474

Open
OnurGumus wants to merge 2 commits intofable-compiler:mainfrom
OnurGumus:quotation-support
Open

[JS/TS] Add F# quotation support with serializable QuotExpr AST#4474
OnurGumus wants to merge 2 commits intofable-compiler:mainfrom
OnurGumus:quotation-support

Conversation

@OnurGumus
Copy link
Copy Markdown
Contributor

Summary

  • Add support for F# code quotations (<@ expr @> and <@@ expr @@>) targeting JS/TS
  • Quotations compile to a serializable QuotExpr discriminated union (defined in Fable.Core.Quotations)
  • Designed for cross-platform JSON serialization via Thoth.Json / Fable.Remoting between Fable clients and .NET backends
  • The % splice operator is supported for composing quotations
  • Other targets (Python, Rust, Dart, Beam, PHP) have stubs with error messages

Supported quotation nodes

Values (int, float, string, char, bool, unit, null), lambdas, let/letrec bindings, if/then/else, function calls, operators, tuples, unions, records, options, lists, sequential, variable set, field get/set

New files

File Purpose
src/Fable.Core/Fable.Core.Quotations.fs Shared F# DU types (QuotExpr, QuotVar, QuotLiteral) for .NET deserialization
src/fable-library-ts/Quotation.ts JS/TS runtime with Union subclasses, reflection info, and quotExprFromJSON deserializer
src/Fable.Transforms/QuotationEmitter.fs Compiler transform: Fable Quote AST → runtime constructor calls
tests/Js/Main/QuotationTests.fs 18 tests including structural verification, splice composition, and JSON round-trip

Usage

// Fable client — construct and serialize
let q = <@ fun x -> x + 1 @>
let json = JS.JSON.stringify(q)
// → ["Lambda",{"Name":"x","Type":"System.Int32","IsMutable":false},["Call",null,"op_Addition",...]]

// .NET backend — deserialize (add Fable.Core NuGet)
open Fable.Core.Quotations
let expr = Thoth.Json.Net.Decode.Auto.fromString<QuotExpr>(json)

Test plan

  • All 2805 JS tests pass (0 failing)
  • All 2642 .NET tests pass (0 errored)
  • TypeScript quicktest compiles and runs
  • Structural tests verify quotation AST shape (case names, field values)
  • Splice (%) tests verify expression composition
  • JSON serialization round-trip test
  • Manual test with Thoth.Json on .NET side

🤖 Generated with Claude Code

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>
@dbrattli
Copy link
Copy Markdown
Collaborator

dbrattli commented Apr 2, 2026

Nice! We should coordinate / align this with #4474

| Unit -> "Microsoft.FSharp.Core.Unit"
| Any -> "System.Object"
| LambdaType(argType, returnType) ->
$"Microsoft.FSharp.Core.FSharpFunc`2[{typeToString argType},{typeToString returnType}]"

Check warning

Code scanning / Ionide.Analyzers.Cli

Warns about missing type specifiers in interpolated strings Warning

Interpolated hole expression without format detected. Use prefix with the correct % to enforce type safety.
| Unit -> "Microsoft.FSharp.Core.Unit"
| Any -> "System.Object"
| LambdaType(argType, returnType) ->
$"Microsoft.FSharp.Core.FSharpFunc`2[{typeToString argType},{typeToString returnType}]"

Check warning

Code scanning / Ionide.Analyzers.Cli

Warns about missing type specifiers in interpolated strings Warning

Interpolated hole expression without format detected. Use prefix with the correct % to enforce type safety.
$"Microsoft.FSharp.Core.FSharpFunc`2[{typeToString argType},{typeToString returnType}]"
| Tuple(genArgs, _) ->
let args = genArgs |> List.map typeToString |> String.concat ","
$"System.Tuple`{genArgs.Length}[{args}]"

Check warning

Code scanning / Ionide.Analyzers.Cli

Warns about missing type specifiers in interpolated strings Warning

Interpolated hole expression without format detected. Use prefix with the correct % to enforce type safety.
$"Microsoft.FSharp.Core.FSharpFunc`2[{typeToString argType},{typeToString returnType}]"
| Tuple(genArgs, _) ->
let args = genArgs |> List.map typeToString |> String.concat ","
$"System.Tuple`{genArgs.Length}[{args}]"

Check warning

Code scanning / Ionide.Analyzers.Cli

Warns about missing type specifiers in interpolated strings Warning

Interpolated hole expression without format detected. Use prefix with the correct % to enforce type safety.
let args = genArgs |> List.map typeToString |> String.concat ","
$"System.Tuple`{genArgs.Length}[{args}]"
| DeclaredType(entRef, _genArgs) -> entRef.FullName
| Option(genArg, _) -> $"Microsoft.FSharp.Core.FSharpOption`1[{typeToString genArg}]"

Check warning

Code scanning / Ionide.Analyzers.Cli

Warns about missing type specifiers in interpolated strings Warning

Interpolated hole expression without format detected. Use prefix with the correct % to enforce type safety.
$"System.Tuple`{genArgs.Length}[{args}]"
| DeclaredType(entRef, _genArgs) -> entRef.FullName
| Option(genArg, _) -> $"Microsoft.FSharp.Core.FSharpOption`1[{typeToString genArg}]"
| List genArg -> $"Microsoft.FSharp.Collections.FSharpList`1[{typeToString genArg}]"

Check warning

Code scanning / Ionide.Analyzers.Cli

Warns about missing type specifiers in interpolated strings Warning

Interpolated hole expression without format detected. Use prefix with the correct % to enforce type safety.
| DeclaredType(entRef, _genArgs) -> entRef.FullName
| Option(genArg, _) -> $"Microsoft.FSharp.Core.FSharpOption`1[{typeToString genArg}]"
| List genArg -> $"Microsoft.FSharp.Collections.FSharpList`1[{typeToString genArg}]"
| Array(genArg, _) -> $"{typeToString genArg}[]"

Check warning

Code scanning / Ionide.Analyzers.Cli

Warns about missing type specifiers in interpolated strings Warning

Interpolated hole expression without format detected. Use prefix with the correct % to enforce type safety.
Helper.LibCall(com, "Quotation", "mkValue", Any, [ lit ])

and private emitUnsupported (com: Compiler) (nodeName: string) : Expr =
let msg = $"Unsupported quotation node: {nodeName}"

Check warning

Code scanning / Ionide.Analyzers.Cli

Warns about missing type specifiers in interpolated strings Warning

Interpolated hole expression without format detected. Use prefix with the correct % to enforce type safety.
dbrattli added a commit that referenced this pull request Apr 4, 2026
Rename to match #4474 conventions so it can rebase cleanly:
- mkVarExpr -> mkVar (Expr.Var), mkVar -> mkQuotVar (Var constructor)
- mkApp -> mkApplication
- isNewUnion -> isNewUnionCase

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dbrattli added a commit that referenced this pull request Apr 4, 2026
Use "quotation" as the canonical module name:
- Python: quotation.py (renamed from fable_quotation.py)
- Beam: fable_quotation.erl (fable_ prefix added by getLibPath)
- JS (#4474): Quotation.ts (natural mapping)

Revert the Beam getLibPath workaround as it's no longer needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dbrattli
Copy link
Copy Markdown
Collaborator

dbrattli commented Apr 4, 2026

I have fixed #4398 so that the common code will work for JS/TS as well. Thus can be reused by this PR, so most changes to Replacements.fs will not be needed. @MangelMaxime @ncave. If we are good with adding Quotations then I can merge my PR? Then this PR will be much simpler on top of that.

Add QuotationEmitter.fs to Fable.Standalone.fsproj and add type-safe
format specifiers to interpolated strings in QuotationEmitter.fs.
@OnurGumus
Copy link
Copy Markdown
Contributor Author

@dbrattli by all means that's fine by me. probably better as my pr only targets js/ts as of now.

@dbrattli
Copy link
Copy Markdown
Collaborator

dbrattli commented Apr 4, 2026

@OnurGumus Ok, let us just check with @MangelMaxime that we are ok with the Fable.AST change at this point since it may break extensions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants