diff --git a/src/Fable.AST/Fable.fs b/src/Fable.AST/Fable.fs
index dff776a7ec..8e2d9b2b56 100644
--- a/src/Fable.AST/Fable.fs
+++ b/src/Fable.AST/Fable.fs
@@ -836,11 +836,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 * range: SourceLocation option
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
@@ -892,6 +894,7 @@ type Expr =
| Set(_, _, _, _, r)
| ForLoop(_, _, _, _, _, r)
| WhileLoop(_, _, r) -> r
+ | Quote(_, _, r) -> r
// module PrettyPrint =
// let rec printType (t: Type) = "T" // TODO
diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md
index df6bfd45ee..784242bbc2 100644
--- a/src/Fable.Cli/CHANGELOG.md
+++ b/src/Fable.Cli/CHANGELOG.md
@@ -9,6 +9,7 @@ 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)` (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 f7e809b562..6d2ace2072 100644
--- a/src/Fable.Compiler/CHANGELOG.md
+++ b/src/Fable.Compiler/CHANGELOG.md
@@ -9,6 +9,7 @@ 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)` (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.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs
index 90ece23326..d88a12b524 100644
--- a/src/Fable.Transforms/Beam/Fable2Beam.fs
+++ b/src/Fable.Transforms/Beam/Fable2Beam.fs
@@ -1175,6 +1175,10 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er
]
)
+ | Quote(body, _isTyped, _r) ->
+ 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 93322138f0..23f14139fd 100644
--- a/src/Fable.Transforms/Beam/Replacements.fs
+++ b/src/Fable.Transforms/Beam/Replacements.fs
@@ -5679,7 +5679,8 @@ let tryCall
| _ -> emitExpr r t [ c ] "maps:get(name, $0)" |> Some
| _ -> None
| "System.Text.StringBuilder" -> bclType com ctx r t info thisArg args
- | _ -> None
+ // F# Quotations
+ | typeName -> Quotations.tryQuotationCall "quotation" com ctx r t info thisArg args typeName
let tryBaseConstructor
(_com: ICompiler)
diff --git a/src/Fable.Transforms/Dart/Fable2Dart.fs b/src/Fable.Transforms/Dart/Fable2Dart.fs
index 4b0dd7ac32..91ae2b017f 100644
--- a/src/Fable.Transforms/Dart/Fable2Dart.fs
+++ b/src/Fable.Transforms/Dart/Fable2Dart.fs
@@ -2191,6 +2191,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 55ff5b16cd..31ceffc3ba 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, makeRangeFrom fsExpr)
| 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, r) -> Fable.Quote(resolveInlineExpr com ctx info e, isTyped, r)
+
| 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 599777b288..93ee7c5707 100644
--- a/src/Fable.Transforms/Fable2Babel.fs
+++ b/src/Fable.Transforms/Fable2Babel.fs
@@ -1114,6 +1114,7 @@ module Util =
| Fable.CurriedApply _
| Fable.Operation _
| Fable.Get _
+ | Fable.Quote _
| Fable.Test _ -> false
| Fable.TypeCast(e, _) -> isJsStatement ctx preferStatement e
@@ -3141,6 +3142,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) ->
@@ -3308,6 +3311,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 7b09895783..9a7accae16 100644
--- a/src/Fable.Transforms/FableTransforms.fs
+++ b/src/Fable.Transforms/FableTransforms.fs
@@ -159,7 +159,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 cb2380fbde..5e5f41ffe5 100644
--- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs
+++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs
@@ -2659,6 +2659,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, _r) ->
+ let emitted = QuotationEmitter.emitQuotedExpr com body
+ transformAsExpr com ctx emitted
let transformAsSlice (com: IPythonCompiler) ctx expr (info: Fable.CallInfo) : Expression * Statement list =
Expression.withStmts {
@@ -2981,6 +2984,10 @@ let rec transformAsStatements (com: IPythonCompiler) ctx returnStrategy (expr: F
[ Statement.for' (target = target, iter = iter, body = body) ]
+ | Fable.Quote(body, _isTyped, _r) ->
+ 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 e95fdbcf94..dc68275c28 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 f440597c6f..9efc30857f 100644
--- a/src/Fable.Transforms/Python/Replacements.fs
+++ b/src/Fable.Transforms/Python/Replacements.fs
@@ -4157,7 +4157,8 @@ 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
- | _ -> None
+ // F# Quotations
+ | 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
new file mode 100644
index 0000000000..e2341a438f
--- /dev/null
+++ b/src/Fable.Transforms/QuotationEmitter.fs
@@ -0,0 +1,326 @@
+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
+/// 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: quotation.mkVar(Var)
+ // We need a var reference. Create a var and then wrap it.
+ let varExpr =
+ Helper.LibCall(
+ com,
+ "quotation",
+ "mkQuotVar",
+ Any,
+ [
+ makeStrConst ident.Name
+ makeStrConst (typeToString ident.Type)
+ makeBoolConst false
+ ]
+ )
+
+ Helper.LibCall(com, "quotation", "mkVar", Any, [ varExpr ])
+
+ | Lambda(arg, body, _name) ->
+ let varExpr =
+ Helper.LibCall(
+ com,
+ "quotation",
+ "mkQuotVar",
+ Any,
+ [
+ makeStrConst arg.Name
+ makeStrConst (typeToString arg.Type)
+ makeBoolConst false
+ ]
+ )
+
+ let bodyExpr = emitQuotedExpr com body
+ Helper.LibCall(com, "quotation", "mkLambda", 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,
+ "quotation",
+ "mkQuotVar",
+ Any,
+ [
+ makeStrConst arg.Name
+ makeStrConst (typeToString arg.Type)
+ makeBoolConst false
+ ]
+ )
+
+ let innerBody = nestLambdas rest body
+ Helper.LibCall(com, "quotation", "mkLambda", Any, [ varExpr; innerBody ])
+
+ nestLambdas args body
+
+ | Let(ident, value, body) ->
+ let varExpr =
+ Helper.LibCall(
+ com,
+ "quotation",
+ "mkQuotVar",
+ Any,
+ [
+ makeStrConst ident.Name
+ makeStrConst (typeToString ident.Type)
+ makeBoolConst ident.IsMutable
+ ]
+ )
+
+ let valueExpr = emitQuotedExpr com value
+ let bodyExpr = emitQuotedExpr com body
+
+ 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, "quotation", "mkIfThenElse", 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, "quotation", "mkApplication", 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, "quotation", "mkCall", 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, "quotation", "mkSequential", 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, "quotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs ])
+
+ | Get(expr, kind, _typ, _r) ->
+ let target = emitQuotedExpr com expr
+
+ match kind with
+ | TupleIndex index -> Helper.LibCall(com, "quotation", "mkTupleGet", Any, [ target; makeIntConst index ])
+ | UnionTag -> Helper.LibCall(com, "quotation", "mkUnionTag", Any, [ target ])
+ | UnionField info ->
+ 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, "quotation", "mkValue", 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, "quotation", "mkVarSet", Any, [ target; valueExpr ])
+ | FieldSet fieldName ->
+ Helper.LibCall(com, "quotation", "mkFieldSet", Any, [ target; makeStrConst fieldName; valueExpr ])
+ | _ ->
+ let msg = "Unsupported quotation Set kind"
+ Helper.LibCall(com, "quotation", "mkValue", 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, "quotation", "mkValue", 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, "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, "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, "quotation", "mkValue", Any, [ makeBoolConst b; makeStrConst "bool" ])
+
+ | NumberConstant(NumberValue.Int32 i, _) ->
+ 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, "quotation", "mkValue", Any, [ floatExpr; makeStrConst "float64" ])
+
+ | StringConstant s -> Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst s; makeStrConst "string" ])
+
+ | UnitConstant ->
+ Helper.LibCall(com, "quotation", "mkValue", Any, [ Value(UnitConstant, None); makeStrConst "unit" ])
+
+ | Null _ -> Helper.LibCall(com, "quotation", "mkValue", Any, [ Value(Null Any, None); makeStrConst "null" ])
+
+ | CharConstant c ->
+ 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, "quotation", "mkNewTuple", 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, "quotation", "mkNewUnion", 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, "quotation", "mkNewRecord", Any, [ fieldNames; emittedValues ])
+
+ | NewOption(value, _typ, _isStruct) ->
+ match value with
+ | Some v ->
+ let emitted = emitQuotedExpr com v
+ 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, "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, "quotation", "mkValue", 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) -> $"%s{typeToString argType} -> %s{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, ImmutableArray), None)
diff --git a/src/Fable.Transforms/Replacements.Util.fs b/src/Fable.Transforms/Replacements.Util.fs
index 1e27877374..8fb2c68a3a 100644
--- a/src/Fable.Transforms/Replacements.Util.fs
+++ b/src/Fable.Transforms/Replacements.Util.fs
@@ -1445,3 +1445,137 @@ 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., "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.)
+ 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, "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, "mkApplication", 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, "mkQuotVar", t, [ name; typ; isMutable ], ?loc = r)
+ |> Some
+ | ".ctor", None, [ name; typ ] ->
+ 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
+ | "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, "isNewUnionCase", 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
diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs
index 1c6d9ab5ef..9e976995b2 100644
--- a/src/Fable.Transforms/Rust/Fable2Rust.fs
+++ b/src/Fable.Transforms/Rust/Fable2Rust.fs
@@ -3532,6 +3532,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 851fcffc00..61426d6458 100644
--- a/src/Fable.Transforms/Transforms.Util.fs
+++ b/src/Fable.Transforms/Transforms.Util.fs
@@ -480,6 +480,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"
@@ -1662,6 +1674,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
@@ -1752,6 +1765,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..117d4f967c
--- /dev/null
+++ b/src/fable-library-beam/fable_quotation.erl
@@ -0,0 +1,292 @@
+-module(fable_quotation).
+-export([
+ 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,
+ 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_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
+]).
+
+%% ===================================================================
+%% Var constructor: {var, Name, Type, IsMutable}
+%% ===================================================================
+
+mk_quot_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(Var) -> {expr, var_expr, Var}.
+mk_value(Value, Type) -> {expr, value, Value, Type}.
+mk_lambda(Var, Body) -> {expr, lambda, Var, Body}.
+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}.
+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} (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 (dereference if stored as Ref)
+is_new_tuple({expr, new_tuple, E}) -> deref(E);
+is_new_tuple(_) -> undefined.
+
+%% NewUnionCase pattern: returns {TypeName, Tag, Fields} (dereference Fields)
+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)};
+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, #{}).
+
+%% 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);
+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 <- 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 <- deref(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.
+
+%% ===================================================================
+%% 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/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: ...
diff --git a/src/fable-library-py/fable_library/quotation.py b/src/fable-library-py/fable_library/quotation.py
new file mode 100644
index 0000000000..b3d817eec2
--- /dev/null
+++ b/src/fable-library-py/fable_library/quotation.py
@@ -0,0 +1,635 @@
+"""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
+
+from .array_ import Array
+from .list import FSharpList, of_array
+
+
+# ===================================================================
+# Var: represents an F# quotation variable
+# ===================================================================
+
+
+@dataclass
+class Var:
+ name: str
+ type: str
+ is_mutable: bool
+
+
+def mk_quot_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: Array[Any]
+
+
+@dataclass
+class ExprSequential:
+ first: Any # Expr node
+ second: Any # Expr node
+
+
+@dataclass
+class ExprNewTuple:
+ elements: Array[Any]
+
+
+@dataclass
+class ExprNewUnion:
+ type_name: str
+ tag: int
+ fields: Array[Any]
+
+
+@dataclass
+class ExprNewRecord:
+ field_names: Array[str]
+ values: Array[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(var: Var) -> ExprVarExpr:
+ return ExprVarExpr(var)
+
+
+def mk_lambda(var: Var, body: Expr) -> ExprLambda:
+ return ExprLambda(var, body)
+
+
+def mk_application(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: Array[Any]) -> ExprCall:
+ return ExprCall(instance, method, args)
+
+
+def mk_sequential(first: Expr, second: Expr) -> ExprSequential:
+ return ExprSequential(first, second)
+
+
+def mk_new_tuple(elements: Array[Any]) -> ExprNewTuple:
+ return ExprNewTuple(elements)
+
+
+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: Array[str], values: Array[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, Array[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) -> FSharpList[Any] | None:
+ if isinstance(expr, ExprNewTuple):
+ return of_array(expr.elements)
+ return 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
+
+
+def is_new_record(expr: Expr) -> tuple[Array[str], Array[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__}")
+
+
+# ===================================================================
+# 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) -> Array[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 Array[Var](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/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 @@
+
diff --git a/tests/Beam/Fable.Tests.Beam.fsproj b/tests/Beam/Fable.Tests.Beam.fsproj
index 5f6a5c1885..19bed181ad 100644
--- a/tests/Beam/Fable.Tests.Beam.fsproj
+++ b/tests/Beam/Fable.Tests.Beam.fsproj
@@ -88,6 +88,7 @@
+
diff --git a/tests/Beam/QuotationTests.fs b/tests/Beam/QuotationTests.fs
new file mode 100644
index 0000000000..8163afde08
--- /dev/null
+++ b/tests/Beam/QuotationTests.fs
@@ -0,0 +1,148 @@
+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
+
+// --- 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/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj
index 817fbaa04d..cabc6a85c4 100644
--- a/tests/Python/Fable.Tests.Python.fsproj
+++ b/tests/Python/Fable.Tests.Python.fsproj
@@ -85,6 +85,7 @@
+
diff --git a/tests/Python/TestQuotation.fs b/tests/Python/TestQuotation.fs
new file mode 100644
index 0000000000..9d0cb8705a
--- /dev/null
+++ b/tests/Python/TestQuotation.fs
@@ -0,0 +1,163 @@
+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
+
+// --- 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