[JS/TS] Add F# quotation support#4474
Conversation
|
Nice! We should coordinate / align this with #4474 |
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>
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>
|
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. |
|
@dbrattli by all means that's fine by me. probably better as my pr only targets js/ts as of now. |
|
@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. |
|
@OnurGumus We are good to go. Please rebase PR on main now, then we get support for JS/TS/Beam and Python for this feature |
…d evaluation Builds on the shared quotation infrastructure from fable-compiler#4398 to enable quotation support for the JavaScript/TypeScript target. Replaces the "not yet supported" error stubs in Fable2Babel.fs with actual Quote handlers, adds the JS/TS runtime library (quotation.ts) matching the Python/Beam API, wires up Replacements.fs via the shared Quotations.tryQuotationCall, and adds comprehensive tests.
7264c80 to
e662d62
Compare
Replace constructor parameter properties (readonly params) with explicit field declarations to comply with erasableSyntaxOnly.
- Let binding: check name starts with "x" (FSharp2Fable may add suffix) - NewTuple: use List.length instead of .Length (isNewTuple returns array)
|
@dbrattli done |
|
@OnurGumus Please update PR description. What happened to JSON serialization? Will you add it back in a later PR? |
|
I've changed. Sorry, in my day to day work, I never use PRs but use gerrit. |
- Add toJSON() methods to all Expr classes for JSON.stringify support - Add exprFromJSON() deserializer for reconstructing from JSON - Add Fable.Core.Quotations.fs with QuotExpr/QuotVar types for .NET-side deserialization (e.g. via Thoth.Json) - Add JSON serialization tests
dbrattli
left a comment
There was a problem hiding this comment.
Code Review
Nice clean integration with the existing QuotationEmitter infrastructure — the Fable2Babel.fs changes are minimal and the approach is consistent with Python/Beam targets. A few items to address:
Incomplete traversal in getFreeVars and substitute
Both getFreeVars and substitute in quotation.ts are missing recursion into several AST node types: ExprNewUnion, ExprNewRecord, ExprNewList, ExprFieldSet, ExprVarSet, ExprUnionTag, ExprUnionField. Free variables inside these nodes would be silently missed / not substituted.
Loose any typing
Several fields use any where more specific types apply:
ExprCall.instance→Expr | nullExprCall.args→Expr[]ExprNewTuple.elements→Expr[]ExprNewUnion.fields→Expr[]ExprNewRecord.fieldNames→string[]ExprNewRecord.values→Expr[]
Simplify substitute instance check
The verbose instanceof chain for ExprCall.instance in substitute:
const newInst = e.instance instanceof ExprVarExpr || e.instance instanceof ExprValue
|| e.instance instanceof ExprLambda || ...
? sub(e.instance) : e.instance;can be simplified to:
const newInst = e.instance != null ? sub(e.instance) : e.instance;Minor
op_Divisionalways truncates to integer via| 0— will produce wrong results for float quotations. Can be fixed for all targets in a follow-up PR.
Overall looks good 👍
- Complete getFreeVars/substitute traversal for all AST nodes: ExprNewUnion, ExprNewRecord, ExprNewList, ExprFieldSet, ExprVarSet, ExprUnionTag, ExprUnionField - Tighten types: Expr | null for ExprCall.instance, Expr[] for args/ elements/fields/values, string[] for fieldNames - Simplify substitute instance check to: e.instance != null ? sub(...) - Remove integer truncation (| 0) from op_Division
dbrattli
left a comment
There was a problem hiding this comment.
This looks good to me 👍🏻 Perhaps the only concern is the JSON format in Fable.Core. I think it's nice and would love to use it myself. There is no .NET standard for this, so this is Fable specific. Reminds me of Reaqtor Bonsai trees. But its optional and anyone could make their own format if they wanted to so I don't see any problems adding it. I say we add it and test it out over a few -rc releases to see how this works. I will let @MangelMaxime have the last word before merging.
Change toJSON() output from ["Tag", ...fields] arrays to
{"Case":"Tag","Fields":[...]} objects. This matches Thoth.Json's
standard DU encoding, enabling Decode.Auto.fromString<QuotExpr>(json)
on .NET with zero custom decoders.
|
@dbrattli I added one change, that would make thoth to be able to deserialize |
It depends what you mean by that. In general, Thoth.Json don't really have a fixed format by default because of the manual API. However, there are 2 places where we have format in place:
I believe you wanted to make it compatible with the later? @OnurGumus |
Change toJSON() from {"Case":"Tag","Fields":[...]} objects to
["Tag", ...fields] arrays, matching Thoth.Json Auto encoding.
On .NET, deserialize with FSharp.SystemTextJson ThothLike:
options.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.ThothLike))
Verified end-to-end: Fable JS -> JSON -> .NET deserialize -> evaluate.
|
@OnurGumus @MangelMaxime If we're using a different structure for the Fable quotations, instead of the F# quotations (which require full reflection), do you think it is possible to convert it to F# implementation that serializes the same way, instead of a TypeScript implementation? This way we might be able to share it more uniformly between other Fable targets. |
|
@ncave Do you mean by writing Like done by @OnurGumus @dbrattli Quotations have been released in Fable RC 7 |
|
@MangelMaxime Yes, I was just wondering if we can use the same quotations.fs across all targets. |
|
@ncave Yes, a good chunk of it could be shared F#. The data types, constructors (mkValue, mkLambda, mkCall etc.), and the pure tree-walking functions (evaluate, getFreeVars, substitute, exprToString) are all just plain data structures and recursion, no target-specific code needed. That part would work the same way Seq.fs or Map.fs are shared across targets today. The part that would still need to be per-target is serialization. The JS toJSON() method hooks into JSON.stringify automatically, Python uses dataclasses with snake_case, Erlang uses tuples. Those idioms don't translate from a single F# source. So maybe 70% could become a shared Quotation.fs, with a thin per-target layer for serialization. |
Summary
Builds on the shared quotation infrastructure from #4398 to enable F# code quotations (
<@ expr @>and<@@ expr @@>) for the JavaScript/TypeScript target.Quotehandlers that call the sharedQuotationEmitterquotation.tsJS/TS runtime library matching the Python/Beam API (constructors, pattern matchers, evaluator, toString, getFreeVars, substitute)Replacements.fsvia the sharedQuotations.tryQuotationCallfor pattern matching (Patterns.|Value|_|, etc.), evaluation (LeafExpressionConverter.EvaluateQuotation), andVaraccessors%) support for composing quotationstoJSON()producing Thoth.Json Auto-compatible["Tag", ...fields]arrays, plusexprFromJSON()deserializerFable.Core.Quotations: sharedQuotExpr/QuotVarDU types for .NET-side deserializationDeserializing on .NET
The JSON format uses
["Tag", ...fields]arrays, matching the Thoth.Json Auto DU encoding. On .NET, deserialize with FSharp.SystemTextJson'sThothLikepreset:End-to-end example — using a Fable quotation as a server-side filter (verified working):
Changed files
src/Fable.Transforms/Fable2Babel.fsQuotationEmitter.emitQuotedExprhandlerssrc/Fable.Transforms/Replacements.fsQuotations.tryQuotationCall "quotation"src/fable-library-ts/quotation.tssrc/Fable.Core/Fable.Core.Quotations.fsQuotExpr/QuotVartypes for .NET deserializationtests/Js/Main/QuotationTests.fsTest plan
Value,Lambda,Let,IfThenElse,Application,NewTuple,Sequential,Var)LeafExpressionConverter.EvaluateQuotationfor arithmetic, let bindings, lambdas, tuples, comparisons)GetFreeVars)<@@ @@>) works