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