Conversation
…ching, and evaluation Add support for F# code quotations (`<@ ... @>`) to the Python and Beam targets. This enables constructing quotation ASTs at runtime, pattern matching over them using `Microsoft.FSharp.Quotations.Patterns`, and evaluating them via `LeafExpressionConverter.EvaluateQuotation`. Architecture: - Adds `Quote of quotedExpr: Expr * isTyped: bool` to Fable AST (minimal change) - QuotationEmitter.fs (shared): transforms quoted Fable.Expr into runtime library calls - Runtime libraries: fable_quotation.py and fable_quotation.erl with dataclass/tuple-based AST nodes, pattern match helpers, and an evaluator with operator dispatch Supported quotation nodes: Value, Lambda, Let, IfThenElse, Application, Call, Sequential, NewTuple, Operation, TypeCast, Get (TupleIndex, UnionField, UnionTag, FieldGet), Set (ValueSet, FieldSet), NewUnion, NewRecord, NewOption, NewList. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ern helpers Beam arrays are stored as process dictionary references, not plain lists. Added deref/1 helper to convert Refs back to lists before iteration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Python Type Checking Results (Pyright)
Excluded files with errors (4 files)These files have known type errors and are excluded from CI. Remove from
|
The NewTuple active pattern returns Expr list (FSharpList) on .NET. Convert the native array to FSharpList in the Python runtime so exprs.Length works correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove unnecessary $ prefix from plain strings and add %s format specifier to typeToString interpolation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add QuotationEmitter.fs to fable-standalone project file list - Use Array[Any] instead of list[Any] in fable_quotation.py to match Fable's array representation (FSharpArray), fixing pyright errors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- get_free_vars: walks AST collecting unbound variables - substitute: replaces variables using a substitution function - expr_to_string: pretty-prints quotation AST as F#-like source code (ToString wiring deferred to separate task — needs to match .NET format) - Wired up in both Python and Beam Replacements - Added GetFreeVars test for both targets - Updated CLAUDE.md to clarify test running Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Question: Query expression support and FCS inliningWhile implementing quotation support, we investigated adding What happens:
We added replacement handlers for Question: Does this analysis make sense? Are we right that FCS inlines the QueryBuilder before Fable can intercept it? If so, is there a way in the FCS fork to prevent this inlining, or to add a hook that lets Fable intercept QueryBuilder calls before they're resolved? This would enable mapping |
…lity Seq.length expects IEnumerable_1, which Array implements but plain list does not. Fixes pyright error in transpiled test output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The list module is auto-generated from List.fs during build. A minimal .pyi stub lets pyright/Pylance resolve the import in the source tree without needing a full build. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The FCS fork that Fable uses is intentionally a very thin layer/facade on top of the actual FCS, with no customization of the F# AST. That helps to shield us from the FCS internal churn and helps with migrating to new FCS versions, but it is true that it is sometimes hard to accurately reconstruct F# features from the F# AST, as exposed by FCS. For example, in Fable we go through a lot of trouble to try to reconstruct the original F# match expressions, which get compiled into F# AST decision trees and targets by FCS, and that reconstruction back can never be perfect. I don't know if such reconstruction from F# AST is possible for quotations, does the F# AST retain enough fidelity for Fable to reconstruct the original form. Good question to ask in the That said, if you have a proposal for modifying the Fable FCS fork, we technically can make changes, assuming it is not a drastic modification of the F# AST that would impede future maintenance when adopting new FCS versions. |
In theory, if we don't use a code from Fable it gets removed by bundlers when generating the bundle. This is for example, the case for things related to So this is probably fine, to add support for JavaScript too. |
|
@ncave Thanks for the feedback. I will drop this for now. Probably easier to just make our own query builder outside the compiler if query syntax is needed. |
| // F# Quotation: FSharpExpr static methods (e.g. Expr.Value, Expr.Lambda, etc.) | ||
| let private quotationExprs | ||
| (com: ICompiler) | ||
| (_ctx: Context) | ||
| r | ||
| (t: Type) | ||
| (i: CallInfo) | ||
| (_thisArg: Expr option) | ||
| (args: Expr list) | ||
| = | ||
| match i.CompiledName, _thisArg, args with | ||
| | "Value", _, [ value; typeArg ] -> | ||
| Helper.LibCall(com, "fable_quotation", "mk_value", t, [ value; typeArg ], ?loc = r) | ||
| |> Some | ||
| | "Var", _, [ var ] -> | ||
| Helper.LibCall(com, "fable_quotation", "mk_var_expr", t, [ var ], ?loc = r) | ||
| |> Some | ||
| | "Lambda", _, [ var; body ] -> | ||
| Helper.LibCall(com, "fable_quotation", "mk_lambda", t, [ var; body ], ?loc = r) | ||
| |> Some | ||
| | "Application", _, [ func; arg ] -> | ||
| Helper.LibCall(com, "fable_quotation", "mk_app", t, [ func; arg ], ?loc = r) | ||
| |> Some | ||
| | "Let", _, [ var; value; body ] -> | ||
| Helper.LibCall(com, "fable_quotation", "mk_let", t, [ var; value; body ], ?loc = r) | ||
| |> Some | ||
| | "IfThenElse", _, [ guard; thenExpr; elseExpr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "mk_if_then_else", t, [ guard; thenExpr; elseExpr ], ?loc = r) | ||
| |> Some | ||
| | "Call", _, [ instance; methodInfo; argList ] -> | ||
| Helper.LibCall(com, "fable_quotation", "mk_call", t, [ instance; methodInfo; argList ], ?loc = r) | ||
| |> Some | ||
| | "NewTuple", _, [ elements ] -> | ||
| Helper.LibCall(com, "fable_quotation", "mk_new_tuple", t, [ elements ], ?loc = r) | ||
| |> Some | ||
| | "Sequential", _, [ first; second ] -> | ||
| Helper.LibCall(com, "fable_quotation", "mk_sequential", t, [ first; second ], ?loc = r) | ||
| |> Some | ||
| | "get_Type", Some callee, _ -> | ||
| Helper.LibCall(com, "fable_quotation", "get_type", t, [ callee ], ?loc = r) | ||
| |> Some | ||
| | "GetFreeVars", Some callee, _ -> | ||
| Helper.LibCall(com, "fable_quotation", "get_free_vars", t, [ callee ], ?loc = r) | ||
| |> Some | ||
| | "Substitute", Some callee, [ fn ] -> | ||
| Helper.LibCall(com, "fable_quotation", "substitute", t, [ callee; fn ], ?loc = r) | ||
| |> Some | ||
| | "ToString", Some callee, _ | ||
| | "ToString", Some callee, [ _ ] -> | ||
| Helper.LibCall(com, "fable_quotation", "expr_to_string", t, [ callee ], ?loc = r) | ||
| |> Some | ||
| | _ -> None | ||
|
|
||
| // F# Quotation: FSharpVar (.ctor, get_Name, get_Type, get_IsMutable) | ||
| let private quotationVars | ||
| (com: ICompiler) | ||
| (_ctx: Context) | ||
| r | ||
| (t: Type) | ||
| (i: CallInfo) | ||
| (thisArg: Expr option) | ||
| (args: Expr list) | ||
| = | ||
| match i.CompiledName, thisArg, args with | ||
| | ".ctor", None, [ name; typ; isMutable ] -> | ||
| Helper.LibCall(com, "fable_quotation", "mk_var", t, [ name; typ; isMutable ], ?loc = r) | ||
| |> Some | ||
| | ".ctor", None, [ name; typ ] -> | ||
| Helper.LibCall(com, "fable_quotation", "mk_var", t, [ name; typ; makeBoolConst false ], ?loc = r) | ||
| |> Some | ||
| | "get_Name", Some callee, _ -> | ||
| Helper.LibCall(com, "fable_quotation", "var_get_name", t, [ callee ], ?loc = r) | ||
| |> Some | ||
| | "get_Type", Some callee, _ -> | ||
| Helper.LibCall(com, "fable_quotation", "var_get_type", t, [ callee ], ?loc = r) | ||
| |> Some | ||
| | "get_IsMutable", Some callee, _ -> | ||
| Helper.LibCall(com, "fable_quotation", "var_get_is_mutable", t, [ callee ], ?loc = r) | ||
| |> Some | ||
| | _ -> None | ||
|
|
||
| // F# Quotation: PatternsModule (active patterns like ValuePattern, LambdaPattern, etc.) | ||
| let private quotationPatterns | ||
| (com: ICompiler) | ||
| (_ctx: Context) | ||
| r | ||
| (t: Type) | ||
| (i: CallInfo) | ||
| (_thisArg: Expr option) | ||
| (args: Expr list) | ||
| = | ||
| match i.CompiledName, args with | ||
| | ("ValuePattern" | "|Value|_|"), [ expr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "is_value", t, [ expr ], ?loc = r) | ||
| |> Some | ||
| | ("VarPattern" | "|Var|_|"), [ expr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "is_var", t, [ expr ], ?loc = r) |> Some | ||
| | ("LambdaPattern" | "|Lambda|_|"), [ expr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "is_lambda", t, [ expr ], ?loc = r) | ||
| |> Some | ||
| | ("ApplicationPattern" | "|Application|_|"), [ expr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "is_application", t, [ expr ], ?loc = r) | ||
| |> Some | ||
| | ("LetPattern" | "|Let|_|"), [ expr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "is_let", t, [ expr ], ?loc = r) |> Some | ||
| | ("IfThenElsePattern" | "|IfThenElse|_|"), [ expr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "is_if_then_else", t, [ expr ], ?loc = r) | ||
| |> Some | ||
| | ("CallPattern" | "|Call|_|"), [ expr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "is_call", t, [ expr ], ?loc = r) |> Some | ||
| | ("NewTuplePattern" | "|NewTuple|_|"), [ expr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "is_new_tuple", t, [ expr ], ?loc = r) | ||
| |> Some | ||
| | ("SequentialPattern" | "|Sequential|_|"), [ expr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "is_sequential", t, [ expr ], ?loc = r) | ||
| |> Some | ||
| | ("NewUnionCasePattern" | "|NewUnionCase|_|"), [ expr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "is_new_union", t, [ expr ], ?loc = r) | ||
| |> Some | ||
| | ("NewRecordPattern" | "|NewRecord|_|"), [ expr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "is_new_record", t, [ expr ], ?loc = r) | ||
| |> Some | ||
| | ("TupleGetPattern" | "|TupleGet|_|"), [ expr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "is_tuple_get", t, [ expr ], ?loc = r) | ||
| |> Some | ||
| | ("PropertyGetPattern" | "|PropertyGet|_|"), [ expr ] -> | ||
| Helper.LibCall(com, "fable_quotation", "is_field_get", t, [ expr ], ?loc = r) | ||
| |> Some | ||
| | _ -> None |
There was a problem hiding this comment.
When looking at the code from both Replacement.fs files, I am wondering if it would be worth sharing the code.
It seems like both code are identical match.
Summary
FYI: This is just an investigation of what it would take to add Quotation support to Fable [WIP]
<@ ... @>) to the Python and Beam targetsMicrosoft.FSharp.Quotations.Patterns), and evaluated (viaLeafExpressionConverter.EvaluateQuotation)QuotationEmitter.fstransforms quotedFable.Exprinto runtime library calls at compile timeSupported quotation nodes
Value, Lambda, Let, IfThenElse, Application, Call, Sequential, NewTuple, Operation (all arithmetic/comparison/logical), TypeCast, Get (TupleIndex, UnionField, UnionTag, FieldGet), Set (ValueSet, FieldSet), NewUnion, NewRecord, NewOption, NewList, DecisionTree (simple cases).
Files changed
Fable.fs— addsQuote of quotedExpr: Expr * isTyped: bool(3 lines)FSharp2Fable.fs,FableTransforms.fs,Transforms.Util.fs— capture and propagateQuoteQuotationEmitter.fs(new) — transforms quoted Fable.Expr → runtime lib callsFable2Python.Transforms.fs,Python/Replacements.fs,fable_quotation.py(new)Fable2Beam.fs,Beam/Replacements.fs,fable_quotation.erlFable2Babel.fs,Fable2Dart.fs,Fable2Rust.fs,Fable2Php.fs— error stubs forQuoteTestQuotation.fs(Python, 24 tests),QuotationTests.fs(Beam, 18 tests)Fable.Cli/CHANGELOG.md,Fable.Compiler/CHANGELOG.mdDesign note: AST approach vs original PR #1839
The original quotation PR (#1839) added a complex
ExprDatarecord to the AST:This PR takes a minimal AST approach instead:
The quoted
Expris kept as-is in the Fable AST. All decomposition into runtime calls happens in the transform layer (QuotationEmitter.fs), not the AST. This means:Fable.AST(no new types, no newTypevariants)QuotationEmittercan evolve without breaking the AST contractQuote _ -> Anyis less precise thanExpr(Some typ)/Expr NoneIf a more precise type is desired, a
| Quotation of gen: Type optioncould be added to theTypeunion — but that would be a separate AST change worth discussing.Test plan
./build.sh test python)./build.sh test beam)🤖 Generated with Claude Code