From a5ebe8ba9fc83ddecb91d4390a2f789a6e51f2b2 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 14 Mar 2026 08:40:41 +0100 Subject: [PATCH 01/15] [Python/Beam] Add F# quotation support with construction, pattern matching, 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) --- src/Fable.AST/Fable.fs | 3 + src/Fable.Cli/CHANGELOG.md | 4 + src/Fable.Compiler/CHANGELOG.md | 4 + src/Fable.Transforms/Beam/Fable2Beam.fs | 4 + src/Fable.Transforms/Beam/Replacements.fs | 131 +++++ src/Fable.Transforms/Dart/Fable2Dart.fs | 4 + src/Fable.Transforms/FSharp2Fable.fs | 11 +- src/Fable.Transforms/Fable.Transforms.fsproj | 1 + src/Fable.Transforms/Fable2Babel.fs | 7 + src/Fable.Transforms/FableTransforms.fs | 3 +- src/Fable.Transforms/Php/Fable2Php.fs | 1 + .../Python/Fable2Python.Transforms.fs | 7 + .../Python/Fable2Python.Util.fs | 1 + src/Fable.Transforms/Python/Replacements.fs | 131 +++++ src/Fable.Transforms/QuotationEmitter.fs | 338 +++++++++++++ src/Fable.Transforms/Rust/Fable2Rust.fs | 4 + src/Fable.Transforms/Transforms.Util.fs | 14 + src/fable-library-beam/fable_quotation.erl | 171 +++++++ .../fable_library/fable_quotation.py | 462 ++++++++++++++++++ tests/Beam/Fable.Tests.Beam.fsproj | 1 + tests/Beam/QuotationTests.fs | 140 ++++++ tests/Python/Fable.Tests.Python.fsproj | 1 + tests/Python/TestQuotation.fs | 155 ++++++ 23 files changed, 1593 insertions(+), 5 deletions(-) create mode 100644 src/Fable.Transforms/QuotationEmitter.fs create mode 100644 src/fable-library-beam/fable_quotation.erl create mode 100644 src/fable-library-py/fable_library/fable_quotation.py create mode 100644 tests/Beam/QuotationTests.fs create mode 100644 tests/Python/TestQuotation.fs diff --git a/src/Fable.AST/Fable.fs b/src/Fable.AST/Fable.fs index babda01ba9..bb86fd70a1 100644 --- a/src/Fable.AST/Fable.fs +++ b/src/Fable.AST/Fable.fs @@ -834,11 +834,13 @@ type Expr = | Unresolved of expr: UnresolvedExpr * typ: Type * range: SourceLocation option | Extended of expr: ExtendedSet * range: SourceLocation option + | Quote of quotedExpr: Expr * isTyped: bool member this.Type = match this with | Unresolved(_, t, _) -> t | Extended(kind, _) -> kind.Type + | Quote _ -> Any | Test _ -> Boolean | Value(kind, _) -> kind.Type | IdentExpr id -> id.Type @@ -890,6 +892,7 @@ type Expr = | Set(_, _, _, _, r) | ForLoop(_, _, _, _, _, r) | WhileLoop(_, _, r) -> r + | Quote(e, _) -> e.Range // module PrettyPrint = // let rec printType (t: Type) = "T" // TODO diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 96552b1495..d5f2a49b93 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +* [Python/Beam] Add F# quotation support — construction, pattern matching, and evaluation via `LeafExpressionConverter.EvaluateQuotation` (by @dbrattli) + ### Fixed * [Beam] Fix unused term warning in try/catch when exception variable is not referenced (by @dbrattli) diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 50ed292ee7..07604ad2c2 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +* [Python/Beam] Add F# quotation support — construction, pattern matching, and evaluation via `LeafExpressionConverter.EvaluateQuotation` (by @dbrattli) + ### Fixed * [Beam] Fix unused term warning in try/catch when exception variable is not referenced (by @dbrattli) diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index 7094e59cfd..6c8ea249b9 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -1165,6 +1165,10 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er ] ) + | Quote(body, _isTyped) -> + let emitted = QuotationEmitter.emitQuotedExpr com body + transformExpr com ctx emitted + | Extended(kind, _range) -> match kind with | Throw(Some exprArg, _typ) -> diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 21572ac7c5..ea57cfcddf 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -4576,6 +4576,126 @@ let private bclType (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (thisArg: Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some +// 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 + | _ -> 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 + let tryType (_t: Type) = None /// Compile-time resolution for System.Type methods when the type is known statically via TypeInfo. @@ -5144,6 +5264,17 @@ let tryCall | _ -> emitExpr r t [ c ] "maps:get(name, $0)" |> Some | _ -> None | "System.Text.StringBuilder" -> bclType com ctx r t info thisArg args + // F# Quotations + | Types.fsharpExpr + | Types.fsharpExprGeneric -> quotationExprs com ctx r t info thisArg args + | Types.fsharpVar -> quotationVars com ctx r t info thisArg args + | Types.patternsModule -> quotationPatterns com ctx r t info thisArg args + | "Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter" -> + match info.CompiledName, args with + | "EvaluateQuotation", [ expr ] -> + Helper.LibCall(com, "fable_quotation", "evaluate", t, [ expr ], ?loc = r) + |> Some + | _ -> None | _ -> None let tryBaseConstructor diff --git a/src/Fable.Transforms/Dart/Fable2Dart.fs b/src/Fable.Transforms/Dart/Fable2Dart.fs index bd0169f731..b8579c97c2 100644 --- a/src/Fable.Transforms/Dart/Fable2Dart.fs +++ b/src/Fable.Transforms/Dart/Fable2Dart.fs @@ -2188,6 +2188,10 @@ module Util = ], None + | Fable.Quote _ -> + addError com [] None "Quotations are not yet supported for Dart target" + [], None + let getLocalFunctionGenericParams (_com: IDartCompiler) (ctx: Context) diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 72348c8a7d..4c9feea779 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -1456,10 +1456,11 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs $"Cannot compile ILFieldGet(%A{ownerTyp}, %s{fieldName})" |> addErrorAndReturnNull com ctx.InlinePath (makeRangeFrom fsExpr) - | FSharpExprPatterns.Quote _ -> - return - "Quotes are not currently supported by Fable" - |> addErrorAndReturnNull com ctx.InlinePath (makeRangeFrom fsExpr) + | FSharpExprPatterns.Quote quotedExpr -> + let! body = transformExpr com ctx [] quotedExpr + let exprType = fsExpr.Type + let isTyped = exprType.GenericArguments.Count > 0 + return Fable.Quote(body, isTyped) | FSharpExprPatterns.AddressOf expr -> let r = makeRangeFrom fsExpr @@ -2475,6 +2476,8 @@ let resolveInlineExpr (com: IFableCompiler) ctx info expr = |> makeValue r | Fable.TypeInfo(t, d) -> Fable.TypeInfo(resolveInlineType ctx.GenericArgs t, d) |> makeValue r + | Fable.Quote(e, isTyped) -> Fable.Quote(resolveInlineExpr com ctx info e, isTyped) + | Fable.Extended(kind, r) as e -> match kind with | Fable.Curry(e, arity) -> Fable.Extended(Fable.Curry(resolveInlineExpr com ctx info e, arity), r) diff --git a/src/Fable.Transforms/Fable.Transforms.fsproj b/src/Fable.Transforms/Fable.Transforms.fsproj index 2e9579b408..7eeec3c7e8 100644 --- a/src/Fable.Transforms/Fable.Transforms.fsproj +++ b/src/Fable.Transforms/Fable.Transforms.fsproj @@ -28,6 +28,7 @@ + diff --git a/src/Fable.Transforms/Fable2Babel.fs b/src/Fable.Transforms/Fable2Babel.fs index 61241b6e9c..dca43b5fe8 100644 --- a/src/Fable.Transforms/Fable2Babel.fs +++ b/src/Fable.Transforms/Fable2Babel.fs @@ -1102,6 +1102,7 @@ module Util = | Fable.CurriedApply _ | Fable.Operation _ | Fable.Get _ + | Fable.Quote _ | Fable.Test _ -> false | Fable.TypeCast(e, _) -> isJsStatement ctx preferStatement e @@ -3123,6 +3124,8 @@ but thanks to the optimisation done below we get | Fable.Throw _ | Fable.Debugger -> iife com ctx expr + | Fable.Quote _ -> addErrorAndReturnNull com None "Quotations are not yet supported for JS/TS target" + let rec transformAsStatements (com: IBabelCompiler) ctx returnStrategy (expr: Fable.Expr) : Statement array = match expr with | Fable.Unresolved(_, _, r) -> @@ -3290,6 +3293,10 @@ but thanks to the optimisation done below we get ) |] + | Fable.Quote _ -> + addError com [] None "Quotations are not yet supported for JS/TS target" + [||] + let transformFunction com ctx name (args: Fable.Ident list) (body: Fable.Expr) : Parameter array * BlockStatement = let tailcallChance = Option.map (fun name -> NamedTailCallOpportunity(com, ctx, name, args) :> ITailCallOpportunity) name diff --git a/src/Fable.Transforms/FableTransforms.fs b/src/Fable.Transforms/FableTransforms.fs index 0504a57575..ea7d912087 100644 --- a/src/Fable.Transforms/FableTransforms.fs +++ b/src/Fable.Transforms/FableTransforms.fs @@ -160,7 +160,8 @@ let noSideEffectBeforeIdent identName expr = findIdentOrSideEffect e | Import _ | Lambda _ - | Delegate _ -> false + | Delegate _ + | Quote _ -> false | Extended((Throw _ | Debugger), _) -> true | Extended(Curry(e, _), _) -> findIdentOrSideEffect e | CurriedApply(callee, args, _, _) -> callee :: args |> findIdentOrSideEffectInList |> orSideEffect diff --git a/src/Fable.Transforms/Php/Fable2Php.fs b/src/Fable.Transforms/Php/Fable2Php.fs index 084890f42a..9f89ed14ae 100644 --- a/src/Fable.Transforms/Php/Fable2Php.fs +++ b/src/Fable.Transforms/Php/Fable2Php.fs @@ -935,6 +935,7 @@ let rec tryFindMethod methodName (phpType: PhpType) = let rec convertExpr (com: IPhpCompiler) (expr: Fable.Expr) = match expr with | Fable.Extended _ -> failwith "TODO: Extended instructions" + | Fable.Quote _ -> failwith "Quotations are not yet supported for PHP target" | Fable.Unresolved _ -> failwith "Unexpected unresolved expression" diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 7f280d15ec..0ddd3007f7 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -2652,6 +2652,9 @@ let rec transformAsExpr (com: IPythonCompiler) ctx (expr: Fable.Expr) : Expressi | Fable.Curry(e, arity) -> transformCurry com ctx e arity | Fable.Throw _ | Fable.Debugger -> iife com ctx expr + | Fable.Quote(body, _isTyped) -> + let emitted = QuotationEmitter.emitQuotedExpr com body + transformAsExpr com ctx emitted let transformAsSlice (com: IPythonCompiler) ctx expr (info: Fable.CallInfo) : Expression * Statement list = Expression.withStmts { @@ -2974,6 +2977,10 @@ let rec transformAsStatements (com: IPythonCompiler) ctx returnStrategy (expr: F [ Statement.for' (target = target, iter = iter, body = body) ] + | Fable.Quote(body, _isTyped) -> + let emitted = QuotationEmitter.emitQuotedExpr com body + transformAsStatements com ctx returnStrategy emitted + let transformFunction com ctx diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index ce58803536..7a9efbd15c 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -554,6 +554,7 @@ module Util = | Fable.Operation _ | Fable.Get _ | Fable.Test _ + | Fable.Quote _ | Fable.TypeCast _ -> false | Fable.TryCatch _ diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 9bfb9ecd2d..c9fedf16b9 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -3754,6 +3754,126 @@ let tryField com returnTyp ownerTyp fieldName = | _ -> None | _ -> None +// 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 + | _ -> 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 + let private replacedModules = dict [ @@ -3952,6 +4072,17 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr getTypeName com ctx loc exprType |> StringConstant |> makeValue r |> Some | c -> Helper.LibCall(com, "Reflection", "name", t, [ c ], ?loc = r) |> Some | _ -> None + // F# Quotations + | Types.fsharpExpr + | Types.fsharpExprGeneric -> quotationExprs com ctx r t info thisArg args + | Types.fsharpVar -> quotationVars com ctx r t info thisArg args + | Types.patternsModule -> quotationPatterns com ctx r t info thisArg args + | "Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter" -> + match info.CompiledName, args with + | "EvaluateQuotation", [ expr ] -> + Helper.LibCall(com, "fable_quotation", "evaluate", t, [ expr ], ?loc = r) + |> Some + | _ -> None | _ -> None let tryBaseConstructor com ctx (ent: EntityRef) (argTypes: Lazy) genArgs args = diff --git a/src/Fable.Transforms/QuotationEmitter.fs b/src/Fable.Transforms/QuotationEmitter.fs new file mode 100644 index 0000000000..35db3e66fe --- /dev/null +++ b/src/Fable.Transforms/QuotationEmitter.fs @@ -0,0 +1,338 @@ +module Fable.Transforms.QuotationEmitter + +open Fable +open Fable.AST +open Fable.AST.Fable +open Fable.Transforms +open Replacements.Util + +/// Emits a Fable expression that, when compiled, produces runtime calls +/// to construct a quotation AST. The input is the Fable.Expr captured +/// inside a Quote node; the output is a Fable.Expr that calls the +/// fable_quotation runtime library to build the AST at runtime. +let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = + match expr with + | Value(kind, r) -> emitQuotedValue com kind r + + | IdentExpr ident -> + // Reference to a variable already introduced by a lambda/let in the quotation. + // Emit: fable_quotation:mk_var_expr(Var) + // We need a var reference. Create a var and then wrap it. + let varExpr = + Helper.LibCall( + com, + "fable_quotation", + "mk_var", + Any, + [ + makeStrConst ident.Name + makeStrConst (typeToString ident.Type) + makeBoolConst false + ] + ) + + Helper.LibCall(com, "fable_quotation", "mk_var_expr", Any, [ varExpr ]) + + | Lambda(arg, body, _name) -> + let varExpr = + Helper.LibCall( + com, + "fable_quotation", + "mk_var", + Any, + [ + makeStrConst arg.Name + makeStrConst (typeToString arg.Type) + makeBoolConst false + ] + ) + + let bodyExpr = emitQuotedExpr com body + Helper.LibCall(com, "fable_quotation", "mk_lambda", Any, [ varExpr; bodyExpr ]) + + | Delegate(args, body, _name, _tags) -> + // Multi-arg delegate: nest as curried lambdas + let rec nestLambdas args body = + match args with + | [] -> emitQuotedExpr com body + | (arg: Ident) :: rest -> + let varExpr = + Helper.LibCall( + com, + "fable_quotation", + "mk_var", + Any, + [ + makeStrConst arg.Name + makeStrConst (typeToString arg.Type) + makeBoolConst false + ] + ) + + let innerBody = nestLambdas rest body + Helper.LibCall(com, "fable_quotation", "mk_lambda", Any, [ varExpr; innerBody ]) + + nestLambdas args body + + | Let(ident, value, body) -> + let varExpr = + Helper.LibCall( + com, + "fable_quotation", + "mk_var", + Any, + [ + makeStrConst ident.Name + makeStrConst (typeToString ident.Type) + makeBoolConst ident.IsMutable + ] + ) + + let valueExpr = emitQuotedExpr com value + let bodyExpr = emitQuotedExpr com body + + Helper.LibCall(com, "fable_quotation", "mk_let", Any, [ varExpr; valueExpr; bodyExpr ]) + + | IfThenElse(guardExpr, thenExpr, elseExpr, _r) -> + let guard = emitQuotedExpr com guardExpr + let thenE = emitQuotedExpr com thenExpr + let elseE = emitQuotedExpr com elseExpr + Helper.LibCall(com, "fable_quotation", "mk_if_then_else", Any, [ guard; thenE; elseE ]) + + | CurriedApply(applied, args, _typ, _r) -> + // Emit nested applications: Application(Application(f, a1), a2) + let appliedExpr = emitQuotedExpr com applied + + args + |> List.fold + (fun acc arg -> + let argExpr = emitQuotedExpr com arg + Helper.LibCall(com, "fable_quotation", "mk_app", Any, [ acc; argExpr ]) + ) + appliedExpr + + | Call(callee, info, _typ, _r) -> + let instanceExpr = + match info.ThisArg with + | Some thisArg -> emitQuotedExpr com thisArg + | None -> Value(Null Any, None) + + let methodName = + match info.MemberRef with + | Some(MemberRef(_, mInfo)) -> mInfo.CompiledName + | _ -> "unknown" + + let methodExpr = makeStrConst methodName + + let argExprs = info.Args |> List.map (emitQuotedExpr com) |> makeArray Any + + Helper.LibCall(com, "fable_quotation", "mk_call", Any, [ instanceExpr; methodExpr; argExprs ]) + + | Sequential exprs -> + match exprs with + | [] -> emitQuotedExpr com (Value(UnitConstant, None)) + | [ single ] -> emitQuotedExpr com single + | first :: rest -> + let restExpr = emitQuotedExpr com (Sequential rest) + let firstExpr = emitQuotedExpr com first + Helper.LibCall(com, "fable_quotation", "mk_sequential", Any, [ firstExpr; restExpr ]) + + | Operation(kind, _tags, _typ, _r) -> + // Represent operations as calls to the operator method + let opName, args = + match kind with + | Unary(op, operand) -> + let name = + match op with + | UnaryMinus -> "op_UnaryNegation" + | UnaryPlus -> "op_UnaryPlus" + | UnaryNot -> "op_LogicalNot" + | UnaryNotBitwise -> "op_OnesComplement" + | UnaryAddressOf -> "op_AddressOf" + + name, [ operand ] + | Binary(op, left, right) -> + let name = + match op with + | BinaryPlus -> "op_Addition" + | BinaryMinus -> "op_Subtraction" + | BinaryMultiply -> "op_Multiply" + | BinaryDivide -> "op_Division" + | BinaryModulus -> "op_Modulus" + | BinaryExponent -> "op_Exponentiation" + | BinaryOrBitwise -> "op_BitwiseOr" + | BinaryAndBitwise -> "op_BitwiseAnd" + | BinaryXorBitwise -> "op_ExclusiveOr" + | BinaryShiftLeft -> "op_LeftShift" + | BinaryShiftRightSignPropagating -> "op_RightShift" + | BinaryShiftRightZeroFill -> "op_UnsignedRightShift" + | BinaryEqual -> "op_Equality" + | BinaryUnequal -> "op_Inequality" + | BinaryLess -> "op_LessThan" + | BinaryLessOrEqual -> "op_LessThanOrEqual" + | BinaryGreater -> "op_GreaterThan" + | BinaryGreaterOrEqual -> "op_GreaterThanOrEqual" + + name, [ left; right ] + | Logical(op, left, right) -> + let name = + match op with + | LogicalAnd -> "op_BooleanAnd" + | LogicalOr -> "op_BooleanOr" + + name, [ left; right ] + + let methodExpr = makeStrConst opName + let instanceExpr = Value(Null Any, None) + + let argExprs = args |> List.map (emitQuotedExpr com) |> makeArray Any + + Helper.LibCall(com, "fable_quotation", "mk_call", Any, [ instanceExpr; methodExpr; argExprs ]) + + | Get(expr, kind, _typ, _r) -> + let target = emitQuotedExpr com expr + + match kind with + | TupleIndex index -> + Helper.LibCall(com, "fable_quotation", "mk_tuple_get", Any, [ target; makeIntConst index ]) + | UnionTag -> Helper.LibCall(com, "fable_quotation", "mk_union_tag", Any, [ target ]) + | UnionField info -> + Helper.LibCall(com, "fable_quotation", "mk_union_field", Any, [ target; makeIntConst info.FieldIndex ]) + | FieldGet info -> + Helper.LibCall(com, "fable_quotation", "mk_field_get", Any, [ target; makeStrConst info.Name ]) + | _ -> + // ListHead, ListTail, OptionValue, ExprGet — fall through + let msg = $"Unsupported quotation Get kind" + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) + + | Set(expr, kind, _typ, value, _r) -> + let target = emitQuotedExpr com expr + let valueExpr = emitQuotedExpr com value + + match kind with + | ValueSet -> + // Mutable variable set: expr is the ident, value is the new value + Helper.LibCall(com, "fable_quotation", "mk_var_set", Any, [ target; valueExpr ]) + | FieldSet fieldName -> + Helper.LibCall(com, "fable_quotation", "mk_field_set", Any, [ target; makeStrConst fieldName; valueExpr ]) + | _ -> + let msg = $"Unsupported quotation Set kind" + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) + + | TypeCast(innerExpr, _typ) -> + // Coerce/cast: just emit the inner expression for now + emitQuotedExpr com innerExpr + + | DecisionTree(matchExpr, targets) -> + // Simple pattern: if this is a single-target decision tree (e.g. let binding), + // emit the target body directly. Otherwise fall through to unsupported. + match targets with + | [ ([], body) ] -> emitQuotedExpr com body + | _ -> + let msg = "Unsupported quotation node: DecisionTree" + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) + + | DecisionTreeSuccess(idx, boundValues, _typ) -> + match boundValues with + | [] -> emitQuotedExpr com (Value(UnitConstant, None)) + | [ single ] -> emitQuotedExpr com single + | _ -> + let msg = "Unsupported quotation node: DecisionTreeSuccess" + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) + + | _ -> + // Unsupported node: emit an error value + let msg = $"Unsupported quotation node: %A{expr.GetType().Name}" + + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) + +and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocation option) : Expr = + match kind with + | BoolConstant b -> + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeBoolConst b; makeStrConst "bool" ]) + + | NumberConstant(NumberValue.Int32 i, _) -> + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeIntConst i; makeStrConst "int32" ]) + + | NumberConstant(NumberValue.Float64 f, _) -> + let floatExpr = Value(NumberConstant(NumberValue.Float64 f, NumberInfo.Empty), None) + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ floatExpr; makeStrConst "float64" ]) + + | StringConstant s -> + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst s; makeStrConst "string" ]) + + | UnitConstant -> + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ Value(UnitConstant, None); makeStrConst "unit" ]) + + | Null _ -> Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ Value(Null Any, None); makeStrConst "null" ]) + + | CharConstant c -> + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ Value(CharConstant c, None); makeStrConst "char" ]) + + | NewTuple(values, _isStruct) -> + let emittedValues = values |> List.map (emitQuotedExpr com) |> makeArray Any + + Helper.LibCall(com, "fable_quotation", "mk_new_tuple", Any, [ emittedValues ]) + + | NewUnion(values, tag, entRef, _genArgs) -> + let entName = + match com.TryGetEntity(entRef) with + | Some ent -> ent.FullName + | None -> entRef.FullName + + let emittedValues = values |> List.map (emitQuotedExpr com) |> makeArray Any + + Helper.LibCall( + com, + "fable_quotation", + "mk_new_union", + Any, + [ makeStrConst entName; makeIntConst tag; emittedValues ] + ) + + | NewRecord(values, entRef, _genArgs) -> + let fieldNames = + match com.TryGetEntity(entRef) with + | Some ent -> ent.FSharpFields |> List.map (fun f -> makeStrConst f.Name) |> makeArray Any + | None -> makeArray Any [] + + let emittedValues = values |> List.map (emitQuotedExpr com) |> makeArray Any + + Helper.LibCall(com, "fable_quotation", "mk_new_record", Any, [ fieldNames; emittedValues ]) + + | NewOption(value, _typ, _isStruct) -> + match value with + | Some v -> + let emitted = emitQuotedExpr com v + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ emitted; makeStrConst "option" ]) + | None -> + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ Value(Null Any, None); makeStrConst "option" ]) + + | NewList(headAndTail, _typ) -> + match headAndTail with + | Some(head, tail) -> + let headExpr = emitQuotedExpr com head + let tailExpr = emitQuotedExpr com tail + Helper.LibCall(com, "fable_quotation", "mk_new_list", Any, [ headExpr; tailExpr ]) + | None -> + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ Value(Null Any, None); makeStrConst "list" ]) + + | _ -> + // Fallback for other value kinds + let msg = $"Unsupported quotation value" + Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) + +and private typeToString (t: Type) : string = + match t with + | Boolean -> "bool" + | Number(Int32, _) -> "int32" + | Number(Float64, _) -> "float64" + | String -> "string" + | Unit -> "unit" + | Any -> "obj" + | LambdaType(argType, returnType) -> $"{typeToString argType} -> {typeToString returnType}" + | Tuple(genArgs, _) -> genArgs |> List.map typeToString |> String.concat " * " + | _ -> "obj" + +and private makeArray (elementType: Type) (elements: Expr list) : Expr = + Value(NewArray(ArrayValues elements, elementType, MutableArray), None) diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 6d939cdfa4..b0dcbc30da 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -3512,6 +3512,10 @@ module Util = mkUnitExpr () + | Fable.Quote _ -> + addError com [] None "Quotations are not yet supported for Rust target" + mkUnitExpr () + let rec tryFindEntryPoint (com: IRustCompiler) decl : string list option = match decl with | Fable.ModuleDeclaration decl -> diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index c4f5c794b4..7ae712fb4b 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -471,6 +471,18 @@ module Types = [] let refCell = "Microsoft.FSharp.Core.FSharpRef`1" + [] + let fsharpExpr = "Microsoft.FSharp.Quotations.FSharpExpr" + + [] + let fsharpExprGeneric = "Microsoft.FSharp.Quotations.FSharpExpr`1" + + [] + let fsharpVar = "Microsoft.FSharp.Quotations.FSharpVar" + + [] + let patternsModule = "Microsoft.FSharp.Quotations.PatternsModule" + [] let printfModule = "Microsoft.FSharp.Core.PrintfModule" @@ -1659,6 +1671,7 @@ module AST = let targets = targets |> List.map (fun (idents, v) -> idents, f v) DecisionTree(f expr, targets) | DecisionTreeSuccess(idx, boundValues, t) -> DecisionTreeSuccess(idx, List.map f boundValues, t) + | Quote _ -> e // Quoted expressions are data — don't optimize/transform their contents let rec visitFromInsideOut f e = visit (visitFromInsideOut f) e |> f @@ -1749,6 +1762,7 @@ module AST = | None -> body :: (Option.toList finalizer) | DecisionTree(expr, targets) -> expr :: (List.map snd targets) | DecisionTreeSuccess(_, boundValues, _) -> boundValues + | Quote _ -> [] // Quoted expressions are opaque data — don't traverse let deepExists (f: Expr -> bool) expr = let rec deepExistsInner (exprs: ResizeArray) = diff --git a/src/fable-library-beam/fable_quotation.erl b/src/fable-library-beam/fable_quotation.erl new file mode 100644 index 0000000000..7ad3737f7e --- /dev/null +++ b/src/fable-library-beam/fable_quotation.erl @@ -0,0 +1,171 @@ +-module(fable_quotation). +-export([ + mk_var/3, mk_var_expr/1, mk_value/2, mk_lambda/2, + mk_app/2, mk_let/3, mk_if_then_else/3, mk_call/3, + mk_sequential/2, mk_new_tuple/1, + mk_new_union/3, mk_new_record/2, mk_new_list/2, + mk_tuple_get/2, mk_union_tag/1, mk_union_field/2, + mk_field_get/2, mk_field_set/3, mk_var_set/2, + var_get_name/1, var_get_type/1, var_get_is_mutable/1, + get_type/1, + is_value/1, is_var/1, is_lambda/1, is_application/1, + is_let/1, is_if_then_else/1, is_call/1, is_sequential/1, + is_new_tuple/1, is_new_union/1, is_new_record/1, + is_tuple_get/1, is_field_get/1, + evaluate/1 +]). + +%% =================================================================== +%% Var constructor: {var, Name, Type, IsMutable} +%% =================================================================== + +mk_var(Name, Type, IsMutable) -> {var, Name, Type, IsMutable}. + +%% Var accessors +var_get_name({var, Name, _, _}) -> Name. +var_get_type({var, _, Type, _}) -> Type. +var_get_is_mutable({var, _, _, IsMutable}) -> IsMutable. + +%% =================================================================== +%% Expr node constructors: {expr, Tag, ...fields} +%% =================================================================== + +mk_var_expr(Var) -> {expr, var_expr, Var}. +mk_value(Value, Type) -> {expr, value, Value, Type}. +mk_lambda(Var, Body) -> {expr, lambda, Var, Body}. +mk_app(Func, Arg) -> {expr, application, Func, Arg}. +mk_let(Var, Value, Body) -> {expr, 'let', Var, Value, Body}. +mk_if_then_else(Guard, Then, Else) -> {expr, if_then_else, Guard, Then, Else}. +mk_call(Instance, Method, Args) -> {expr, call, Instance, Method, Args}. +mk_sequential(First, Second) -> {expr, sequential, First, Second}. +mk_new_tuple(Elements) -> {expr, new_tuple, Elements}. +mk_new_union(TypeName, Tag, Fields) -> {expr, new_union, TypeName, Tag, Fields}. +mk_new_record(FieldNames, Values) -> {expr, new_record, FieldNames, Values}. +mk_new_list(Head, Tail) -> {expr, new_list, Head, Tail}. +mk_tuple_get(Expr, Index) -> {expr, tuple_get, Expr, Index}. +mk_union_tag(Expr) -> {expr, union_tag, Expr}. +mk_union_field(Expr, FieldIndex) -> {expr, union_field, Expr, FieldIndex}. +mk_field_get(Expr, FieldName) -> {expr, field_get, Expr, FieldName}. +mk_field_set(Expr, FieldName, Value) -> {expr, field_set, Expr, FieldName, Value}. +mk_var_set(Target, Value) -> {expr, var_set, Target, Value}. + +%% =================================================================== +%% Expr accessor +%% =================================================================== + +get_type({expr, value, _, T}) -> T; +get_type({expr, lambda, {var, _, T, _}, _}) -> T; +get_type(_) -> <<"obj">>. + +%% =================================================================== +%% Pattern match helpers (for Quotations.Patterns active patterns) +%% Returns {some, Result} | undefined (matching Fable's option convention) +%% =================================================================== + +%% Value pattern: returns {Value, Type} +is_value({expr, value, V, T}) -> {V, T}; +is_value(_) -> undefined. + +%% Var pattern: returns the Var +is_var({expr, var_expr, Var}) -> Var; +is_var(_) -> undefined. + +%% Lambda pattern: returns {Var, Body} +is_lambda({expr, lambda, V, B}) -> {V, B}; +is_lambda(_) -> undefined. + +%% Application pattern: returns {Func, Arg} +is_application({expr, application, F, A}) -> {F, A}; +is_application(_) -> undefined. + +%% Let pattern: returns {Var, Value, Body} +is_let({expr, 'let', V, Val, B}) -> {V, Val, B}; +is_let(_) -> undefined. + +%% IfThenElse pattern: returns {Guard, Then, Else} +is_if_then_else({expr, if_then_else, G, T, E}) -> {G, T, E}; +is_if_then_else(_) -> undefined. + +%% Call pattern: returns {Instance, Method, Args} +is_call({expr, call, I, M, A}) -> {I, M, A}; +is_call(_) -> undefined. + +%% Sequential pattern: returns {First, Second} +is_sequential({expr, sequential, F, S}) -> {F, S}; +is_sequential(_) -> undefined. + +%% NewTuple pattern: returns Elements list +is_new_tuple({expr, new_tuple, E}) -> E; +is_new_tuple(_) -> undefined. + +%% NewUnionCase pattern: returns {TypeName, Tag, Fields} +is_new_union({expr, new_union, N, T, F}) -> {N, T, F}; +is_new_union(_) -> undefined. + +%% NewRecord pattern: returns {FieldNames, Values} +is_new_record({expr, new_record, N, V}) -> {N, V}; +is_new_record(_) -> undefined. + +%% TupleGet pattern: returns {Expr, Index} +is_tuple_get({expr, tuple_get, E, I}) -> {E, I}; +is_tuple_get(_) -> undefined. + +%% FieldGet/PropertyGet pattern: returns {Expr, FieldName} +is_field_get({expr, field_get, E, N}) -> {E, N}; +is_field_get(_) -> undefined. + +%% =================================================================== +%% Evaluation +%% =================================================================== + +evaluate(Expr) -> evaluate(Expr, #{}). + +evaluate({expr, value, V, _T}, _Env) -> V; +evaluate({expr, var_expr, {var, Name, _, _}}, Env) -> + maps:get(Name, Env); +evaluate({expr, lambda, {var, Name, _, _}, Body}, Env) -> + CapturedEnv = Env, + fun(Arg) -> evaluate(Body, CapturedEnv#{Name => Arg}) end; +evaluate({expr, application, Func, Arg}, Env) -> + F = evaluate(Func, Env), + A = evaluate(Arg, Env), + F(A); +evaluate({expr, 'let', {var, Name, _, _}, Value, Body}, Env) -> + V = evaluate(Value, Env), + evaluate(Body, Env#{Name => V}); +evaluate({expr, if_then_else, Guard, Then, Else}, Env) -> + case evaluate(Guard, Env) of + true -> evaluate(Then, Env); + _ -> evaluate(Else, Env) + end; +evaluate({expr, sequential, First, Second}, Env) -> + evaluate(First, Env), + evaluate(Second, Env); +evaluate({expr, new_tuple, Elements}, Env) -> + list_to_tuple([evaluate(E, Env) || E <- Elements]); +evaluate({expr, tuple_get, Inner, Index}, Env) -> + element(Index + 1, evaluate(Inner, Env)); +evaluate({expr, call, _Instance, Method, Args}, Env) -> + EvaluatedArgs = [evaluate(A, Env) || A <- Args], + apply_operator(Method, EvaluatedArgs). + +apply_operator(<<"op_Addition">>, [A, B]) -> A + B; +apply_operator(<<"op_Subtraction">>, [A, B]) -> A - B; +apply_operator(<<"op_Multiply">>, [A, B]) -> A * B; +apply_operator(<<"op_Division">>, [A, B]) -> A div B; +apply_operator(<<"op_Modulus">>, [A, B]) -> A rem B; +apply_operator(<<"op_UnaryNegation">>, [A]) -> -A; +apply_operator(<<"op_Equality">>, [A, B]) -> A =:= B; +apply_operator(<<"op_Inequality">>, [A, B]) -> A =/= B; +apply_operator(<<"op_LessThan">>, [A, B]) -> A < B; +apply_operator(<<"op_LessThanOrEqual">>, [A, B]) -> A =< B; +apply_operator(<<"op_GreaterThan">>, [A, B]) -> A > B; +apply_operator(<<"op_GreaterThanOrEqual">>, [A, B]) -> A >= B; +apply_operator(<<"op_BooleanAnd">>, [A, B]) -> A andalso B; +apply_operator(<<"op_BooleanOr">>, [A, B]) -> A orelse B; +apply_operator(<<"op_LogicalNot">>, [A]) -> not A; +apply_operator(<<"op_BitwiseOr">>, [A, B]) -> A bor B; +apply_operator(<<"op_BitwiseAnd">>, [A, B]) -> A band B; +apply_operator(<<"op_ExclusiveOr">>, [A, B]) -> A bxor B; +apply_operator(<<"op_LeftShift">>, [A, B]) -> A bsl B; +apply_operator(<<"op_RightShift">>, [A, B]) -> A bsr B. diff --git a/src/fable-library-py/fable_library/fable_quotation.py b/src/fable-library-py/fable_library/fable_quotation.py new file mode 100644 index 0000000000..9501296491 --- /dev/null +++ b/src/fable-library-py/fable_library/fable_quotation.py @@ -0,0 +1,462 @@ +"""F# Quotation runtime support for Fable Python target. + +Provides dataclass-based representations of F# quotation AST nodes +and pattern matching helpers compatible with Fable's option convention +(None = no match, tuple = match). +""" + +from __future__ import annotations + +import operator +from dataclasses import dataclass +from typing import Any + + +# =================================================================== +# Var: represents an F# quotation variable +# =================================================================== + + +@dataclass +class Var: + name: str + type: str + is_mutable: bool + + +def mk_var(name: str, type: str, is_mutable: bool = False) -> Var: + return Var(name, type, is_mutable) + + +def var_get_name(var: Var) -> str: + return var.name + + +def var_get_type(var: Var) -> str: + return var.type + + +def var_get_is_mutable(var: Var) -> bool: + return var.is_mutable + + +# =================================================================== +# Expr nodes: tagged dataclasses for each quotation expression kind +# =================================================================== + + +@dataclass +class ExprValue: + value: Any + type: str + + +@dataclass +class ExprVarExpr: + var: Var + + +@dataclass +class ExprLambda: + var: Var + body: Any # Expr node + + +@dataclass +class ExprApplication: + func: Any # Expr node + arg: Any # Expr node + + +@dataclass +class ExprLet: + var: Var + value: Any # Expr node + body: Any # Expr node + + +@dataclass +class ExprIfThenElse: + guard: Any # Expr node + then_expr: Any # Expr node + else_expr: Any # Expr node + + +@dataclass +class ExprCall: + instance: Any + method: str + args: list[Any] + + +@dataclass +class ExprSequential: + first: Any # Expr node + second: Any # Expr node + + +@dataclass +class ExprNewTuple: + elements: list[Any] + + +@dataclass +class ExprNewUnion: + type_name: str + tag: int + fields: list[Any] + + +@dataclass +class ExprNewRecord: + field_names: list[str] + values: list[Any] + + +@dataclass +class ExprNewList: + head: Any # Expr node + tail: Any # Expr node + + +@dataclass +class ExprTupleGet: + expr: Any # Expr node + index: int + + +@dataclass +class ExprUnionTag: + expr: Any # Expr node + + +@dataclass +class ExprUnionField: + expr: Any # Expr node + field_index: int + + +@dataclass +class ExprFieldGet: + expr: Any # Expr node + field_name: str + + +@dataclass +class ExprFieldSet: + expr: Any # Expr node + field_name: str + value: Any # Expr node + + +@dataclass +class ExprVarSet: + target: Any # Expr node + value: Any # Expr node + + +# Expr is the union of all expression node types +type Expr = ( + ExprValue + | ExprVarExpr + | ExprLambda + | ExprApplication + | ExprLet + | ExprIfThenElse + | ExprCall + | ExprSequential + | ExprNewTuple + | ExprNewUnion + | ExprNewRecord + | ExprNewList + | ExprTupleGet + | ExprUnionTag + | ExprUnionField + | ExprFieldGet + | ExprFieldSet + | ExprVarSet +) + + +# =================================================================== +# Constructors +# =================================================================== + + +def mk_value(value: Any, type: str) -> ExprValue: + return ExprValue(value, type) + + +def mk_var_expr(var: Var) -> ExprVarExpr: + return ExprVarExpr(var) + + +def mk_lambda(var: Var, body: Expr) -> ExprLambda: + return ExprLambda(var, body) + + +def mk_app(func: Expr, arg: Expr) -> ExprApplication: + return ExprApplication(func, arg) + + +def mk_let(var: Var, value: Expr, body: Expr) -> ExprLet: + return ExprLet(var, value, body) + + +def mk_if_then_else(guard: Expr, then_expr: Expr, else_expr: Expr) -> ExprIfThenElse: + return ExprIfThenElse(guard, then_expr, else_expr) + + +def mk_call(instance: Any, method: str, args: list[Any]) -> ExprCall: + return ExprCall(instance, method, args) + + +def mk_sequential(first: Expr, second: Expr) -> ExprSequential: + return ExprSequential(first, second) + + +def mk_new_tuple(elements: list[Any]) -> ExprNewTuple: + return ExprNewTuple(elements) + + +def mk_new_union(type_name: str, tag: int, fields: list[Any]) -> ExprNewUnion: + return ExprNewUnion(type_name, tag, fields) + + +def mk_new_record(field_names: list[str], values: list[Any]) -> ExprNewRecord: + return ExprNewRecord(field_names, values) + + +def mk_new_list(head: Expr, tail: Expr) -> ExprNewList: + return ExprNewList(head, tail) + + +def mk_tuple_get(expr: Expr, index: int) -> ExprTupleGet: + return ExprTupleGet(expr, index) + + +def mk_union_tag(expr: Expr) -> ExprUnionTag: + return ExprUnionTag(expr) + + +def mk_union_field(expr: Expr, field_index: int) -> ExprUnionField: + return ExprUnionField(expr, field_index) + + +def mk_field_get(expr: Expr, field_name: str) -> ExprFieldGet: + return ExprFieldGet(expr, field_name) + + +def mk_field_set(expr: Expr, field_name: str, value: Expr) -> ExprFieldSet: + return ExprFieldSet(expr, field_name, value) + + +def mk_var_set(target: Expr, value: Expr) -> ExprVarSet: + return ExprVarSet(target, value) + + +# =================================================================== +# Accessor +# =================================================================== + + +def get_type(expr: Expr) -> str: + if isinstance(expr, ExprValue): + return expr.type + if isinstance(expr, ExprLambda): + return expr.var.type + return "obj" + + +# =================================================================== +# Pattern match helpers +# Returns None (no match) or a tuple (match), following Fable's +# option convention for active patterns. +# =================================================================== + + +def is_value(expr: Expr) -> tuple[Any, str] | None: + if isinstance(expr, ExprValue): + return (expr.value, expr.type) + return None + + +def is_var(expr: Expr) -> Var | None: + if isinstance(expr, ExprVarExpr): + return expr.var + return None + + +def is_lambda(expr: Expr) -> tuple[Var, Expr] | None: + if isinstance(expr, ExprLambda): + return (expr.var, expr.body) + return None + + +def is_application(expr: Expr) -> tuple[Expr, Expr] | None: + if isinstance(expr, ExprApplication): + return (expr.func, expr.arg) + return None + + +def is_let(expr: Expr) -> tuple[Var, Expr, Expr] | None: + if isinstance(expr, ExprLet): + return (expr.var, expr.value, expr.body) + return None + + +def is_if_then_else(expr: Expr) -> tuple[Expr, Expr, Expr] | None: + if isinstance(expr, ExprIfThenElse): + return (expr.guard, expr.then_expr, expr.else_expr) + return None + + +def is_call(expr: Expr) -> tuple[Any, str, list[Any]] | None: + if isinstance(expr, ExprCall): + return (expr.instance, expr.method, expr.args) + return None + + +def is_sequential(expr: Expr) -> tuple[Expr, Expr] | None: + if isinstance(expr, ExprSequential): + return (expr.first, expr.second) + return None + + +def is_new_tuple(expr: Expr) -> list[Any] | None: + if isinstance(expr, ExprNewTuple): + return expr.elements + return None + + +def is_new_union(expr: Expr) -> tuple[str, int, list[Any]] | None: + if isinstance(expr, ExprNewUnion): + return (expr.type_name, expr.tag, expr.fields) + return None + + +def is_new_record(expr: Expr) -> tuple[list[str], list[Any]] | None: + if isinstance(expr, ExprNewRecord): + return (expr.field_names, expr.values) + return None + + +def is_tuple_get(expr: Expr) -> tuple[Expr, int] | None: + if isinstance(expr, ExprTupleGet): + return (expr.expr, expr.index) + return None + + +def is_field_get(expr: Expr) -> tuple[Expr, str] | None: + if isinstance(expr, ExprFieldGet): + return (expr.expr, expr.field_name) + return None + + +# =================================================================== +# Evaluation +# =================================================================== + +_OPERATORS: dict[str, Any] = { + "op_Addition": operator.add, + "op_Subtraction": operator.sub, + "op_Multiply": operator.mul, + "op_Division": operator.floordiv, + "op_Modulus": operator.mod, + "op_Exponentiation": operator.pow, + "op_UnaryNegation": operator.neg, + "op_UnaryPlus": operator.pos, + "op_LogicalNot": operator.not_, + "op_BitwiseOr": operator.or_, + "op_BitwiseAnd": operator.and_, + "op_ExclusiveOr": operator.xor, + "op_LeftShift": operator.lshift, + "op_RightShift": operator.rshift, + "op_Equality": operator.eq, + "op_Inequality": operator.ne, + "op_LessThan": operator.lt, + "op_LessThanOrEqual": operator.le, + "op_GreaterThan": operator.gt, + "op_GreaterThanOrEqual": operator.ge, + "op_BooleanAnd": lambda a, b: a and b, + "op_BooleanOr": lambda a, b: a or b, +} + + +def evaluate(expr: Expr, env: dict[str, Any] | None = None) -> Any: + """Evaluate a quotation AST node and return the resulting value.""" + if env is None: + env = {} + + match expr: + case ExprValue(value=value): + return value + + case ExprVarExpr(var=var): + if var.name in env: + return env[var.name] + raise ValueError(f"Unbound variable: {var.name}") + + case ExprLambda(var=var, body=body): + captured_env = env.copy() + + def closure(arg: Any) -> Any: + new_env = captured_env.copy() + new_env[var.name] = arg + return evaluate(body, new_env) + + return closure + + case ExprApplication(func=func, arg=arg): + return evaluate(func, env)(evaluate(arg, env)) + + case ExprLet(var=var, value=value, body=body): + new_env = env | {var.name: evaluate(value, env)} + return evaluate(body, new_env) + + case ExprIfThenElse(guard=guard, then_expr=then_expr, else_expr=else_expr): + if evaluate(guard, env): + return evaluate(then_expr, env) + return evaluate(else_expr, env) + + case ExprSequential(first=first, second=second): + evaluate(first, env) + return evaluate(second, env) + + case ExprNewTuple(elements=elements): + return tuple(evaluate(e, env) for e in elements) + + case ExprCall(method=method, args=args): + evaluated_args = [evaluate(a, env) for a in args] + if method in _OPERATORS: + return _OPERATORS[method](*evaluated_args) + raise ValueError(f"Unknown method: {method}") + + case ExprTupleGet(expr=inner, index=index): + return evaluate(inner, env)[index] + + case ExprNewUnion(tag=tag, fields=fields): + return (tag, *[evaluate(f, env) for f in fields]) + + case ExprNewRecord(field_names=names, values=values): + return {n: evaluate(v, env) for n, v in zip(names, values)} + + case ExprNewList(head=head, tail=tail): + return [evaluate(head, env), *evaluate(tail, env)] + + case ExprVarSet(target=target, value=value): + match target: + case ExprVarExpr(var=var): + env[var.name] = evaluate(value, env) + return None + case _: + raise ValueError("VarSet target must be a variable") + + case ExprFieldGet(expr=inner, field_name=name): + obj = evaluate(inner, env) + if isinstance(obj, dict): + return obj[name] + return getattr(obj, name) + + case _: + raise ValueError(f"Cannot evaluate expression: {type(expr).__name__}") diff --git a/tests/Beam/Fable.Tests.Beam.fsproj b/tests/Beam/Fable.Tests.Beam.fsproj index 35c862476a..0b5654120d 100644 --- a/tests/Beam/Fable.Tests.Beam.fsproj +++ b/tests/Beam/Fable.Tests.Beam.fsproj @@ -85,6 +85,7 @@ + diff --git a/tests/Beam/QuotationTests.fs b/tests/Beam/QuotationTests.fs new file mode 100644 index 0000000000..30232d4fe0 --- /dev/null +++ b/tests/Beam/QuotationTests.fs @@ -0,0 +1,140 @@ +module Fable.Tests.Quotation + +open Fable.Tests.Util +open Util.Testing +open Microsoft.FSharp.Quotations +open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Linq.RuntimeHelpers + +[] +let ``test Simple integer value quotation`` () = + let q = <@ 42 @> + match q with + | Value(v, _t) -> equal 42 (v :?> int) + | _ -> failwith "Expected Value" + +[] +let ``test Boolean value quotation`` () = + let q = <@ true @> + match q with + | Value(v, _t) -> equal true (v :?> bool) + | _ -> failwith "Expected Value" + +[] +let ``test String value quotation`` () = + let q = <@ "hello" @> + match q with + | Value(v, _t) -> equal "hello" (v :?> string) + | _ -> failwith "Expected Value" + +[] +let ``test Lambda quotation`` () = + let q = <@ fun x -> x + 1 @> + match q with + | Lambda(v, _body) -> equal "x" v.Name + | _ -> failwith "Expected Lambda" + +[] +let ``test Let binding quotation`` () = + let q = <@ let x = 5 in x @> + match q with + | Let(v, Value(value, _), _) -> + equal "x" v.Name + equal 5 (value :?> int) + | _ -> failwith "Expected Let" + +[] +let ``test IfThenElse quotation`` () = + let q = <@ if true then 1 else 2 @> + match q with + | IfThenElse(_, _, _) -> () + | _ -> failwith "Expected IfThenElse" + +[] +let ``test Application quotation`` () = + let q = <@ (fun x -> x) 42 @> + match q with + | Application(Lambda _, Value _) -> () + | _ -> failwith "Expected Application of Lambda" + +[] +let ``test Evaluate simple value`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ 42 @> + equal 42 (result :?> int) + +[] +let ``test Evaluate addition`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ 1 + 2 @> + equal 3 (result :?> int) + +[] +let ``test Evaluate let binding`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ let x = 10 in x + 5 @> + equal 15 (result :?> int) + +[] +let ``test Evaluate if then else`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ if true then 1 else 2 @> + equal 1 (result :?> int) + +[] +let ``test Evaluate lambda application`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ (fun x -> x + 1) 5 @> + equal 6 (result :?> int) + +// --- Pattern matching: new node types --- + +[] +let ``test NewTuple quotation`` () = + let q = <@ (1, 2, 3) @> + match q with + | NewTuple(exprs) -> equal 3 exprs.Length + | _ -> failwith "Expected NewTuple" + +[] +let ``test Sequential quotation`` () = + let q = <@ (); 42 @> + match q with + | Sequential(_, Value(v, _)) -> equal 42 (v :?> int) + | _ -> failwith "Expected Sequential" + +[] +let ``test Var quotation`` () = + let q = <@ fun x -> x @> + match q with + | Lambda(v, Var(v2)) -> + equal v.Name v2.Name + | _ -> failwith "Expected Lambda with Var body" + +// --- Evaluation: more operators --- + +[] +let ``test Evaluate subtraction`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ 10 - 3 @> + equal 7 (result :?> int) + +[] +let ``test Evaluate multiplication`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ 6 * 7 @> + equal 42 (result :?> int) + +[] +let ``test Evaluate comparison`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ 5 > 3 @> + equal true (result :?> bool) + +[] +let ``test Evaluate nested let bindings`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ let x = 3 in let y = 4 in x * y @> + equal 12 (result :?> int) + +[] +let ``test Evaluate nested lambda`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ (fun x -> (fun y -> x + y)) 3 4 @> + equal 7 (result :?> int) + +[] +let ``test Evaluate tuple`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ (1, 2) @> + let t = result :?> (int * int) + equal (1, 2) t diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index e8c0644094..6585df40b6 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -82,6 +82,7 @@ + diff --git a/tests/Python/TestQuotation.fs b/tests/Python/TestQuotation.fs new file mode 100644 index 0000000000..6af9a473a6 --- /dev/null +++ b/tests/Python/TestQuotation.fs @@ -0,0 +1,155 @@ +module Fable.Tests.Quotation + +open Fable.Tests.Util +open Util.Testing +open Microsoft.FSharp.Quotations +open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Linq.RuntimeHelpers + +[] +let ``test Simple integer value quotation`` () = + let q = <@ 42 @> + match q with + | Value(v, _t) -> equal 42 (v :?> int) + | _ -> failwith "Expected Value" + +[] +let ``test Boolean value quotation`` () = + let q = <@ true @> + match q with + | Value(v, _t) -> equal true (v :?> bool) + | _ -> failwith "Expected Value" + +[] +let ``test String value quotation`` () = + let q = <@ "hello" @> + match q with + | Value(v, _t) -> equal "hello" (v :?> string) + | _ -> failwith "Expected Value" + +[] +let ``test Lambda quotation`` () = + let q = <@ fun x -> x + 1 @> + match q with + | Lambda(v, _body) -> equal "x" v.Name + | _ -> failwith "Expected Lambda" + +[] +let ``test Let binding quotation`` () = + let q = <@ let x = 5 in x @> + match q with + | Let(v, Value(value, _), _) -> + equal "x" v.Name + equal 5 (value :?> int) + | _ -> failwith "Expected Let" + +[] +let ``test IfThenElse quotation`` () = + let q = <@ if true then 1 else 2 @> + match q with + | IfThenElse(_, _, _) -> () + | _ -> failwith "Expected IfThenElse" + +[] +let ``test Application quotation`` () = + let q = <@ (fun x -> x) 42 @> + match q with + | Application(Lambda _, Value _) -> () + | _ -> failwith "Expected Application of Lambda" + +[] +let ``test Evaluate simple value`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ 42 @> + equal 42 (result :?> int) + +[] +let ``test Evaluate boolean value`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ true @> + equal true (result :?> bool) + +[] +let ``test Evaluate string value`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ "hello" @> + equal "hello" (result :?> string) + +[] +let ``test Evaluate addition`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ 1 + 2 @> + equal 3 (result :?> int) + +[] +let ``test Evaluate let binding`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ let x = 10 in x + 5 @> + equal 15 (result :?> int) + +[] +let ``test Evaluate if then else true branch`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ if true then 1 else 2 @> + equal 1 (result :?> int) + +[] +let ``test Evaluate if then else false branch`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ if false then 1 else 2 @> + equal 2 (result :?> int) + +[] +let ``test Evaluate lambda application`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ (fun x -> x + 1) 5 @> + equal 6 (result :?> int) + +// --- Pattern matching: new node types --- + +[] +let ``test NewTuple quotation`` () = + let q = <@ (1, 2, 3) @> + match q with + | NewTuple(exprs) -> equal 3 exprs.Length + | _ -> failwith "Expected NewTuple" + +[] +let ``test Sequential quotation`` () = + let q = <@ (); 42 @> + match q with + | Sequential(_, Value(v, _)) -> equal 42 (v :?> int) + | _ -> failwith "Expected Sequential" + +[] +let ``test Var quotation`` () = + let q = <@ fun x -> x @> + match q with + | Lambda(v, Var(v2)) -> + equal v.Name v2.Name + | _ -> failwith "Expected Lambda with Var body" + +// --- Evaluation: more operators --- + +[] +let ``test Evaluate subtraction`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ 10 - 3 @> + equal 7 (result :?> int) + +[] +let ``test Evaluate multiplication`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ 6 * 7 @> + equal 42 (result :?> int) + +[] +let ``test Evaluate comparison`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ 5 > 3 @> + equal true (result :?> bool) + +[] +let ``test Evaluate nested let bindings`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ let x = 3 in let y = 4 in x * y @> + equal 12 (result :?> int) + +[] +let ``test Evaluate nested lambda`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ (fun x -> (fun y -> x + y)) 3 4 @> + equal 7 (result :?> int) + +[] +let ``test Evaluate tuple`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ (1, 2) @> + let t = result :?> (int * int) + equal (1, 2) t From 755b9c80a1f8f2c2610180d6584cd1d3b2de73dc Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 14 Mar 2026 08:54:57 +0100 Subject: [PATCH 02/15] Fix Beam quotation tests: dereference array Refs in evaluate and pattern 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) --- src/Fable.Transforms/QuotationEmitter.fs | 2 +- src/fable-library-beam/fable_quotation.erl | 24 +++++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Fable.Transforms/QuotationEmitter.fs b/src/Fable.Transforms/QuotationEmitter.fs index 35db3e66fe..767ce6cf85 100644 --- a/src/Fable.Transforms/QuotationEmitter.fs +++ b/src/Fable.Transforms/QuotationEmitter.fs @@ -335,4 +335,4 @@ and private typeToString (t: Type) : string = | _ -> "obj" and private makeArray (elementType: Type) (elements: Expr list) : Expr = - Value(NewArray(ArrayValues elements, elementType, MutableArray), None) + Value(NewArray(ArrayValues elements, elementType, ImmutableArray), None) diff --git a/src/fable-library-beam/fable_quotation.erl b/src/fable-library-beam/fable_quotation.erl index 7ad3737f7e..6efe390275 100644 --- a/src/fable-library-beam/fable_quotation.erl +++ b/src/fable-library-beam/fable_quotation.erl @@ -86,24 +86,24 @@ is_let(_) -> undefined. is_if_then_else({expr, if_then_else, G, T, E}) -> {G, T, E}; is_if_then_else(_) -> undefined. -%% Call pattern: returns {Instance, Method, Args} -is_call({expr, call, I, M, A}) -> {I, M, A}; +%% Call pattern: returns {Instance, Method, Args} (dereference Args) +is_call({expr, call, I, M, A}) -> {I, M, deref(A)}; is_call(_) -> undefined. %% Sequential pattern: returns {First, Second} is_sequential({expr, sequential, F, S}) -> {F, S}; is_sequential(_) -> undefined. -%% NewTuple pattern: returns Elements list -is_new_tuple({expr, new_tuple, E}) -> E; +%% NewTuple pattern: returns Elements list (dereference if stored as Ref) +is_new_tuple({expr, new_tuple, E}) -> deref(E); is_new_tuple(_) -> undefined. -%% NewUnionCase pattern: returns {TypeName, Tag, Fields} -is_new_union({expr, new_union, N, T, F}) -> {N, T, F}; +%% NewUnionCase pattern: returns {TypeName, Tag, Fields} (dereference Fields) +is_new_union({expr, new_union, N, T, F}) -> {N, T, deref(F)}; is_new_union(_) -> undefined. -%% NewRecord pattern: returns {FieldNames, Values} -is_new_record({expr, new_record, N, V}) -> {N, V}; +%% NewRecord pattern: returns {FieldNames, Values} (dereference both) +is_new_record({expr, new_record, N, V}) -> {deref(N), deref(V)}; is_new_record(_) -> undefined. %% TupleGet pattern: returns {Expr, Index} @@ -120,6 +120,10 @@ is_field_get(_) -> undefined. evaluate(Expr) -> evaluate(Expr, #{}). +%% Dereference a Fable array (stored as a Ref in the process dictionary) to a plain list. +deref(Ref) when is_reference(Ref) -> get(Ref); +deref(List) when is_list(List) -> List. + evaluate({expr, value, V, _T}, _Env) -> V; evaluate({expr, var_expr, {var, Name, _, _}}, Env) -> maps:get(Name, Env); @@ -142,11 +146,11 @@ evaluate({expr, sequential, First, Second}, Env) -> evaluate(First, Env), evaluate(Second, Env); evaluate({expr, new_tuple, Elements}, Env) -> - list_to_tuple([evaluate(E, Env) || E <- Elements]); + list_to_tuple([evaluate(E, Env) || E <- deref(Elements)]); evaluate({expr, tuple_get, Inner, Index}, Env) -> element(Index + 1, evaluate(Inner, Env)); evaluate({expr, call, _Instance, Method, Args}, Env) -> - EvaluatedArgs = [evaluate(A, Env) || A <- Args], + EvaluatedArgs = [evaluate(A, Env) || A <- deref(Args)], apply_operator(Method, EvaluatedArgs). apply_operator(<<"op_Addition">>, [A, B]) -> A + B; From 1302734c4980d84d5124d0a37da9dbcbb6456c56 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 14 Mar 2026 09:09:42 +0100 Subject: [PATCH 03/15] Fix is_new_tuple to return FSharpList matching .NET quotation API 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) --- src/fable-library-py/fable_library/fable_quotation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/fable-library-py/fable_library/fable_quotation.py b/src/fable-library-py/fable_library/fable_quotation.py index 9501296491..db048b95e8 100644 --- a/src/fable-library-py/fable_library/fable_quotation.py +++ b/src/fable-library-py/fable_library/fable_quotation.py @@ -11,6 +11,8 @@ from dataclasses import dataclass from typing import Any +from .list import FSharpList, of_array + # =================================================================== # Var: represents an F# quotation variable @@ -323,9 +325,9 @@ def is_sequential(expr: Expr) -> tuple[Expr, Expr] | None: return None -def is_new_tuple(expr: Expr) -> list[Any] | None: +def is_new_tuple(expr: Expr) -> FSharpList[Any] | None: if isinstance(expr, ExprNewTuple): - return expr.elements + return of_array(expr.elements) return None From 0a8f667ea7c741b2969326c5950f9fa484782dda Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 14 Mar 2026 09:12:13 +0100 Subject: [PATCH 04/15] Fix interpolated string warnings in QuotationEmitter.fs Remove unnecessary $ prefix from plain strings and add %s format specifier to typeToString interpolation. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Fable.Transforms/QuotationEmitter.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Fable.Transforms/QuotationEmitter.fs b/src/Fable.Transforms/QuotationEmitter.fs index 767ce6cf85..fda9cceb81 100644 --- a/src/Fable.Transforms/QuotationEmitter.fs +++ b/src/Fable.Transforms/QuotationEmitter.fs @@ -202,7 +202,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = Helper.LibCall(com, "fable_quotation", "mk_field_get", Any, [ target; makeStrConst info.Name ]) | _ -> // ListHead, ListTail, OptionValue, ExprGet — fall through - let msg = $"Unsupported quotation Get kind" + let msg = "Unsupported quotation Get kind" Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) | Set(expr, kind, _typ, value, _r) -> @@ -216,7 +216,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | FieldSet fieldName -> Helper.LibCall(com, "fable_quotation", "mk_field_set", Any, [ target; makeStrConst fieldName; valueExpr ]) | _ -> - let msg = $"Unsupported quotation Set kind" + let msg = "Unsupported quotation Set kind" Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) | TypeCast(innerExpr, _typ) -> @@ -319,7 +319,7 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio | _ -> // Fallback for other value kinds - let msg = $"Unsupported quotation value" + let msg = "Unsupported quotation value" Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) and private typeToString (t: Type) : string = @@ -330,7 +330,7 @@ and private typeToString (t: Type) : string = | String -> "string" | Unit -> "unit" | Any -> "obj" - | LambdaType(argType, returnType) -> $"{typeToString argType} -> {typeToString returnType}" + | LambdaType(argType, returnType) -> $"%s{typeToString argType} -> %s{typeToString returnType}" | Tuple(genArgs, _) -> genArgs |> List.map typeToString |> String.concat " * " | _ -> "obj" From d93a06e0effe56373e05b4a7fb5d3faf8e8f5557 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 14 Mar 2026 09:21:16 +0100 Subject: [PATCH 05/15] Fix standalone build and Python type annotations - 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) --- .../fable_library/fable_quotation.py | 25 ++++++++++--------- .../src/Fable.Standalone.fsproj | 1 + 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/fable-library-py/fable_library/fable_quotation.py b/src/fable-library-py/fable_library/fable_quotation.py index db048b95e8..bf144c7bed 100644 --- a/src/fable-library-py/fable_library/fable_quotation.py +++ b/src/fable-library-py/fable_library/fable_quotation.py @@ -11,6 +11,7 @@ from dataclasses import dataclass from typing import Any +from .array_ import Array from .list import FSharpList, of_array @@ -88,7 +89,7 @@ class ExprIfThenElse: class ExprCall: instance: Any method: str - args: list[Any] + args: Array[Any] @dataclass @@ -99,20 +100,20 @@ class ExprSequential: @dataclass class ExprNewTuple: - elements: list[Any] + elements: Array[Any] @dataclass class ExprNewUnion: type_name: str tag: int - fields: list[Any] + fields: Array[Any] @dataclass class ExprNewRecord: - field_names: list[str] - values: list[Any] + field_names: Array[str] + values: Array[Any] @dataclass @@ -209,7 +210,7 @@ def mk_if_then_else(guard: Expr, then_expr: Expr, else_expr: Expr) -> ExprIfThen return ExprIfThenElse(guard, then_expr, else_expr) -def mk_call(instance: Any, method: str, args: list[Any]) -> ExprCall: +def mk_call(instance: Any, method: str, args: Array[Any]) -> ExprCall: return ExprCall(instance, method, args) @@ -217,15 +218,15 @@ def mk_sequential(first: Expr, second: Expr) -> ExprSequential: return ExprSequential(first, second) -def mk_new_tuple(elements: list[Any]) -> ExprNewTuple: +def mk_new_tuple(elements: Array[Any]) -> ExprNewTuple: return ExprNewTuple(elements) -def mk_new_union(type_name: str, tag: int, fields: list[Any]) -> ExprNewUnion: +def mk_new_union(type_name: str, tag: int, fields: Array[Any]) -> ExprNewUnion: return ExprNewUnion(type_name, tag, fields) -def mk_new_record(field_names: list[str], values: list[Any]) -> ExprNewRecord: +def mk_new_record(field_names: Array[str], values: Array[Any]) -> ExprNewRecord: return ExprNewRecord(field_names, values) @@ -313,7 +314,7 @@ def is_if_then_else(expr: Expr) -> tuple[Expr, Expr, Expr] | None: return None -def is_call(expr: Expr) -> tuple[Any, str, list[Any]] | None: +def is_call(expr: Expr) -> tuple[Any, str, Array[Any]] | None: if isinstance(expr, ExprCall): return (expr.instance, expr.method, expr.args) return None @@ -331,13 +332,13 @@ def is_new_tuple(expr: Expr) -> FSharpList[Any] | None: return None -def is_new_union(expr: Expr) -> tuple[str, int, list[Any]] | None: +def is_new_union(expr: Expr) -> tuple[str, int, Array[Any]] | None: if isinstance(expr, ExprNewUnion): return (expr.type_name, expr.tag, expr.fields) return None -def is_new_record(expr: Expr) -> tuple[list[str], list[Any]] | None: +def is_new_record(expr: Expr) -> tuple[Array[str], Array[Any]] | None: if isinstance(expr, ExprNewRecord): return (expr.field_names, expr.values) return None diff --git a/src/fable-standalone/src/Fable.Standalone.fsproj b/src/fable-standalone/src/Fable.Standalone.fsproj index a18075bfb6..db9ce50b47 100644 --- a/src/fable-standalone/src/Fable.Standalone.fsproj +++ b/src/fable-standalone/src/Fable.Standalone.fsproj @@ -38,6 +38,7 @@ + From 264be9f350f9c3adae4711b425dc6024bb41ac02 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 14 Mar 2026 09:55:21 +0100 Subject: [PATCH 06/15] Add FSharpExpr instance methods: GetFreeVars, Substitute, expr_to_string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- CLAUDE.md | 2 +- src/Fable.Transforms/Beam/Replacements.fs | 10 + src/Fable.Transforms/Python/Replacements.fs | 10 + src/fable-library-beam/fable_quotation.erl | 119 +++++++++++- .../fable_library/fable_quotation.py | 172 +++++++++++++++++- tests/Beam/QuotationTests.fs | 8 + tests/Python/TestQuotation.fs | 8 + 7 files changed, 326 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 86c151ca5c..b76ab2ad3b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,7 +110,7 @@ All transpiled Python code is type-checked with Pyright at standard settings (`. Quicktest is also preferred when adding debug output (e.g., `printfn` in compiler code) since running full tests with debug prints produces too much output. -**Test suites** are in `tests//` (e.g., `tests/Python/`, `tests/Js/Main/`). Tests are first run on .NET, then transpiled to `temp/tests//` (e.g., `temp/tests/py/`, `temp/tests/js/`) and executed with the target's test runner. Full test runs take several minutes. +**Test suites** are in `tests//` (e.g., `tests/Python/`, `tests/Js/Main/`). Tests are first run on .NET, then transpiled to `temp/tests//` (e.g., `temp/tests/py/`, `temp/tests/js/`) and executed with the target's test runner. Full test runs take several minutes. You can and should run `./build.sh test ` yourself after making changes — use `--skip-fable-library` when only compiler code changed to speed things up. When adding a test, check if other targets already have a test for the same case (e.g., look in `tests/Js/Main/` before adding to `tests/Python/`) — reuse or adapt existing test patterns where possible. diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index ea57cfcddf..a5d1667626 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -4617,6 +4617,16 @@ let private quotationExprs | "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) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index c9fedf16b9..dd9fd9e2f7 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -3795,6 +3795,16 @@ let private quotationExprs | "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) diff --git a/src/fable-library-beam/fable_quotation.erl b/src/fable-library-beam/fable_quotation.erl index 6efe390275..85893bbd7e 100644 --- a/src/fable-library-beam/fable_quotation.erl +++ b/src/fable-library-beam/fable_quotation.erl @@ -12,7 +12,8 @@ is_let/1, is_if_then_else/1, is_call/1, is_sequential/1, is_new_tuple/1, is_new_union/1, is_new_record/1, is_tuple_get/1, is_field_get/1, - evaluate/1 + evaluate/1, + expr_to_string/1, get_free_vars/1, substitute/2 ]). %% =================================================================== @@ -173,3 +174,119 @@ apply_operator(<<"op_BitwiseAnd">>, [A, B]) -> A band B; apply_operator(<<"op_ExclusiveOr">>, [A, B]) -> A bxor B; apply_operator(<<"op_LeftShift">>, [A, B]) -> A bsl B; apply_operator(<<"op_RightShift">>, [A, B]) -> A bsr B. + +%% =================================================================== +%% FSharpExpr instance methods +%% =================================================================== + +expr_to_string({expr, value, V, T}) -> + case T of + <<"string">> -> iolist_to_binary(["\"", V, "\""]); + <<"bool">> -> case V of true -> <<"true">>; _ -> <<"false">> end; + <<"unit">> -> <<"()">>; + _ -> iolist_to_binary(io_lib:format("~w", [V])) + end; +expr_to_string({expr, var_expr, {var, Name, _, _}}) -> Name; +expr_to_string({expr, lambda, {var, Name, _, _}, Body}) -> + iolist_to_binary(["fun ", Name, " -> ", expr_to_string(Body)]); +expr_to_string({expr, application, Func, Arg}) -> + iolist_to_binary([expr_to_string(Func), " ", expr_to_string(Arg)]); +expr_to_string({expr, 'let', {var, Name, _, _}, Value, Body}) -> + iolist_to_binary(["let ", Name, " = ", expr_to_string(Value), " in ", expr_to_string(Body)]); +expr_to_string({expr, if_then_else, Guard, Then, Else}) -> + iolist_to_binary(["if ", expr_to_string(Guard), " then ", expr_to_string(Then), " else ", expr_to_string(Else)]); +expr_to_string({expr, call, _, Method, Args}) -> + ArgsList = deref(Args), + ArgsStr = lists:join(", ", [expr_to_string(A) || A <- ArgsList]), + case op_symbol(Method) of + {binary, Sym} when length(ArgsList) =:= 2 -> + [A1, A2] = ArgsList, + iolist_to_binary(["(", expr_to_string(A1), " ", Sym, " ", expr_to_string(A2), ")"]); + {unary, Sym} -> + [A1] = ArgsList, + iolist_to_binary([Sym, expr_to_string(A1)]); + none -> + iolist_to_binary([Method, "(", ArgsStr, ")"]) + end; +expr_to_string({expr, sequential, First, Second}) -> + iolist_to_binary([expr_to_string(First), "; ", expr_to_string(Second)]); +expr_to_string({expr, new_tuple, Elements}) -> + Inner = lists:join(", ", [expr_to_string(E) || E <- deref(Elements)]), + iolist_to_binary(["(", Inner, ")"]); +expr_to_string({expr, tuple_get, Inner, Index}) -> + iolist_to_binary(["Item", integer_to_binary(Index + 1), "(", expr_to_string(Inner), ")"]); +expr_to_string({expr, field_get, Inner, Name}) -> + iolist_to_binary([expr_to_string(Inner), ".", Name]); +expr_to_string(_) -> <<"">>. + +op_symbol(<<"op_Addition">>) -> {binary, <<"+">>}; +op_symbol(<<"op_Subtraction">>) -> {binary, <<"-">>}; +op_symbol(<<"op_Multiply">>) -> {binary, <<"*">>}; +op_symbol(<<"op_Division">>) -> {binary, <<"/">>}; +op_symbol(<<"op_Modulus">>) -> {binary, <<"%">>}; +op_symbol(<<"op_Equality">>) -> {binary, <<"=">>}; +op_symbol(<<"op_Inequality">>) -> {binary, <<"<>">>}; +op_symbol(<<"op_LessThan">>) -> {binary, <<"<">>}; +op_symbol(<<"op_LessThanOrEqual">>) -> {binary, <<"<=">>}; +op_symbol(<<"op_GreaterThan">>) -> {binary, <<">">>}; +op_symbol(<<"op_GreaterThanOrEqual">>) -> {binary, <<">=">>}; +op_symbol(<<"op_BooleanAnd">>) -> {binary, <<"&&">>}; +op_symbol(<<"op_BooleanOr">>) -> {binary, <<"||">>}; +op_symbol(<<"op_UnaryNegation">>) -> {unary, <<"-">>}; +op_symbol(<<"op_LogicalNot">>) -> {unary, <<"not">>}; +op_symbol(_) -> none. + +get_free_vars(Expr) -> + {Free, _} = collect_free_vars(Expr, sets:new()), + Free. + +collect_free_vars({expr, var_expr, {var, Name, _, _} = Var}, Bound) -> + case sets:is_element(Name, Bound) of + true -> {[], Bound}; + false -> {[Var], Bound} + end; +collect_free_vars({expr, lambda, {var, Name, _, _}, Body}, Bound) -> + collect_free_vars(Body, sets:add_element(Name, Bound)); +collect_free_vars({expr, 'let', {var, Name, _, _}, Value, Body}, Bound) -> + {FV, _} = collect_free_vars(Value, Bound), + {FB, _} = collect_free_vars(Body, sets:add_element(Name, Bound)), + {FV ++ FB, Bound}; +collect_free_vars({expr, application, Func, Arg}, Bound) -> + {F1, _} = collect_free_vars(Func, Bound), + {F2, _} = collect_free_vars(Arg, Bound), + {F1 ++ F2, Bound}; +collect_free_vars({expr, if_then_else, Guard, Then, Else}, Bound) -> + {F1, _} = collect_free_vars(Guard, Bound), + {F2, _} = collect_free_vars(Then, Bound), + {F3, _} = collect_free_vars(Else, Bound), + {F1 ++ F2 ++ F3, Bound}; +collect_free_vars({expr, call, _, _, Args}, Bound) -> + Fs = [F || A <- deref(Args), F <- element(1, collect_free_vars(A, Bound))], + {Fs, Bound}; +collect_free_vars({expr, sequential, First, Second}, Bound) -> + {F1, _} = collect_free_vars(First, Bound), + {F2, _} = collect_free_vars(Second, Bound), + {F1 ++ F2, Bound}; +collect_free_vars({expr, new_tuple, Elements}, Bound) -> + Fs = [F || E <- deref(Elements), F <- element(1, collect_free_vars(E, Bound))], + {Fs, Bound}; +collect_free_vars(_, Bound) -> {[], Bound}. + +substitute(Expr, Fn) -> sub(Expr, Fn). + +sub({expr, var_expr, Var} = E, Fn) -> + case Fn(Var) of + undefined -> E; + Replacement -> Replacement + end; +sub({expr, lambda, Var, Body}, Fn) -> + {expr, lambda, Var, sub(Body, Fn)}; +sub({expr, 'let', Var, Value, Body}, Fn) -> + {expr, 'let', Var, sub(Value, Fn), sub(Body, Fn)}; +sub({expr, application, Func, Arg}, Fn) -> + {expr, application, sub(Func, Fn), sub(Arg, Fn)}; +sub({expr, if_then_else, Guard, Then, Else}, Fn) -> + {expr, if_then_else, sub(Guard, Fn), sub(Then, Fn), sub(Else, Fn)}; +sub({expr, sequential, First, Second}, Fn) -> + {expr, sequential, sub(First, Fn), sub(Second, Fn)}; +sub(E, _Fn) -> E. diff --git a/src/fable-library-py/fable_library/fable_quotation.py b/src/fable-library-py/fable_library/fable_quotation.py index bf144c7bed..f2cfe01d9c 100644 --- a/src/fable-library-py/fable_library/fable_quotation.py +++ b/src/fable-library-py/fable_library/fable_quotation.py @@ -12,7 +12,7 @@ from typing import Any from .array_ import Array -from .list import FSharpList, of_array +from .list import FSharpList, of_array # pyright: ignore[reportMissingImports], auto-generated # =================================================================== @@ -463,3 +463,173 @@ def closure(arg: Any) -> Any: case _: raise ValueError(f"Cannot evaluate expression: {type(expr).__name__}") + + +# =================================================================== +# FSharpExpr instance methods +# =================================================================== + +_OP_SYMBOLS: dict[str, str] = { + "op_Addition": "+", + "op_Subtraction": "-", + "op_Multiply": "*", + "op_Division": "/", + "op_Modulus": "%", + "op_Exponentiation": "**", + "op_Equality": "=", + "op_Inequality": "<>", + "op_LessThan": "<", + "op_LessThanOrEqual": "<=", + "op_GreaterThan": ">", + "op_GreaterThanOrEqual": ">=", + "op_BooleanAnd": "&&", + "op_BooleanOr": "||", + "op_UnaryNegation": "-", + "op_LogicalNot": "not", +} + + +def expr_to_string(expr: Expr) -> str: + """Pretty-print a quotation AST as F#-like source code.""" + match expr: + case ExprValue(value=v, type=t): + if t == "string": + return f'"{v}"' + if t == "unit": + return "()" + if t == "bool": + return "true" if v else "false" + return str(v) + + case ExprVarExpr(var=var): + return var.name + + case ExprLambda(var=var, body=body): + return f"fun {var.name} -> {expr_to_string(body)}" + + case ExprApplication(func=func, arg=arg): + return f"{expr_to_string(func)} {expr_to_string(arg)}" + + case ExprLet(var=var, value=value, body=body): + return f"let {var.name} = {expr_to_string(value)} in {expr_to_string(body)}" + + case ExprIfThenElse(guard=guard, then_expr=then_expr, else_expr=else_expr): + return f"if {expr_to_string(guard)} then {expr_to_string(then_expr)} else {expr_to_string(else_expr)}" + + case ExprCall(method=method, args=args): + if method in _OP_SYMBOLS and len(args) == 2: + return f"({expr_to_string(args[0])} {_OP_SYMBOLS[method]} {expr_to_string(args[1])})" + if method in _OP_SYMBOLS and len(args) == 1: + return f"{_OP_SYMBOLS[method]}{expr_to_string(args[0])}" + arg_strs = ", ".join(expr_to_string(a) for a in args) + return f"{method}({arg_strs})" + + case ExprSequential(first=first, second=second): + return f"{expr_to_string(first)}; {expr_to_string(second)}" + + case ExprNewTuple(elements=elements): + return "(" + ", ".join(expr_to_string(e) for e in elements) + ")" + + case ExprTupleGet(expr=inner, index=index): + return f"Item{index + 1}({expr_to_string(inner)})" + + case ExprFieldGet(expr=inner, field_name=name): + return f"{expr_to_string(inner)}.{name}" + + case _: + return f"<{type(expr).__name__}>" + + +def get_free_vars(expr: Expr) -> list[Var]: + """Get the free variables in a quotation expression.""" + free: list[Var] = [] + seen: set[str] = set() + + def walk(e: Expr, bound: set[str]) -> None: + match e: + case ExprVarExpr(var=var): + if var.name not in bound and var.name not in seen: + free.append(var) + seen.add(var.name) + case ExprLambda(var=var, body=body): + walk(body, bound | {var.name}) + case ExprLet(var=var, value=value, body=body): + walk(value, bound) + walk(body, bound | {var.name}) + case ExprApplication(func=func, arg=arg): + walk(func, bound) + walk(arg, bound) + case ExprIfThenElse(guard=guard, then_expr=then_expr, else_expr=else_expr): + walk(guard, bound) + walk(then_expr, bound) + walk(else_expr, bound) + case ExprCall(args=args): + for a in args: + walk(a, bound) + case ExprSequential(first=first, second=second): + walk(first, bound) + walk(second, bound) + case ExprNewTuple(elements=elements): + for e in elements: + walk(e, bound) + case ExprTupleGet(expr=inner): + walk(inner, bound) + case ExprFieldGet(expr=inner): + walk(inner, bound) + case _: + pass + + walk(expr, set()) + return free + + +def substitute(expr: Expr, fn: Any) -> Expr: + """Substitute variables in a quotation using a function Var -> Expr option.""" + + def sub(e: Expr) -> Expr: + match e: + case ExprVarExpr(var=var): + result = fn(var) + if result is not None: + return result + return e + case ExprLambda(var=var, body=body): + return ExprLambda(var, sub(body)) + case ExprLet(var=var, value=value, body=body): + return ExprLet(var, sub(value), sub(body)) + case ExprApplication(func=func, arg=arg): + return ExprApplication(sub(func), sub(arg)) + case ExprIfThenElse(guard=guard, then_expr=then_expr, else_expr=else_expr): + return ExprIfThenElse(sub(guard), sub(then_expr), sub(else_expr)) + case ExprCall(instance=instance, method=method, args=args): + new_inst = ( + sub(instance) + if isinstance( + instance, + ( + ExprValue, + ExprVarExpr, + ExprLambda, + ExprApplication, + ExprLet, + ExprIfThenElse, + ExprCall, + ExprSequential, + ExprNewTuple, + ), + ) + else instance + ) + return ExprCall(new_inst, method, type(args)([sub(a) for a in args])) + case ExprSequential(first=first, second=second): + return ExprSequential(sub(first), sub(second)) + case ExprNewTuple(elements=elements): + return ExprNewTuple(type(elements)([sub(e) for e in elements])) + case ExprTupleGet(expr=inner, index=index): + return ExprTupleGet(sub(inner), index) + case ExprFieldGet(expr=inner, field_name=name): + return ExprFieldGet(sub(inner), name) + case _: + return e + + return sub(expr) diff --git a/tests/Beam/QuotationTests.fs b/tests/Beam/QuotationTests.fs index 30232d4fe0..8163afde08 100644 --- a/tests/Beam/QuotationTests.fs +++ b/tests/Beam/QuotationTests.fs @@ -138,3 +138,11 @@ let ``test Evaluate tuple`` () = let result = LeafExpressionConverter.EvaluateQuotation <@ (1, 2) @> let t = result :?> (int * int) equal (1, 2) t + +// --- FSharpExpr instance methods --- + +[] +let ``test Expr.GetFreeVars returns empty for closed expr`` () = + let q = <@ fun x -> x + 1 @> + let freeVars = q.GetFreeVars() |> Seq.length + equal 0 freeVars diff --git a/tests/Python/TestQuotation.fs b/tests/Python/TestQuotation.fs index 6af9a473a6..9d0cb8705a 100644 --- a/tests/Python/TestQuotation.fs +++ b/tests/Python/TestQuotation.fs @@ -153,3 +153,11 @@ let ``test Evaluate tuple`` () = let result = LeafExpressionConverter.EvaluateQuotation <@ (1, 2) @> let t = result :?> (int * int) equal (1, 2) t + +// --- FSharpExpr instance methods --- + +[] +let ``test Expr.GetFreeVars returns empty for closed expr`` () = + let q = <@ fun x -> x + 1 @> + let freeVars = q.GetFreeVars() |> Seq.length + equal 0 freeVars From 19f1b1476bf0e3169dccbb0b8aec4a87634e3a75 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 14 Mar 2026 10:30:51 +0100 Subject: [PATCH 07/15] Fix get_free_vars return type to Array[Var] for IEnumerable compatibility 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) --- src/fable-library-py/fable_library/fable_quotation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fable-library-py/fable_library/fable_quotation.py b/src/fable-library-py/fable_library/fable_quotation.py index f2cfe01d9c..ae243935f9 100644 --- a/src/fable-library-py/fable_library/fable_quotation.py +++ b/src/fable-library-py/fable_library/fable_quotation.py @@ -540,7 +540,7 @@ def expr_to_string(expr: Expr) -> str: return f"<{type(expr).__name__}>" -def get_free_vars(expr: Expr) -> list[Var]: +def get_free_vars(expr: Expr) -> Array[Var]: """Get the free variables in a quotation expression.""" free: list[Var] = [] seen: set[str] = set() @@ -580,7 +580,7 @@ def walk(e: Expr, bound: set[str]) -> None: pass walk(expr, set()) - return free + return Array[Var](free) def substitute(expr: Expr, fn: Any) -> Expr: From cf93b98ecc1187d5b2eec28691e0cbf5d7cc4c69 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 14 Mar 2026 10:40:24 +0100 Subject: [PATCH 08/15] Add list.pyi stub and remove pyright ignore comment 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) --- src/fable-library-py/fable_library/fable_quotation.py | 2 +- src/fable-library-py/fable_library/list.pyi | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 src/fable-library-py/fable_library/list.pyi diff --git a/src/fable-library-py/fable_library/fable_quotation.py b/src/fable-library-py/fable_library/fable_quotation.py index ae243935f9..adeaef8edb 100644 --- a/src/fable-library-py/fable_library/fable_quotation.py +++ b/src/fable-library-py/fable_library/fable_quotation.py @@ -12,7 +12,7 @@ from typing import Any from .array_ import Array -from .list import FSharpList, of_array # pyright: ignore[reportMissingImports], auto-generated +from .list import FSharpList, of_array # =================================================================== diff --git a/src/fable-library-py/fable_library/list.pyi b/src/fable-library-py/fable_library/list.pyi new file mode 100644 index 0000000000..b59b01ad47 --- /dev/null +++ b/src/fable-library-py/fable_library/list.pyi @@ -0,0 +1,9 @@ +from .array_ import Array +from .protocols import IEnumerable_1 + +class FSharpList[T](IEnumerable_1[T]): + head_: T | None + tail_: FSharpList[T] | None + +def of_array[T](xs: Array[T]) -> FSharpList[T]: ... +def length[T](xs: FSharpList[T]) -> int: ... From 25d80ce8a844cbd444e6e8f7b21e5a8d01c27733 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 2 Apr 2026 10:57:21 +0200 Subject: [PATCH 09/15] Extract shared quotation replacements to Replacements.Util.fs Move duplicated quotationExprs, quotationVars, quotationPatterns functions from Python and Beam Replacements into a shared Quotations module. Uses camelCase function names that both targets auto-convert to snake_case. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Fable.Transforms/Beam/Replacements.fs | 142 +------------------- src/Fable.Transforms/Python/Replacements.fs | 142 +------------------- src/Fable.Transforms/Replacements.Util.fs | 132 ++++++++++++++++++ 3 files changed, 134 insertions(+), 282 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 0b3742d447..9ce94e4b5e 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -4601,136 +4601,6 @@ let private bclType (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (thisArg: Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some -// 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 - let tryType (_t: Type) = None /// Compile-time resolution for System.Type methods when the type is known statically via TypeInfo. @@ -5300,17 +5170,7 @@ let tryCall | _ -> None | "System.Text.StringBuilder" -> bclType com ctx r t info thisArg args // F# Quotations - | Types.fsharpExpr - | Types.fsharpExprGeneric -> quotationExprs com ctx r t info thisArg args - | Types.fsharpVar -> quotationVars com ctx r t info thisArg args - | Types.patternsModule -> quotationPatterns com ctx r t info thisArg args - | "Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter" -> - match info.CompiledName, args with - | "EvaluateQuotation", [ expr ] -> - Helper.LibCall(com, "fable_quotation", "evaluate", t, [ expr ], ?loc = r) - |> Some - | _ -> None - | _ -> None + | typeName -> Quotations.tryQuotationCall "fable_quotation" com ctx r t info thisArg args typeName let tryBaseConstructor (_com: ICompiler) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 238bcaf95a..6c3e6c50a9 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -3804,136 +3804,6 @@ let tryField com returnTyp ownerTyp fieldName = | _ -> None | _ -> None -// 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 - let private replacedModules = dict [ @@ -4133,17 +4003,7 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr | c -> Helper.LibCall(com, "Reflection", "name", t, [ c ], ?loc = r) |> Some | _ -> None // F# Quotations - | Types.fsharpExpr - | Types.fsharpExprGeneric -> quotationExprs com ctx r t info thisArg args - | Types.fsharpVar -> quotationVars com ctx r t info thisArg args - | Types.patternsModule -> quotationPatterns com ctx r t info thisArg args - | "Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter" -> - match info.CompiledName, args with - | "EvaluateQuotation", [ expr ] -> - Helper.LibCall(com, "fable_quotation", "evaluate", t, [ expr ], ?loc = r) - |> Some - | _ -> None - | _ -> None + | typeName -> Quotations.tryQuotationCall "fableQuotation" com ctx r t info thisArg args typeName let tryBaseConstructor com ctx (ent: EntityRef) (argTypes: Lazy) genArgs args = match ent.FullName with diff --git a/src/Fable.Transforms/Replacements.Util.fs b/src/Fable.Transforms/Replacements.Util.fs index 1e27877374..1dd65de119 100644 --- a/src/Fable.Transforms/Replacements.Util.fs +++ b/src/Fable.Transforms/Replacements.Util.fs @@ -1445,3 +1445,135 @@ module AnonRecords = | [] -> Ok() | errors -> Error errors | _ -> Ok() // TODO: Error instead if we cannot check the interface? + +/// Shared quotation replacement functions for all targets. +/// Each target calls these with its own library module name (e.g., "fableQuotation" for Python/Beam, "Quotation" for JS). +/// Python and Beam auto-convert camelCase to snake_case in imports. +module Quotations = + + // F# Quotation: FSharpExpr static methods (e.g. Expr.Value, Expr.Lambda, etc.) + let quotationExprs + (libModule: string) + (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, libModule, "mkValue", t, [ value; typeArg ], ?loc = r) + |> Some + | "Var", _, [ var ] -> Helper.LibCall(com, libModule, "mkVarExpr", t, [ var ], ?loc = r) |> Some + | "Lambda", _, [ var; body ] -> Helper.LibCall(com, libModule, "mkLambda", t, [ var; body ], ?loc = r) |> Some + | "Application", _, [ func; arg ] -> Helper.LibCall(com, libModule, "mkApp", t, [ func; arg ], ?loc = r) |> Some + | "Let", _, [ var; value; body ] -> + Helper.LibCall(com, libModule, "mkLet", t, [ var; value; body ], ?loc = r) + |> Some + | "IfThenElse", _, [ guard; thenExpr; elseExpr ] -> + Helper.LibCall(com, libModule, "mkIfThenElse", t, [ guard; thenExpr; elseExpr ], ?loc = r) + |> Some + | "Call", _, [ instance; methodInfo; argList ] -> + Helper.LibCall(com, libModule, "mkCall", t, [ instance; methodInfo; argList ], ?loc = r) + |> Some + | "NewTuple", _, [ elements ] -> Helper.LibCall(com, libModule, "mkNewTuple", t, [ elements ], ?loc = r) |> Some + | "Sequential", _, [ first; second ] -> + Helper.LibCall(com, libModule, "mkSequential", t, [ first; second ], ?loc = r) + |> Some + | "get_Type", Some callee, _ -> Helper.LibCall(com, libModule, "getType", t, [ callee ], ?loc = r) |> Some + | "GetFreeVars", Some callee, _ -> + Helper.LibCall(com, libModule, "getFreeVars", t, [ callee ], ?loc = r) |> Some + | "Substitute", Some callee, [ fn ] -> + Helper.LibCall(com, libModule, "substitute", t, [ callee; fn ], ?loc = r) + |> Some + | "ToString", Some callee, _ + | "ToString", Some callee, [ _ ] -> + Helper.LibCall(com, libModule, "exprToString", t, [ callee ], ?loc = r) |> Some + | _ -> None + + // F# Quotation: FSharpVar (.ctor, get_Name, get_Type, get_IsMutable) + let quotationVars + (libModule: string) + (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, libModule, "mkVar", t, [ name; typ; isMutable ], ?loc = r) + |> Some + | ".ctor", None, [ name; typ ] -> + Helper.LibCall(com, libModule, "mkVar", t, [ name; typ; makeBoolConst false ], ?loc = r) + |> Some + | "get_Name", Some callee, _ -> Helper.LibCall(com, libModule, "varGetName", t, [ callee ], ?loc = r) |> Some + | "get_Type", Some callee, _ -> Helper.LibCall(com, libModule, "varGetType", t, [ callee ], ?loc = r) |> Some + | "get_IsMutable", Some callee, _ -> + Helper.LibCall(com, libModule, "varGetIsMutable", t, [ callee ], ?loc = r) + |> Some + | _ -> None + + // F# Quotation: PatternsModule (active patterns like ValuePattern, LambdaPattern, etc.) + let quotationPatterns + (libModule: string) + (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, libModule, "isValue", t, [ expr ], ?loc = r) |> Some + | ("VarPattern" | "|Var|_|"), [ expr ] -> Helper.LibCall(com, libModule, "isVar", t, [ expr ], ?loc = r) |> Some + | ("LambdaPattern" | "|Lambda|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isLambda", t, [ expr ], ?loc = r) |> Some + | ("ApplicationPattern" | "|Application|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isApplication", t, [ expr ], ?loc = r) |> Some + | ("LetPattern" | "|Let|_|"), [ expr ] -> Helper.LibCall(com, libModule, "isLet", t, [ expr ], ?loc = r) |> Some + | ("IfThenElsePattern" | "|IfThenElse|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isIfThenElse", t, [ expr ], ?loc = r) |> Some + | ("CallPattern" | "|Call|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isCall", t, [ expr ], ?loc = r) |> Some + | ("NewTuplePattern" | "|NewTuple|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isNewTuple", t, [ expr ], ?loc = r) |> Some + | ("SequentialPattern" | "|Sequential|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isSequential", t, [ expr ], ?loc = r) |> Some + | ("NewUnionCasePattern" | "|NewUnionCase|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isNewUnion", t, [ expr ], ?loc = r) |> Some + | ("NewRecordPattern" | "|NewRecord|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isNewRecord", t, [ expr ], ?loc = r) |> Some + | ("TupleGetPattern" | "|TupleGet|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isTupleGet", t, [ expr ], ?loc = r) |> Some + | ("PropertyGetPattern" | "|PropertyGet|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isFieldGet", t, [ expr ], ?loc = r) |> Some + | _ -> None + + let tryQuotationCall + (libModule: string) + (com: ICompiler) + (ctx: Context) + r + (t: Type) + (info: CallInfo) + (thisArg: Expr option) + (args: Expr list) + (typeName: string) + = + match typeName with + | Types.fsharpExpr + | Types.fsharpExprGeneric -> quotationExprs libModule com ctx r t info thisArg args + | Types.fsharpVar -> quotationVars libModule com ctx r t info thisArg args + | Types.patternsModule -> quotationPatterns libModule com ctx r t info thisArg args + | "Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter" -> + match info.CompiledName, args with + | "EvaluateQuotation", [ expr ] -> Helper.LibCall(com, libModule, "evaluate", t, [ expr ], ?loc = r) |> Some + | _ -> None + | _ -> None From 371052eba68941589c1fd7c5b94b401b88c989b2 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 4 Apr 2026 15:26:26 +0200 Subject: [PATCH 10/15] Align quotation function names with JS PR #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) --- src/Fable.Transforms/Replacements.Util.fs | 12 +++++++----- src/fable-library-beam/fable_quotation.erl | 16 ++++++++-------- .../fable_library/fable_quotation.py | 8 ++++---- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Fable.Transforms/Replacements.Util.fs b/src/Fable.Transforms/Replacements.Util.fs index 1dd65de119..adc4c733e0 100644 --- a/src/Fable.Transforms/Replacements.Util.fs +++ b/src/Fable.Transforms/Replacements.Util.fs @@ -1466,9 +1466,11 @@ module Quotations = | "Value", _, [ value; typeArg ] -> Helper.LibCall(com, libModule, "mkValue", t, [ value; typeArg ], ?loc = r) |> Some - | "Var", _, [ var ] -> Helper.LibCall(com, libModule, "mkVarExpr", t, [ var ], ?loc = r) |> Some + | "Var", _, [ var ] -> Helper.LibCall(com, libModule, "mkVar", t, [ var ], ?loc = r) |> Some | "Lambda", _, [ var; body ] -> Helper.LibCall(com, libModule, "mkLambda", t, [ var; body ], ?loc = r) |> Some - | "Application", _, [ func; arg ] -> Helper.LibCall(com, libModule, "mkApp", t, [ func; arg ], ?loc = r) |> Some + | "Application", _, [ func; arg ] -> + Helper.LibCall(com, libModule, "mkApplication", t, [ func; arg ], ?loc = r) + |> Some | "Let", _, [ var; value; body ] -> Helper.LibCall(com, libModule, "mkLet", t, [ var; value; body ], ?loc = r) |> Some @@ -1506,10 +1508,10 @@ module Quotations = = match i.CompiledName, thisArg, args with | ".ctor", None, [ name; typ; isMutable ] -> - Helper.LibCall(com, libModule, "mkVar", t, [ name; typ; isMutable ], ?loc = r) + Helper.LibCall(com, libModule, "mkQuotVar", t, [ name; typ; isMutable ], ?loc = r) |> Some | ".ctor", None, [ name; typ ] -> - Helper.LibCall(com, libModule, "mkVar", t, [ name; typ; makeBoolConst false ], ?loc = r) + Helper.LibCall(com, libModule, "mkQuotVar", t, [ name; typ; makeBoolConst false ], ?loc = r) |> Some | "get_Name", Some callee, _ -> Helper.LibCall(com, libModule, "varGetName", t, [ callee ], ?loc = r) |> Some | "get_Type", Some callee, _ -> Helper.LibCall(com, libModule, "varGetType", t, [ callee ], ?loc = r) |> Some @@ -1547,7 +1549,7 @@ module Quotations = | ("SequentialPattern" | "|Sequential|_|"), [ expr ] -> Helper.LibCall(com, libModule, "isSequential", t, [ expr ], ?loc = r) |> Some | ("NewUnionCasePattern" | "|NewUnionCase|_|"), [ expr ] -> - Helper.LibCall(com, libModule, "isNewUnion", t, [ expr ], ?loc = r) |> Some + Helper.LibCall(com, libModule, "isNewUnionCase", t, [ expr ], ?loc = r) |> Some | ("NewRecordPattern" | "|NewRecord|_|"), [ expr ] -> Helper.LibCall(com, libModule, "isNewRecord", t, [ expr ], ?loc = r) |> Some | ("TupleGetPattern" | "|TupleGet|_|"), [ expr ] -> diff --git a/src/fable-library-beam/fable_quotation.erl b/src/fable-library-beam/fable_quotation.erl index 85893bbd7e..117d4f967c 100644 --- a/src/fable-library-beam/fable_quotation.erl +++ b/src/fable-library-beam/fable_quotation.erl @@ -1,7 +1,7 @@ -module(fable_quotation). -export([ - mk_var/3, mk_var_expr/1, mk_value/2, mk_lambda/2, - mk_app/2, mk_let/3, mk_if_then_else/3, mk_call/3, + mk_quot_var/3, mk_var/1, mk_value/2, mk_lambda/2, + mk_application/2, mk_let/3, mk_if_then_else/3, mk_call/3, mk_sequential/2, mk_new_tuple/1, mk_new_union/3, mk_new_record/2, mk_new_list/2, mk_tuple_get/2, mk_union_tag/1, mk_union_field/2, @@ -10,7 +10,7 @@ get_type/1, is_value/1, is_var/1, is_lambda/1, is_application/1, is_let/1, is_if_then_else/1, is_call/1, is_sequential/1, - is_new_tuple/1, is_new_union/1, is_new_record/1, + is_new_tuple/1, is_new_union_case/1, is_new_record/1, is_tuple_get/1, is_field_get/1, evaluate/1, expr_to_string/1, get_free_vars/1, substitute/2 @@ -20,7 +20,7 @@ %% Var constructor: {var, Name, Type, IsMutable} %% =================================================================== -mk_var(Name, Type, IsMutable) -> {var, Name, Type, IsMutable}. +mk_quot_var(Name, Type, IsMutable) -> {var, Name, Type, IsMutable}. %% Var accessors var_get_name({var, Name, _, _}) -> Name. @@ -31,10 +31,10 @@ var_get_is_mutable({var, _, _, IsMutable}) -> IsMutable. %% Expr node constructors: {expr, Tag, ...fields} %% =================================================================== -mk_var_expr(Var) -> {expr, var_expr, Var}. +mk_var(Var) -> {expr, var_expr, Var}. mk_value(Value, Type) -> {expr, value, Value, Type}. mk_lambda(Var, Body) -> {expr, lambda, Var, Body}. -mk_app(Func, Arg) -> {expr, application, Func, Arg}. +mk_application(Func, Arg) -> {expr, application, Func, Arg}. mk_let(Var, Value, Body) -> {expr, 'let', Var, Value, Body}. mk_if_then_else(Guard, Then, Else) -> {expr, if_then_else, Guard, Then, Else}. mk_call(Instance, Method, Args) -> {expr, call, Instance, Method, Args}. @@ -100,8 +100,8 @@ is_new_tuple({expr, new_tuple, E}) -> deref(E); is_new_tuple(_) -> undefined. %% NewUnionCase pattern: returns {TypeName, Tag, Fields} (dereference Fields) -is_new_union({expr, new_union, N, T, F}) -> {N, T, deref(F)}; -is_new_union(_) -> undefined. +is_new_union_case({expr, new_union, N, T, F}) -> {N, T, deref(F)}; +is_new_union_case(_) -> undefined. %% NewRecord pattern: returns {FieldNames, Values} (dereference both) is_new_record({expr, new_record, N, V}) -> {deref(N), deref(V)}; diff --git a/src/fable-library-py/fable_library/fable_quotation.py b/src/fable-library-py/fable_library/fable_quotation.py index adeaef8edb..b3d817eec2 100644 --- a/src/fable-library-py/fable_library/fable_quotation.py +++ b/src/fable-library-py/fable_library/fable_quotation.py @@ -27,7 +27,7 @@ class Var: is_mutable: bool -def mk_var(name: str, type: str, is_mutable: bool = False) -> Var: +def mk_quot_var(name: str, type: str, is_mutable: bool = False) -> Var: return Var(name, type, is_mutable) @@ -190,7 +190,7 @@ def mk_value(value: Any, type: str) -> ExprValue: return ExprValue(value, type) -def mk_var_expr(var: Var) -> ExprVarExpr: +def mk_var(var: Var) -> ExprVarExpr: return ExprVarExpr(var) @@ -198,7 +198,7 @@ def mk_lambda(var: Var, body: Expr) -> ExprLambda: return ExprLambda(var, body) -def mk_app(func: Expr, arg: Expr) -> ExprApplication: +def mk_application(func: Expr, arg: Expr) -> ExprApplication: return ExprApplication(func, arg) @@ -332,7 +332,7 @@ def is_new_tuple(expr: Expr) -> FSharpList[Any] | None: return None -def is_new_union(expr: Expr) -> tuple[str, int, Array[Any]] | None: +def is_new_union_case(expr: Expr) -> tuple[str, int, Array[Any]] | None: if isinstance(expr, ExprNewUnion): return (expr.type_name, expr.tag, expr.fields) return None From 90545bf2f66f04769ec192a5d73da9cf5c15f040 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 4 Apr 2026 15:43:38 +0200 Subject: [PATCH 11/15] Use camelCase for all quotation library names in compiler QuotationEmitter.fs and Beam Replacements now use camelCase module/function names (fableQuotation, mkQuotVar, mkVar, mkApplication, etc.). Python and Beam auto-convert to snake_case via getLibPath and sanitizeErlangName. Fix Beam getLibPath to handle camelCase fable-prefixed names correctly (fableQuotation -> fable_quotation, not fable_fable_quotation). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Fable.Transforms/Beam/Replacements.fs | 2 +- src/Fable.Transforms/QuotationEmitter.fs | 94 +++++++++++------------ src/Fable.Transforms/Transforms.Util.fs | 11 ++- 3 files changed, 55 insertions(+), 52 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 2bb35de568..0d79e5c221 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -5560,7 +5560,7 @@ let tryCall | _ -> None | "System.Text.StringBuilder" -> bclType com ctx r t info thisArg args // F# Quotations - | typeName -> Quotations.tryQuotationCall "fable_quotation" com ctx r t info thisArg args typeName + | typeName -> Quotations.tryQuotationCall "fableQuotation" com ctx r t info thisArg args typeName let tryBaseConstructor (_com: ICompiler) diff --git a/src/Fable.Transforms/QuotationEmitter.fs b/src/Fable.Transforms/QuotationEmitter.fs index fda9cceb81..2e7a11a764 100644 --- a/src/Fable.Transforms/QuotationEmitter.fs +++ b/src/Fable.Transforms/QuotationEmitter.fs @@ -16,13 +16,13 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | IdentExpr ident -> // Reference to a variable already introduced by a lambda/let in the quotation. - // Emit: fable_quotation:mk_var_expr(Var) + // Emit: fableQuotation.mkVar(Var) // We need a var reference. Create a var and then wrap it. let varExpr = Helper.LibCall( com, - "fable_quotation", - "mk_var", + "fableQuotation", + "mkQuotVar", Any, [ makeStrConst ident.Name @@ -31,14 +31,14 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = ] ) - Helper.LibCall(com, "fable_quotation", "mk_var_expr", Any, [ varExpr ]) + Helper.LibCall(com, "fableQuotation", "mkVar", Any, [ varExpr ]) | Lambda(arg, body, _name) -> let varExpr = Helper.LibCall( com, - "fable_quotation", - "mk_var", + "fableQuotation", + "mkQuotVar", Any, [ makeStrConst arg.Name @@ -48,7 +48,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = ) let bodyExpr = emitQuotedExpr com body - Helper.LibCall(com, "fable_quotation", "mk_lambda", Any, [ varExpr; bodyExpr ]) + Helper.LibCall(com, "fableQuotation", "mkLambda", Any, [ varExpr; bodyExpr ]) | Delegate(args, body, _name, _tags) -> // Multi-arg delegate: nest as curried lambdas @@ -59,8 +59,8 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let varExpr = Helper.LibCall( com, - "fable_quotation", - "mk_var", + "fableQuotation", + "mkQuotVar", Any, [ makeStrConst arg.Name @@ -70,7 +70,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = ) let innerBody = nestLambdas rest body - Helper.LibCall(com, "fable_quotation", "mk_lambda", Any, [ varExpr; innerBody ]) + Helper.LibCall(com, "fableQuotation", "mkLambda", Any, [ varExpr; innerBody ]) nestLambdas args body @@ -78,8 +78,8 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let varExpr = Helper.LibCall( com, - "fable_quotation", - "mk_var", + "fableQuotation", + "mkQuotVar", Any, [ makeStrConst ident.Name @@ -91,13 +91,13 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let valueExpr = emitQuotedExpr com value let bodyExpr = emitQuotedExpr com body - Helper.LibCall(com, "fable_quotation", "mk_let", Any, [ varExpr; valueExpr; bodyExpr ]) + Helper.LibCall(com, "fableQuotation", "mkLet", Any, [ varExpr; valueExpr; bodyExpr ]) | IfThenElse(guardExpr, thenExpr, elseExpr, _r) -> let guard = emitQuotedExpr com guardExpr let thenE = emitQuotedExpr com thenExpr let elseE = emitQuotedExpr com elseExpr - Helper.LibCall(com, "fable_quotation", "mk_if_then_else", Any, [ guard; thenE; elseE ]) + Helper.LibCall(com, "fableQuotation", "mkIfThenElse", Any, [ guard; thenE; elseE ]) | CurriedApply(applied, args, _typ, _r) -> // Emit nested applications: Application(Application(f, a1), a2) @@ -107,7 +107,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = |> List.fold (fun acc arg -> let argExpr = emitQuotedExpr com arg - Helper.LibCall(com, "fable_quotation", "mk_app", Any, [ acc; argExpr ]) + Helper.LibCall(com, "fableQuotation", "mkApplication", Any, [ acc; argExpr ]) ) appliedExpr @@ -126,7 +126,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let argExprs = info.Args |> List.map (emitQuotedExpr com) |> makeArray Any - Helper.LibCall(com, "fable_quotation", "mk_call", Any, [ instanceExpr; methodExpr; argExprs ]) + Helper.LibCall(com, "fableQuotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs ]) | Sequential exprs -> match exprs with @@ -135,7 +135,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | first :: rest -> let restExpr = emitQuotedExpr com (Sequential rest) let firstExpr = emitQuotedExpr com first - Helper.LibCall(com, "fable_quotation", "mk_sequential", Any, [ firstExpr; restExpr ]) + Helper.LibCall(com, "fableQuotation", "mkSequential", Any, [ firstExpr; restExpr ]) | Operation(kind, _tags, _typ, _r) -> // Represent operations as calls to the operator method @@ -187,23 +187,21 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let argExprs = args |> List.map (emitQuotedExpr com) |> makeArray Any - Helper.LibCall(com, "fable_quotation", "mk_call", Any, [ instanceExpr; methodExpr; argExprs ]) + Helper.LibCall(com, "fableQuotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs ]) | Get(expr, kind, _typ, _r) -> let target = emitQuotedExpr com expr match kind with - | TupleIndex index -> - Helper.LibCall(com, "fable_quotation", "mk_tuple_get", Any, [ target; makeIntConst index ]) - | UnionTag -> Helper.LibCall(com, "fable_quotation", "mk_union_tag", Any, [ target ]) + | TupleIndex index -> Helper.LibCall(com, "fableQuotation", "mkTupleGet", Any, [ target; makeIntConst index ]) + | UnionTag -> Helper.LibCall(com, "fableQuotation", "mkUnionTag", Any, [ target ]) | UnionField info -> - Helper.LibCall(com, "fable_quotation", "mk_union_field", Any, [ target; makeIntConst info.FieldIndex ]) - | FieldGet info -> - Helper.LibCall(com, "fable_quotation", "mk_field_get", Any, [ target; makeStrConst info.Name ]) + Helper.LibCall(com, "fableQuotation", "mkUnionField", Any, [ target; makeIntConst info.FieldIndex ]) + | FieldGet info -> Helper.LibCall(com, "fableQuotation", "mkFieldGet", Any, [ target; makeStrConst info.Name ]) | _ -> // ListHead, ListTail, OptionValue, ExprGet — fall through let msg = "Unsupported quotation Get kind" - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) + Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) | Set(expr, kind, _typ, value, _r) -> let target = emitQuotedExpr com expr @@ -212,12 +210,12 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = match kind with | ValueSet -> // Mutable variable set: expr is the ident, value is the new value - Helper.LibCall(com, "fable_quotation", "mk_var_set", Any, [ target; valueExpr ]) + Helper.LibCall(com, "fableQuotation", "mkVarSet", Any, [ target; valueExpr ]) | FieldSet fieldName -> - Helper.LibCall(com, "fable_quotation", "mk_field_set", Any, [ target; makeStrConst fieldName; valueExpr ]) + Helper.LibCall(com, "fableQuotation", "mkFieldSet", Any, [ target; makeStrConst fieldName; valueExpr ]) | _ -> let msg = "Unsupported quotation Set kind" - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) + Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) | TypeCast(innerExpr, _typ) -> // Coerce/cast: just emit the inner expression for now @@ -230,7 +228,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | [ ([], body) ] -> emitQuotedExpr com body | _ -> let msg = "Unsupported quotation node: DecisionTree" - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) + Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) | DecisionTreeSuccess(idx, boundValues, _typ) -> match boundValues with @@ -238,41 +236,40 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | [ single ] -> emitQuotedExpr com single | _ -> let msg = "Unsupported quotation node: DecisionTreeSuccess" - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) + Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) | _ -> // Unsupported node: emit an error value let msg = $"Unsupported quotation node: %A{expr.GetType().Name}" - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) + Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocation option) : Expr = match kind with - | BoolConstant b -> - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeBoolConst b; makeStrConst "bool" ]) + | BoolConstant b -> Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeBoolConst b; makeStrConst "bool" ]) | NumberConstant(NumberValue.Int32 i, _) -> - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeIntConst i; makeStrConst "int32" ]) + Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeIntConst i; makeStrConst "int32" ]) | NumberConstant(NumberValue.Float64 f, _) -> let floatExpr = Value(NumberConstant(NumberValue.Float64 f, NumberInfo.Empty), None) - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ floatExpr; makeStrConst "float64" ]) + Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ floatExpr; makeStrConst "float64" ]) | StringConstant s -> - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst s; makeStrConst "string" ]) + Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst s; makeStrConst "string" ]) | UnitConstant -> - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ Value(UnitConstant, None); makeStrConst "unit" ]) + Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ Value(UnitConstant, None); makeStrConst "unit" ]) - | Null _ -> Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ Value(Null Any, None); makeStrConst "null" ]) + | Null _ -> Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ Value(Null Any, None); makeStrConst "null" ]) | CharConstant c -> - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ Value(CharConstant c, None); makeStrConst "char" ]) + Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ Value(CharConstant c, None); makeStrConst "char" ]) | NewTuple(values, _isStruct) -> let emittedValues = values |> List.map (emitQuotedExpr com) |> makeArray Any - Helper.LibCall(com, "fable_quotation", "mk_new_tuple", Any, [ emittedValues ]) + Helper.LibCall(com, "fableQuotation", "mkNewTuple", Any, [ emittedValues ]) | NewUnion(values, tag, entRef, _genArgs) -> let entName = @@ -284,8 +281,8 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio Helper.LibCall( com, - "fable_quotation", - "mk_new_union", + "fableQuotation", + "mkNewUnion", Any, [ makeStrConst entName; makeIntConst tag; emittedValues ] ) @@ -298,29 +295,28 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio let emittedValues = values |> List.map (emitQuotedExpr com) |> makeArray Any - Helper.LibCall(com, "fable_quotation", "mk_new_record", Any, [ fieldNames; emittedValues ]) + Helper.LibCall(com, "fableQuotation", "mkNewRecord", Any, [ fieldNames; emittedValues ]) | NewOption(value, _typ, _isStruct) -> match value with | Some v -> let emitted = emitQuotedExpr com v - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ emitted; makeStrConst "option" ]) + Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ emitted; makeStrConst "option" ]) | None -> - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ Value(Null Any, None); makeStrConst "option" ]) + Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ Value(Null Any, None); makeStrConst "option" ]) | NewList(headAndTail, _typ) -> match headAndTail with | Some(head, tail) -> let headExpr = emitQuotedExpr com head let tailExpr = emitQuotedExpr com tail - Helper.LibCall(com, "fable_quotation", "mk_new_list", Any, [ headExpr; tailExpr ]) - | None -> - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ Value(Null Any, None); makeStrConst "list" ]) + Helper.LibCall(com, "fableQuotation", "mkNewList", Any, [ headExpr; tailExpr ]) + | None -> Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ Value(Null Any, None); makeStrConst "list" ]) | _ -> // Fallback for other value kinds let msg = "Unsupported quotation value" - Helper.LibCall(com, "fable_quotation", "mk_value", Any, [ makeStrConst msg; makeStrConst "string" ]) + Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) and private typeToString (t: Type) : string = match t with diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index 61426d6458..0b02fe3155 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -1194,8 +1194,15 @@ module AST = // Fable-compiled .fs module — use name as-is (no fable_ prefix) moduleName else - // JS fallback module name (e.g., "Option" -> "fable_option") - "fable_" + (moduleName |> Naming.applyCaseRule Fable.Core.CaseRules.SnakeCase) + // Apply snake_case first, then add fable_ prefix only if not already present + // e.g., "fableQuotation" -> "fable_quotation" (already has fable_ prefix after snake_case) + // e.g., "Option" -> "option" -> "fable_option" + let snaked = moduleName |> Naming.applyCaseRule Fable.Core.CaseRules.SnakeCase + + if snaked.StartsWith("fable_", System.StringComparison.Ordinal) then + snaked + else + "fable_" + snaked com.LibraryDir + "/" + beamModuleName + ".erl" From 0293324085ac3940f4adf64f2beb2aef1f920ecb Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 4 Apr 2026 15:55:00 +0200 Subject: [PATCH 12/15] Rename quotation module to "quotation" across all targets 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) --- src/Fable.Transforms/Beam/Replacements.fs | 2 +- src/Fable.Transforms/Python/Replacements.fs | 2 +- src/Fable.Transforms/QuotationEmitter.fs | 90 +++++++++---------- src/Fable.Transforms/Replacements.Util.fs | 4 +- src/Fable.Transforms/Transforms.Util.fs | 11 +-- .../{fable_quotation.py => quotation.py} | 0 6 files changed, 47 insertions(+), 62 deletions(-) rename src/fable-library-py/fable_library/{fable_quotation.py => quotation.py} (100%) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 0d79e5c221..5d7b8755f7 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -5560,7 +5560,7 @@ let tryCall | _ -> None | "System.Text.StringBuilder" -> bclType com ctx r t info thisArg args // F# Quotations - | typeName -> Quotations.tryQuotationCall "fableQuotation" com ctx r t info thisArg args typeName + | typeName -> Quotations.tryQuotationCall "quotation" com ctx r t info thisArg args typeName let tryBaseConstructor (_com: ICompiler) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 89a1cff32c..0e9719aed3 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -4156,7 +4156,7 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr | c -> Helper.LibCall(com, "Reflection", "name", t, [ c ], ?loc = r) |> Some | _ -> None // F# Quotations - | typeName -> Quotations.tryQuotationCall "fableQuotation" com ctx r t info thisArg args typeName + | typeName -> Quotations.tryQuotationCall "quotation" com ctx r t info thisArg args typeName let tryBaseConstructor com ctx (ent: EntityRef) (argTypes: Lazy) genArgs args = match ent.FullName with diff --git a/src/Fable.Transforms/QuotationEmitter.fs b/src/Fable.Transforms/QuotationEmitter.fs index 2e7a11a764..e2341a438f 100644 --- a/src/Fable.Transforms/QuotationEmitter.fs +++ b/src/Fable.Transforms/QuotationEmitter.fs @@ -9,19 +9,19 @@ open Replacements.Util /// Emits a Fable expression that, when compiled, produces runtime calls /// to construct a quotation AST. The input is the Fable.Expr captured /// inside a Quote node; the output is a Fable.Expr that calls the -/// fable_quotation runtime library to build the AST at runtime. +/// quotation runtime library to build the AST at runtime. let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = match expr with | Value(kind, r) -> emitQuotedValue com kind r | IdentExpr ident -> // Reference to a variable already introduced by a lambda/let in the quotation. - // Emit: fableQuotation.mkVar(Var) + // Emit: quotation.mkVar(Var) // We need a var reference. Create a var and then wrap it. let varExpr = Helper.LibCall( com, - "fableQuotation", + "quotation", "mkQuotVar", Any, [ @@ -31,13 +31,13 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = ] ) - Helper.LibCall(com, "fableQuotation", "mkVar", Any, [ varExpr ]) + Helper.LibCall(com, "quotation", "mkVar", Any, [ varExpr ]) | Lambda(arg, body, _name) -> let varExpr = Helper.LibCall( com, - "fableQuotation", + "quotation", "mkQuotVar", Any, [ @@ -48,7 +48,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = ) let bodyExpr = emitQuotedExpr com body - Helper.LibCall(com, "fableQuotation", "mkLambda", Any, [ varExpr; bodyExpr ]) + Helper.LibCall(com, "quotation", "mkLambda", Any, [ varExpr; bodyExpr ]) | Delegate(args, body, _name, _tags) -> // Multi-arg delegate: nest as curried lambdas @@ -59,7 +59,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let varExpr = Helper.LibCall( com, - "fableQuotation", + "quotation", "mkQuotVar", Any, [ @@ -70,7 +70,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = ) let innerBody = nestLambdas rest body - Helper.LibCall(com, "fableQuotation", "mkLambda", Any, [ varExpr; innerBody ]) + Helper.LibCall(com, "quotation", "mkLambda", Any, [ varExpr; innerBody ]) nestLambdas args body @@ -78,7 +78,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let varExpr = Helper.LibCall( com, - "fableQuotation", + "quotation", "mkQuotVar", Any, [ @@ -91,13 +91,13 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let valueExpr = emitQuotedExpr com value let bodyExpr = emitQuotedExpr com body - Helper.LibCall(com, "fableQuotation", "mkLet", Any, [ varExpr; valueExpr; bodyExpr ]) + Helper.LibCall(com, "quotation", "mkLet", Any, [ varExpr; valueExpr; bodyExpr ]) | IfThenElse(guardExpr, thenExpr, elseExpr, _r) -> let guard = emitQuotedExpr com guardExpr let thenE = emitQuotedExpr com thenExpr let elseE = emitQuotedExpr com elseExpr - Helper.LibCall(com, "fableQuotation", "mkIfThenElse", Any, [ guard; thenE; elseE ]) + Helper.LibCall(com, "quotation", "mkIfThenElse", Any, [ guard; thenE; elseE ]) | CurriedApply(applied, args, _typ, _r) -> // Emit nested applications: Application(Application(f, a1), a2) @@ -107,7 +107,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = |> List.fold (fun acc arg -> let argExpr = emitQuotedExpr com arg - Helper.LibCall(com, "fableQuotation", "mkApplication", Any, [ acc; argExpr ]) + Helper.LibCall(com, "quotation", "mkApplication", Any, [ acc; argExpr ]) ) appliedExpr @@ -126,7 +126,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let argExprs = info.Args |> List.map (emitQuotedExpr com) |> makeArray Any - Helper.LibCall(com, "fableQuotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs ]) + Helper.LibCall(com, "quotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs ]) | Sequential exprs -> match exprs with @@ -135,7 +135,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | first :: rest -> let restExpr = emitQuotedExpr com (Sequential rest) let firstExpr = emitQuotedExpr com first - Helper.LibCall(com, "fableQuotation", "mkSequential", Any, [ firstExpr; restExpr ]) + Helper.LibCall(com, "quotation", "mkSequential", Any, [ firstExpr; restExpr ]) | Operation(kind, _tags, _typ, _r) -> // Represent operations as calls to the operator method @@ -187,21 +187,21 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let argExprs = args |> List.map (emitQuotedExpr com) |> makeArray Any - Helper.LibCall(com, "fableQuotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs ]) + Helper.LibCall(com, "quotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs ]) | Get(expr, kind, _typ, _r) -> let target = emitQuotedExpr com expr match kind with - | TupleIndex index -> Helper.LibCall(com, "fableQuotation", "mkTupleGet", Any, [ target; makeIntConst index ]) - | UnionTag -> Helper.LibCall(com, "fableQuotation", "mkUnionTag", Any, [ target ]) + | TupleIndex index -> Helper.LibCall(com, "quotation", "mkTupleGet", Any, [ target; makeIntConst index ]) + | UnionTag -> Helper.LibCall(com, "quotation", "mkUnionTag", Any, [ target ]) | UnionField info -> - Helper.LibCall(com, "fableQuotation", "mkUnionField", Any, [ target; makeIntConst info.FieldIndex ]) - | FieldGet info -> Helper.LibCall(com, "fableQuotation", "mkFieldGet", Any, [ target; makeStrConst info.Name ]) + Helper.LibCall(com, "quotation", "mkUnionField", Any, [ target; makeIntConst info.FieldIndex ]) + | FieldGet info -> Helper.LibCall(com, "quotation", "mkFieldGet", Any, [ target; makeStrConst info.Name ]) | _ -> // ListHead, ListTail, OptionValue, ExprGet — fall through let msg = "Unsupported quotation Get kind" - Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) + Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) | Set(expr, kind, _typ, value, _r) -> let target = emitQuotedExpr com expr @@ -210,12 +210,12 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = match kind with | ValueSet -> // Mutable variable set: expr is the ident, value is the new value - Helper.LibCall(com, "fableQuotation", "mkVarSet", Any, [ target; valueExpr ]) + Helper.LibCall(com, "quotation", "mkVarSet", Any, [ target; valueExpr ]) | FieldSet fieldName -> - Helper.LibCall(com, "fableQuotation", "mkFieldSet", Any, [ target; makeStrConst fieldName; valueExpr ]) + Helper.LibCall(com, "quotation", "mkFieldSet", Any, [ target; makeStrConst fieldName; valueExpr ]) | _ -> let msg = "Unsupported quotation Set kind" - Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) + Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) | TypeCast(innerExpr, _typ) -> // Coerce/cast: just emit the inner expression for now @@ -228,7 +228,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | [ ([], body) ] -> emitQuotedExpr com body | _ -> let msg = "Unsupported quotation node: DecisionTree" - Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) + Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) | DecisionTreeSuccess(idx, boundValues, _typ) -> match boundValues with @@ -236,40 +236,39 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | [ single ] -> emitQuotedExpr com single | _ -> let msg = "Unsupported quotation node: DecisionTreeSuccess" - Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) + Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) | _ -> // Unsupported node: emit an error value let msg = $"Unsupported quotation node: %A{expr.GetType().Name}" - Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) + Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocation option) : Expr = match kind with - | BoolConstant b -> Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeBoolConst b; makeStrConst "bool" ]) + | BoolConstant b -> Helper.LibCall(com, "quotation", "mkValue", Any, [ makeBoolConst b; makeStrConst "bool" ]) | NumberConstant(NumberValue.Int32 i, _) -> - Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeIntConst i; makeStrConst "int32" ]) + Helper.LibCall(com, "quotation", "mkValue", Any, [ makeIntConst i; makeStrConst "int32" ]) | NumberConstant(NumberValue.Float64 f, _) -> let floatExpr = Value(NumberConstant(NumberValue.Float64 f, NumberInfo.Empty), None) - Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ floatExpr; makeStrConst "float64" ]) + Helper.LibCall(com, "quotation", "mkValue", Any, [ floatExpr; makeStrConst "float64" ]) - | StringConstant s -> - Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst s; makeStrConst "string" ]) + | StringConstant s -> Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst s; makeStrConst "string" ]) | UnitConstant -> - Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ Value(UnitConstant, None); makeStrConst "unit" ]) + Helper.LibCall(com, "quotation", "mkValue", Any, [ Value(UnitConstant, None); makeStrConst "unit" ]) - | Null _ -> Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ Value(Null Any, None); makeStrConst "null" ]) + | Null _ -> Helper.LibCall(com, "quotation", "mkValue", Any, [ Value(Null Any, None); makeStrConst "null" ]) | CharConstant c -> - Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ Value(CharConstant c, None); makeStrConst "char" ]) + Helper.LibCall(com, "quotation", "mkValue", Any, [ Value(CharConstant c, None); makeStrConst "char" ]) | NewTuple(values, _isStruct) -> let emittedValues = values |> List.map (emitQuotedExpr com) |> makeArray Any - Helper.LibCall(com, "fableQuotation", "mkNewTuple", Any, [ emittedValues ]) + Helper.LibCall(com, "quotation", "mkNewTuple", Any, [ emittedValues ]) | NewUnion(values, tag, entRef, _genArgs) -> let entName = @@ -279,13 +278,7 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio let emittedValues = values |> List.map (emitQuotedExpr com) |> makeArray Any - Helper.LibCall( - com, - "fableQuotation", - "mkNewUnion", - Any, - [ makeStrConst entName; makeIntConst tag; emittedValues ] - ) + Helper.LibCall(com, "quotation", "mkNewUnion", Any, [ makeStrConst entName; makeIntConst tag; emittedValues ]) | NewRecord(values, entRef, _genArgs) -> let fieldNames = @@ -295,28 +288,27 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio let emittedValues = values |> List.map (emitQuotedExpr com) |> makeArray Any - Helper.LibCall(com, "fableQuotation", "mkNewRecord", Any, [ fieldNames; emittedValues ]) + Helper.LibCall(com, "quotation", "mkNewRecord", Any, [ fieldNames; emittedValues ]) | NewOption(value, _typ, _isStruct) -> match value with | Some v -> let emitted = emitQuotedExpr com v - Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ emitted; makeStrConst "option" ]) - | None -> - Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ Value(Null Any, None); makeStrConst "option" ]) + Helper.LibCall(com, "quotation", "mkValue", Any, [ emitted; makeStrConst "option" ]) + | None -> Helper.LibCall(com, "quotation", "mkValue", Any, [ Value(Null Any, None); makeStrConst "option" ]) | NewList(headAndTail, _typ) -> match headAndTail with | Some(head, tail) -> let headExpr = emitQuotedExpr com head let tailExpr = emitQuotedExpr com tail - Helper.LibCall(com, "fableQuotation", "mkNewList", Any, [ headExpr; tailExpr ]) - | None -> Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ Value(Null Any, None); makeStrConst "list" ]) + Helper.LibCall(com, "quotation", "mkNewList", Any, [ headExpr; tailExpr ]) + | None -> Helper.LibCall(com, "quotation", "mkValue", Any, [ Value(Null Any, None); makeStrConst "list" ]) | _ -> // Fallback for other value kinds let msg = "Unsupported quotation value" - Helper.LibCall(com, "fableQuotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) + Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) and private typeToString (t: Type) : string = match t with diff --git a/src/Fable.Transforms/Replacements.Util.fs b/src/Fable.Transforms/Replacements.Util.fs index adc4c733e0..8fb2c68a3a 100644 --- a/src/Fable.Transforms/Replacements.Util.fs +++ b/src/Fable.Transforms/Replacements.Util.fs @@ -1447,8 +1447,8 @@ module AnonRecords = | _ -> Ok() // TODO: Error instead if we cannot check the interface? /// Shared quotation replacement functions for all targets. -/// Each target calls these with its own library module name (e.g., "fableQuotation" for Python/Beam, "Quotation" for JS). -/// Python and Beam auto-convert camelCase to snake_case in imports. +/// Each target calls these with its own library module name (e.g., "quotation" for Python/Beam, "Quotation" for JS). +/// Python converts to snake_case (quotation.py). Beam adds fable_ prefix (fable_quotation.erl). module Quotations = // F# Quotation: FSharpExpr static methods (e.g. Expr.Value, Expr.Lambda, etc.) diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index 0b02fe3155..61426d6458 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -1194,15 +1194,8 @@ module AST = // Fable-compiled .fs module — use name as-is (no fable_ prefix) moduleName else - // Apply snake_case first, then add fable_ prefix only if not already present - // e.g., "fableQuotation" -> "fable_quotation" (already has fable_ prefix after snake_case) - // e.g., "Option" -> "option" -> "fable_option" - let snaked = moduleName |> Naming.applyCaseRule Fable.Core.CaseRules.SnakeCase - - if snaked.StartsWith("fable_", System.StringComparison.Ordinal) then - snaked - else - "fable_" + snaked + // JS fallback module name (e.g., "Option" -> "fable_option") + "fable_" + (moduleName |> Naming.applyCaseRule Fable.Core.CaseRules.SnakeCase) com.LibraryDir + "/" + beamModuleName + ".erl" diff --git a/src/fable-library-py/fable_library/fable_quotation.py b/src/fable-library-py/fable_library/quotation.py similarity index 100% rename from src/fable-library-py/fable_library/fable_quotation.py rename to src/fable-library-py/fable_library/quotation.py From 0cb164c058a76de9218bbe62701f257930266423 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 4 Apr 2026 16:03:51 +0200 Subject: [PATCH 13/15] Revert CLAUDE.md change (--skip-fable-library no longer exists) Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 68c79e46cc..8bc972fbf3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -109,7 +109,7 @@ All transpiled Python code is type-checked with Pyright at standard settings (`. Quicktest is also preferred when adding debug output (e.g., `printfn` in compiler code) since running full tests with debug prints produces too much output. -**Test suites** are in `tests//` (e.g., `tests/Python/`, `tests/Js/Main/`). Tests are first run on .NET, then transpiled to `temp/tests//` (e.g., `temp/tests/py/`, `temp/tests/js/`) and executed with the target's test runner. Full test runs take several minutes. You can and should run `./build.sh test ` yourself after making changes — use `--skip-fable-library` when only compiler code changed to speed things up. +**Test suites** are in `tests//` (e.g., `tests/Python/`, `tests/Js/Main/`). Tests are first run on .NET, then transpiled to `temp/tests//` (e.g., `temp/tests/py/`, `temp/tests/js/`) and executed with the target's test runner. Full test runs take several minutes. When adding a test, check if other targets already have a test for the same case (e.g., look in `tests/Js/Main/` before adding to `tests/Python/`) — reuse or adapt existing test patterns where possible. From 66dce9cb35e70dd8d17cac12fa0f457e1dc1c8cc Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 4 Apr 2026 21:02:33 +0200 Subject: [PATCH 14/15] Add source location to Quote AST node Quote(quotedExpr, isTyped, range) now carries its own SourceLocation, consistent with all other Expr cases. The range is captured from the FCS quotation expression in FSharp2Fable. Important for JS/TS source maps and error reporting. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Fable.AST/Fable.fs | 4 ++-- src/Fable.Transforms/Beam/Fable2Beam.fs | 2 +- src/Fable.Transforms/FSharp2Fable.fs | 4 ++-- src/Fable.Transforms/Python/Fable2Python.Transforms.fs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Fable.AST/Fable.fs b/src/Fable.AST/Fable.fs index 275d515705..8e2d9b2b56 100644 --- a/src/Fable.AST/Fable.fs +++ b/src/Fable.AST/Fable.fs @@ -836,7 +836,7 @@ type Expr = | Unresolved of expr: UnresolvedExpr * typ: Type * range: SourceLocation option | Extended of expr: ExtendedSet * range: SourceLocation option - | Quote of quotedExpr: Expr * isTyped: bool + | Quote of quotedExpr: Expr * isTyped: bool * range: SourceLocation option member this.Type = match this with @@ -894,7 +894,7 @@ type Expr = | Set(_, _, _, _, r) | ForLoop(_, _, _, _, _, r) | WhileLoop(_, _, r) -> r - | Quote(e, _) -> e.Range + | Quote(_, _, r) -> r // module PrettyPrint = // let rec printType (t: Type) = "T" // TODO diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index 6b3e4a259e..d88a12b524 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -1175,7 +1175,7 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er ] ) - | Quote(body, _isTyped) -> + | Quote(body, _isTyped, _r) -> let emitted = QuotationEmitter.emitQuotedExpr com body transformExpr com ctx emitted diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index b308fa879a..31ceffc3ba 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -1460,7 +1460,7 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs let! body = transformExpr com ctx [] quotedExpr let exprType = fsExpr.Type let isTyped = exprType.GenericArguments.Count > 0 - return Fable.Quote(body, isTyped) + return Fable.Quote(body, isTyped, makeRangeFrom fsExpr) | FSharpExprPatterns.AddressOf expr -> let r = makeRangeFrom fsExpr @@ -2476,7 +2476,7 @@ let resolveInlineExpr (com: IFableCompiler) ctx info expr = |> makeValue r | Fable.TypeInfo(t, d) -> Fable.TypeInfo(resolveInlineType ctx.GenericArgs t, d) |> makeValue r - | Fable.Quote(e, isTyped) -> Fable.Quote(resolveInlineExpr com ctx info e, isTyped) + | Fable.Quote(e, isTyped, r) -> Fable.Quote(resolveInlineExpr com ctx info e, isTyped, r) | Fable.Extended(kind, r) as e -> match kind with diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 4c333feeeb..5e5f41ffe5 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -2659,7 +2659,7 @@ let rec transformAsExpr (com: IPythonCompiler) ctx (expr: Fable.Expr) : Expressi | Fable.Curry(e, arity) -> transformCurry com ctx e arity | Fable.Throw _ | Fable.Debugger -> iife com ctx expr - | Fable.Quote(body, _isTyped) -> + | Fable.Quote(body, _isTyped, _r) -> let emitted = QuotationEmitter.emitQuotedExpr com body transformAsExpr com ctx emitted @@ -2984,7 +2984,7 @@ let rec transformAsStatements (com: IPythonCompiler) ctx returnStrategy (expr: F [ Statement.for' (target = target, iter = iter, body = body) ] - | Fable.Quote(body, _isTyped) -> + | Fable.Quote(body, _isTyped, _r) -> let emitted = QuotationEmitter.emitQuotedExpr com body transformAsStatements com ctx returnStrategy emitted From 234c6827fd8d3b415a525f013e258a84195a8e51 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 6 Apr 2026 13:35:08 +0200 Subject: [PATCH 15/15] chore: Fixup changelog merge --- src/Fable.Cli/CHANGELOG.md | 4 ---- src/Fable.Compiler/CHANGELOG.md | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 688a058513..784242bbc2 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -10,10 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * [Python/Beam] Add F# quotation support — construction, pattern matching, and evaluation via `LeafExpressionConverter.EvaluateQuotation` (by @dbrattli) -* [All] Add support for `Guid.CreateVersion7()` and `Guid.CreateVersion7(DateTimeOffset)` -* [Rust] Add missing `System.Random` implementation and tests (by @ncave) -* [Rust] Add missing `Array`, `List` and `Seq` module members and tests: `randomChoice`, `randomChoiceBy`, `randomChoiceWith`, `randomChoices`, `randomChoicesBy`, `randomChoicesWith`, `randomSample`, `randomSampleBy`, `randomSampleWith`, `randomShuffle`, `randomShuffleBy`, `randomShuffleWith` (by @ncave) -* [Beam] Implement missing DateTimeOffset members, add DateOnly and TimeOnly support * [All] Add support for `Guid.CreateVersion7()` and `Guid.CreateVersion7(DateTimeOffset)` (by @OnurGumus) * [All] Add missing `Array`, `List`, and `Seq` random choice/shuffle/sample members and tests (by @ncave) * [Dart/Rust] Add missing `System.Random` implementations and tests (by @ncave) diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 47e964c0e8..6d2ace2072 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -10,10 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * [Python/Beam] Add F# quotation support — construction, pattern matching, and evaluation via `LeafExpressionConverter.EvaluateQuotation` (by @dbrattli) -* [All] Add support for `Guid.CreateVersion7()` and `Guid.CreateVersion7(DateTimeOffset)` -* [Rust] Add missing `System.Random` implementation and tests (by @ncave) -* [Rust] Add missing `Array`, `List` and `Seq` module members and tests: `randomChoice`, `randomChoiceBy`, `randomChoiceWith`, `randomChoices`, `randomChoicesBy`, `randomChoicesWith`, `randomSample`, `randomSampleBy`, `randomSampleWith`, `randomShuffle`, `randomShuffleBy`, `randomShuffleWith` (by @ncave) -* [Beam] Implement missing DateTimeOffset members, add DateOnly and TimeOnly support * [All] Add support for `Guid.CreateVersion7()` and `Guid.CreateVersion7(DateTimeOffset)` (by @OnurGumus) * [All] Add missing `Array`, `List`, and `Seq` random choice/shuffle/sample members and tests (by @ncave) * [Dart/Rust] Add missing `System.Random` implementations and tests (by @ncave)