From f65722618f89d6d3fa2e6ba887357acb7c7ef07e Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Mon, 13 Jul 2026 23:32:32 +0100 Subject: [PATCH 1/2] feat(quotations): add F# quotation support across targets + Rust/PHP runtimes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improves quotations in the shared emitter and adds quotation support to the Rust and PHP targets. Motivated by compiling F# query (<@ @>) expressions, e.g. toward a future TypeProviders (like SQLProvider) port to Python/Rust. Shared emitter (QuotationEmitter.fs) — benefits all targets: - typeToString: render all numeric kinds, char/regex/guid/datetime/ datetimeoffset/timespan, option/voption/list/array/seq/nullable, declared-type names, generics and delegates instead of "obj". - Call nodes record the declaring type; Get OptionValue/ListHead/ListTail map to property-getter calls. - DecisionTree inlined into IfThenElse/Let; Test nodes (union/option/list/ type) represented via union-tag comparison / getter calls. - mkNull constructor for null instances/values (no raw null literal, so statically typed languages compile); is_call maps it back to None. mk_call kept backward-compatible (3 and 4 params on Beam); mkNull added to each. Rust target (new): route Quote through the emitter (Fable2Rust), wire Replacements to tryQuotationCall, and add a transpiled F# runtime (QuotationTypes.fs + Quotation.fs) exposing FSharpExpr/FSharpVar at Microsoft.FSharp.Quotations and the quotation_ module. Construction + deconstruction verified via cargo; arithmetic evaluate works (let/lambda eval pending an upstream getZero fix for Rc). Check the tests for more use-case examples. --- src/Fable.Transforms/FSharp2Fable.Util.fs | 12 + src/Fable.Transforms/FSharp2Fable.fs | 4 +- src/Fable.Transforms/Php/Fable2Php.fs | 5 +- src/Fable.Transforms/QuotationEmitter.fs | 199 ++++++++++++-- src/Fable.Transforms/Rust/Fable2Rust.fs | 7 +- src/Fable.Transforms/Rust/Replacements.fs | 3 +- src/fable-library-beam/fable_quotation.erl | 35 ++- src/fable-library-php/quotation.php | 232 ++++++++++++++++ .../fable_library/quotation.py | 25 +- .../src/Fable.Library.Rust.fsproj | 2 + src/fable-library-rust/src/Quotation.fs | 248 ++++++++++++++++++ src/fable-library-rust/src/QuotationTypes.fs | 35 +++ src/fable-library-ts/quotation.ts | 25 +- tests/Php/Fable.Tests.Php.fsproj | 1 + tests/Php/TestQuotation.fs | 71 +++++ tests/Python/TestQuotation.fs | 16 ++ tests/Rust/Fable.Tests.Rust.fsproj | 1 + tests/Rust/tests/src/QuotationTests.fs | 60 +++++ 18 files changed, 935 insertions(+), 46 deletions(-) create mode 100644 src/fable-library-php/quotation.php create mode 100644 src/fable-library-rust/src/Quotation.fs create mode 100644 src/fable-library-rust/src/QuotationTypes.fs create mode 100644 tests/Php/TestQuotation.fs create mode 100644 tests/Rust/tests/src/QuotationTests.fs diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 4cd77c6fec..8eccaa9759 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -537,6 +537,10 @@ type Context = InlinePath: Log.InlinePath list CaptureBaseConsCall: (FSharpEntity * (Fable.Expr -> unit)) option Witnesses: Fable.Witness list + // When true we are translating the body of a code quotation (<@ @>). The body is + // data, never executed, so member calls are kept as-is (not replaced/emitted/inlined) + // to preserve their .NET member metadata for QuotationEmitter. + CapturingQuotation: bool } static member Create(?usedRootNames) = @@ -555,6 +559,7 @@ type Context = InlinePath = [] CaptureBaseConsCall = None Witnesses = [] + CapturingQuotation = false } type IFableCompiler = @@ -3058,6 +3063,13 @@ module Util = (callInfo: Fable.CallInfo) = match memb, memb.DeclaringEntity with + // Inside a quotation the body is data, never executed. Preserve the original member + // call (skip replace/emit/import/inline) so its .NET member metadata survives for + // QuotationEmitter. Skipping inlining here also matches real F# quotation semantics. + | _ when ctx.CapturingQuotation -> + let membTyp = makeType ctx.GenericArgs memb.FullType + memberIdent com r membTyp memb membRef |> makeCall r typ callInfo + | Emitted com ctx r typ (Some callInfo) emitted, _ -> emitted | Imported com ctx r typ (Some callInfo) imported -> imported | Replaced com ctx r typ callInfo replaced -> replaced diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 47c0c11e1f..9f5fc00919 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -1503,7 +1503,9 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs |> addErrorAndReturnNull com ctx.InlinePath (makeRangeFrom fsExpr) | FSharpExprPatterns.Quote quotedExpr -> - let! body = transformExpr com ctx [] quotedExpr + // Translate the quoted body in "capturing" mode so member calls keep their + // .NET metadata (not replaced/emitted/inlined) — the body is data, not code. + let! body = transformExpr com { ctx with CapturingQuotation = true } [] quotedExpr let exprType = fsExpr.Type let isTyped = exprType.GenericArguments.Count > 0 return Fable.Quote(body, isTyped, makeRangeFrom fsExpr) diff --git a/src/Fable.Transforms/Php/Fable2Php.fs b/src/Fable.Transforms/Php/Fable2Php.fs index 4afbb5ee5b..66fe72487f 100644 --- a/src/Fable.Transforms/Php/Fable2Php.fs +++ b/src/Fable.Transforms/Php/Fable2Php.fs @@ -936,7 +936,10 @@ 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.Quote(body, _isTyped, _r) -> + // Lower the quoted body to runtime calls that build the quotation AST, + // then convert like any other expression (mirrors JS/Python/Beam/Rust). + convertExpr com (QuotationEmitter.emitQuotedExpr com body) | Fable.Unresolved _ -> failwith "Unexpected unresolved expression" diff --git a/src/Fable.Transforms/QuotationEmitter.fs b/src/Fable.Transforms/QuotationEmitter.fs index e2341a438f..85cac05625 100644 --- a/src/Fable.Transforms/QuotationEmitter.fs +++ b/src/Fable.Transforms/QuotationEmitter.fs @@ -115,18 +115,22 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let instanceExpr = match info.ThisArg with | Some thisArg -> emitQuotedExpr com thisArg - | None -> Value(Null Any, None) + // A static/operator call has no instance. Emit a runtime-built null node + // (not a raw null literal) so every target — notably statically typed ones + // like Rust — sees an Expr in the instance position. + | None -> mkNullExpr com "null" - let methodName = + let declaringType, methodName = match info.MemberRef with - | Some(MemberRef(_, mInfo)) -> mInfo.CompiledName - | _ -> "unknown" + | Some(MemberRef(declaringEntity, mInfo)) -> declaringEntity.FullName, mInfo.CompiledName + | _ -> "", "unknown" let methodExpr = makeStrConst methodName + let declTypeExpr = makeStrConst declaringType let argExprs = info.Args |> List.map (emitQuotedExpr com) |> makeArray Any - Helper.LibCall(com, "quotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs ]) + Helper.LibCall(com, "quotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs; declTypeExpr ]) | Sequential exprs -> match exprs with @@ -183,11 +187,12 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = name, [ left; right ] let methodExpr = makeStrConst opName - let instanceExpr = Value(Null Any, None) + let instanceExpr = mkNullExpr com "null" let argExprs = args |> List.map (emitQuotedExpr com) |> makeArray Any - Helper.LibCall(com, "quotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs ]) + // Operators have no declaring type; pass an empty string. + Helper.LibCall(com, "quotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs; makeStrConst "" ]) | Get(expr, kind, _typ, _r) -> let target = emitQuotedExpr com expr @@ -198,8 +203,29 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | 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 -> + // Represent option/list value accessors as property-getter calls, + // mirroring how F# quotations model them (PropertyGet get_Value/get_Head/get_Tail). + let methodName = + match kind with + | ListHead -> "get_Head" + | ListTail -> "get_Tail" + | _ -> "get_Value" + + let emptyArgs = makeArray Any [] + + Helper.LibCall( + com, + "quotation", + "mkCall", + Any, + [ target; makeStrConst methodName; emptyArgs; makeStrConst "" ] + ) | _ -> - // ListHead, ListTail, OptionValue, ExprGet — fall through + // TupleIndex/UnionTag/UnionField/FieldGet handled above; ExprGet and any + // future kinds fall through here. let msg = "Unsupported quotation Get kind" Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) @@ -221,14 +247,27 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = // 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" ]) + | DecisionTree(decisionExpr, targets) -> + // Inline every DecisionTreeSuccess leaf into its target body, binding the + // target's captured idents to the success's bound values with nested Lets. + // This turns the compiled match into a plain IfThenElse/Let tree that the + // quotation representation can express faithfully (a shared target is simply + // inlined at each reference site). + let inlined = + decisionExpr + |> visitFromInsideOut (fun e -> + match e with + | DecisionTreeSuccess(idx, boundValues, _) when idx < List.length targets -> + let idents, body = List.item idx targets + + if List.length idents = List.length boundValues then + List.foldBack (fun (id, v) acc -> Let(id, v, acc)) (List.zip idents boundValues) body + else + e + | e -> e + ) + + emitQuotedExpr com inlined | DecisionTreeSuccess(idx, boundValues, _typ) -> match boundValues with @@ -238,12 +277,91 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let msg = "Unsupported quotation node: DecisionTreeSuccess" Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) + | Test(testExpr, kind, _r) -> + let target = emitQuotedExpr com testExpr + + match kind with + | UnionCaseTest tag -> + // Represent as: (unionTag target) = tag + let tagExpr = Helper.LibCall(com, "quotation", "mkUnionTag", Any, [ target ]) + let tagConst = emitQuotedExpr com (makeIntConst tag) + + Helper.LibCall( + com, + "quotation", + "mkCall", + Any, + [ + mkNullExpr com "null" + makeStrConst "op_Equality" + makeArray Any [ tagExpr; tagConst ] + makeStrConst "" + ] + ) + | OptionTest isSome -> + let methodName = + if isSome then + "get_IsSome" + else + "get_IsNone" + + Helper.LibCall( + com, + "quotation", + "mkCall", + Any, + [ + target + makeStrConst methodName + makeArray Any [] + makeStrConst "Microsoft.FSharp.Core.FSharpOption`1" + ] + ) + | ListTest isCons -> + let methodName = + if isCons then + "get_IsCons" + else + "get_IsEmpty" + + Helper.LibCall( + com, + "quotation", + "mkCall", + Any, + [ + target + makeStrConst methodName + makeArray Any [] + makeStrConst "Microsoft.FSharp.Collections.FSharpList`1" + ] + ) + | TypeTest typ -> + Helper.LibCall( + com, + "quotation", + "mkCall", + Any, + [ + target + makeStrConst "op_TypeTest" + makeArray Any [] + makeStrConst (typeToString typ) + ] + ) + | _ -> // 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 mkNullExpr (com: Compiler) (typ: string) : Expr = + // A runtime-built null node. Emitting this (rather than a raw null literal) + // keeps generated code compiling on statically typed targets like Rust, while + // producing the same ExprValue(null, typ) shape the other runtimes had before. + Helper.LibCall(com, "quotation", "mkNull", Any, [ makeStrConst typ ]) + 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" ]) @@ -260,7 +378,7 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio | 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" ]) + | Null _ -> mkNullExpr com "null" | CharConstant c -> Helper.LibCall(com, "quotation", "mkValue", Any, [ Value(CharConstant c, None); makeStrConst "char" ]) @@ -295,7 +413,7 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio | 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" ]) + | None -> mkNullExpr com "option" | NewList(headAndTail, _typ) -> match headAndTail with @@ -303,22 +421,61 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio 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" ]) + | None -> mkNullExpr com "list" | _ -> // Fallback for other value kinds let msg = "Unsupported quotation value" Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) +and private numberKindToString (kind: NumberKind) : string = + match kind with + | Int8 -> "int8" + | UInt8 -> "uint8" + | Int16 -> "int16" + | UInt16 -> "uint16" + | Int32 -> "int32" + | UInt32 -> "uint32" + | Int64 -> "int64" + | UInt64 -> "uint64" + | Int128 -> "int128" + | UInt128 -> "uint128" + | BigInt -> "bigint" + | NativeInt -> "nativeint" + | UNativeInt -> "unativeint" + | Float16 -> "float16" + | Float32 -> "float32" + | Float64 -> "float64" + | Decimal -> "decimal" + and private typeToString (t: Type) : string = match t with | Boolean -> "bool" - | Number(Int32, _) -> "int32" - | Number(Float64, _) -> "float64" + | Number(kind, _) -> numberKindToString kind | String -> "string" + | Char -> "char" | Unit -> "unit" + | Regex -> "regex" | Any -> "obj" + | Option(genArg, isStruct) -> + $"""%s{typeToString genArg} %s{if isStruct then + "voption" + else + "option"}""" + | List genArg -> $"%s{typeToString genArg} list" + | Array(genArg, _) -> $"%s{typeToString genArg}[]" + | Nullable(genArg, _) -> $"%s{typeToString genArg} nullable" + | DeclaredType(ref, genArgs) -> + match ref.FullName, genArgs with + | "System.Collections.Generic.IEnumerable`1", [ t ] -> $"%s{typeToString t} seq" + | "System.Guid", _ -> "guid" + | "System.DateTime", _ -> "datetime" + | "System.DateTimeOffset", _ -> "datetimeoffset" + | "System.TimeSpan", _ -> "timespan" + | fullName, _ -> fullName + | GenericParam(name, _, _) -> $"'%s{name}" | LambdaType(argType, returnType) -> $"%s{typeToString argType} -> %s{typeToString returnType}" + | DelegateType(argTypes, returnType) -> (argTypes @ [ returnType ]) |> List.map typeToString |> String.concat " -> " | Tuple(genArgs, _) -> genArgs |> List.map typeToString |> String.concat " * " | _ -> "obj" diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 58aeba6934..10bb174382 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -3662,9 +3662,10 @@ module Util = mkUnitExpr () - | Fable.Quote _ -> - addError com [] None "Quotations are not yet supported for Rust target" - mkUnitExpr () + | Fable.Quote(body, _isTyped, _r) -> + // Lower the quoted body to runtime calls that build the quotation AST, + // then transform those like any other expression (mirrors JS/Python/Beam). + QuotationEmitter.emitQuotedExpr com body |> transformExpr com ctx let rec tryFindEntryPoint (com: IRustCompiler) decl : string list option = match decl with diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index fba43b41c7..86bb9f3b55 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -3705,7 +3705,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-library-beam/fable_quotation.erl b/src/fable-library-beam/fable_quotation.erl index 117d4f967c..7523476a2f 100644 --- a/src/fable-library-beam/fable_quotation.erl +++ b/src/fable-library-beam/fable_quotation.erl @@ -1,13 +1,13 @@ -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_quot_var/3, mk_var/1, mk_value/2, mk_null/1, mk_lambda/2, + mk_application/2, mk_let/3, mk_if_then_else/3, mk_call/3, mk_call/4, 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, + get_type/1, call_declaring_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, @@ -33,11 +33,16 @@ var_get_is_mutable({var, _, _, IsMutable}) -> IsMutable. mk_var(Var) -> {expr, var_expr, Var}. mk_value(Value, Type) -> {expr, value, Value, Type}. +%% A null-value node (no instance / null literal / empty option or list). +mk_null(Type) -> {expr, value, undefined, 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}. +%% 3-arg form kept for backward compatibility (e.g. the programmatic Expr.Call API, +%% which has no separate declaring type); delegates to the 4-arg form. +mk_call(Instance, Method, Args) -> mk_call(Instance, Method, Args, <<"">>). +mk_call(Instance, Method, Args, DeclaringType) -> {expr, call, Instance, Method, Args, DeclaringType}. 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}. @@ -87,10 +92,22 @@ is_let(_) -> undefined. is_if_then_else({expr, if_then_else, G, T, E}) -> {G, T, E}; is_if_then_else(_) -> undefined. -%% Call pattern: returns {Instance, Method, Args} (dereference Args) -is_call({expr, call, I, M, A}) -> {I, M, deref(A)}; +%% Call pattern: returns {Instance, Method, Args} (dereference Args). +%% DeclaringType is stored but not part of the Patterns.Call active pattern. +%% A static/operator call carries the null-value node as its instance +%% (see QuotationEmitter); expose it as undefined so Patterns.Call matches F#. +is_call({expr, call, I, M, A, _DT}) -> + Instance = case I of + {expr, value, _, <<"null">>} -> undefined; + _ -> I + end, + {Instance, M, deref(A)}; is_call(_) -> undefined. +%% DeclaringType accessor for the Call node. +call_declaring_type({expr, call, _, _, _, DT}) -> DT; +call_declaring_type(_) -> <<"">>. + %% Sequential pattern: returns {First, Second} is_sequential({expr, sequential, F, S}) -> {F, S}; is_sequential(_) -> undefined. @@ -150,7 +167,7 @@ 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) -> +evaluate({expr, call, _Instance, Method, Args, _DT}, Env) -> EvaluatedArgs = [evaluate(A, Env) || A <- deref(Args)], apply_operator(Method, EvaluatedArgs). @@ -195,7 +212,7 @@ 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}) -> +expr_to_string({expr, call, _, Method, Args, _DT}) -> ArgsList = deref(Args), ArgsStr = lists:join(", ", [expr_to_string(A) || A <- ArgsList]), case op_symbol(Method) of @@ -260,7 +277,7 @@ collect_free_vars({expr, if_then_else, Guard, Then, Else}, Bound) -> {F2, _} = collect_free_vars(Then, Bound), {F3, _} = collect_free_vars(Else, Bound), {F1 ++ F2 ++ F3, Bound}; -collect_free_vars({expr, call, _, _, Args}, Bound) -> +collect_free_vars({expr, call, _, _, Args, _DT}, Bound) -> Fs = [F || A <- deref(Args), F <- element(1, collect_free_vars(A, Bound))], {Fs, Bound}; collect_free_vars({expr, sequential, First, Second}, Bound) -> diff --git a/src/fable-library-php/quotation.php b/src/fable-library-php/quotation.php new file mode 100644 index 0000000000..091a59d2b7 --- /dev/null +++ b/src/fable-library-php/quotation.php @@ -0,0 +1,232 @@ + $name, 'type' => $type, 'isMutable' => $isMutable]; +} + +function varGetName($v) { return $v['name']; } +function varGetType($v) { return $v['type']; } +function varGetIsMutable($v) { return $v['isMutable']; } + +// --- Expr constructors --- + +function mkValue($value, $type) { return ['tag' => 'Value', 'value' => $value, 'type' => $type]; } + +// A null-value node (no instance / null literal / empty option or list). +function mkNull($type) { return ['tag' => 'Value', 'value' => null, 'type' => $type]; } + +function mkVar($var) { return ['tag' => 'Var', 'var' => $var]; } +function mkLambda($var, $body) { return ['tag' => 'Lambda', 'var' => $var, 'body' => $body]; } +function mkApplication($func, $arg) { return ['tag' => 'Application', 'func' => $func, 'arg' => $arg]; } +function mkLet($var, $value, $body) { return ['tag' => 'Let', 'var' => $var, 'value' => $value, 'body' => $body]; } + +function mkIfThenElse($guard, $thenExpr, $elseExpr) { + return ['tag' => 'IfThenElse', 'guard' => $guard, 'thenExpr' => $thenExpr, 'elseExpr' => $elseExpr]; +} + +function mkCall($instance, $method, $args, $declaringType = '') { + return ['tag' => 'Call', 'instance' => $instance, 'method' => $method, 'args' => $args, 'declaringType' => $declaringType]; +} + +function mkSequential($first, $second) { return ['tag' => 'Sequential', 'first' => $first, 'second' => $second]; } +function mkNewTuple($elements) { return ['tag' => 'NewTuple', 'elements' => $elements]; } + +function mkNewUnion($typeName, $tag, $fields) { + return ['tag' => 'NewUnion', 'typeName' => $typeName, 'unionTag' => $tag, 'fields' => $fields]; +} + +function mkNewRecord($fieldNames, $values) { + return ['tag' => 'NewRecord', 'fieldNames' => $fieldNames, 'values' => $values]; +} + +function mkNewList($head, $tail) { return ['tag' => 'NewList', 'head' => $head, 'tail' => $tail]; } +function mkTupleGet($expr, $index) { return ['tag' => 'TupleGet', 'expr' => $expr, 'index' => $index]; } +function mkUnionTag($expr) { return ['tag' => 'UnionTag', 'expr' => $expr]; } +function mkUnionField($expr, $fieldIndex) { return ['tag' => 'UnionField', 'expr' => $expr, 'fieldIndex' => $fieldIndex]; } +function mkFieldGet($expr, $fieldName) { return ['tag' => 'FieldGet', 'expr' => $expr, 'fieldName' => $fieldName]; } +function mkFieldSet($expr, $fieldName, $value) { return ['tag' => 'FieldSet', 'expr' => $expr, 'fieldName' => $fieldName, 'value' => $value]; } +function mkVarSet($target, $value) { return ['tag' => 'VarSet', 'target' => $target, 'value' => $value]; } + +// --- Type accessor --- + +function getType($e) { + switch ($e['tag']) { + case 'Value': return $e['type']; + case 'Lambda': return $e['var']['type']; + default: return 'obj'; + } +} + +// --- Pattern helpers (null = no match, value = match) --- + +function isValue($e) { return $e['tag'] === 'Value' ? [$e['value'], $e['type']] : null; } +function isVar($e) { return $e['tag'] === 'Var' ? $e['var'] : null; } +function isLambda($e) { return $e['tag'] === 'Lambda' ? [$e['var'], $e['body']] : null; } +function isApplication($e) { return $e['tag'] === 'Application' ? [$e['func'], $e['arg']] : null; } +function isLet($e) { return $e['tag'] === 'Let' ? [$e['var'], $e['value'], $e['body']] : null; } +function isIfThenElse($e) { return $e['tag'] === 'IfThenElse' ? [$e['guard'], $e['thenExpr'], $e['elseExpr']] : null; } + +function isCall($e) { + if ($e['tag'] !== 'Call') return null; + // A static/operator call carries the null-value node as its instance; + // expose it as null so Patterns.Call matches F#. + $inst = $e['instance']; + if (is_array($inst) && ($inst['tag'] ?? null) === 'Value' && ($inst['type'] ?? null) === 'null') { + $inst = null; + } + return [$inst, $e['method'], $e['args']]; +} + +function isSequential($e) { return $e['tag'] === 'Sequential' ? [$e['first'], $e['second']] : null; } +function isNewTuple($e) { return $e['tag'] === 'NewTuple' ? $e['elements'] : null; } +function isNewUnionCase($e) { return $e['tag'] === 'NewUnion' ? [$e['typeName'], $e['unionTag'], $e['fields']] : null; } +function isNewRecord($e) { return $e['tag'] === 'NewRecord' ? [$e['fieldNames'], $e['values']] : null; } +function isTupleGet($e) { return $e['tag'] === 'TupleGet' ? [$e['expr'], $e['index']] : null; } +function isFieldGet($e) { return $e['tag'] === 'FieldGet' ? [$e['expr'], $e['fieldName']] : null; } + +// --- Free variables --- + +function getFreeVars($e) { + $free = []; + $seen = []; + $walk = function ($e, $bound) use (&$walk, &$free, &$seen) { + switch ($e['tag']) { + case 'Var': + $name = $e['var']['name']; + if (!in_array($name, $bound, true) && !in_array($name, $seen, true)) { + $free[] = $e['var']; + $seen[] = $name; + } + break; + case 'Lambda': + $walk($e['body'], array_merge($bound, [$e['var']['name']])); + break; + case 'Let': + $walk($e['value'], $bound); + $walk($e['body'], array_merge($bound, [$e['var']['name']])); + break; + case 'Application': + $walk($e['func'], $bound); + $walk($e['arg'], $bound); + break; + case 'IfThenElse': + $walk($e['guard'], $bound); + $walk($e['thenExpr'], $bound); + $walk($e['elseExpr'], $bound); + break; + case 'Call': + foreach ($e['args'] as $a) $walk($a, $bound); + break; + case 'Sequential': + $walk($e['first'], $bound); + $walk($e['second'], $bound); + break; + case 'NewTuple': + foreach ($e['elements'] as $el) $walk($el, $bound); + break; + case 'TupleGet': + $walk($e['expr'], $bound); + break; + case 'FieldGet': + $walk($e['expr'], $bound); + break; + } + }; + $walk($e, []); + return $free; +} + +// --- Substitution --- + +function substitute($e, $fn) { + $sub = function ($e) use (&$sub, $fn) { + switch ($e['tag']) { + case 'Var': + $r = $fn($e['var']); + return $r !== null ? $r : $e; + case 'Lambda': return mkLambda($e['var'], $sub($e['body'])); + case 'Let': return mkLet($e['var'], $sub($e['value']), $sub($e['body'])); + case 'Application': return mkApplication($sub($e['func']), $sub($e['arg'])); + case 'IfThenElse': return mkIfThenElse($sub($e['guard']), $sub($e['thenExpr']), $sub($e['elseExpr'])); + case 'Call': + return mkCall($e['instance'], $e['method'], array_map($sub, $e['args']), $e['declaringType']); + case 'Sequential': return mkSequential($sub($e['first']), $sub($e['second'])); + case 'NewTuple': return mkNewTuple(array_map($sub, $e['elements'])); + default: return $e; + } + }; + return $sub($e); +} + +// --- Evaluation (structural cases + common operators) --- + +const OPERATORS = [ + 'op_Addition', 'op_Subtraction', 'op_Multiply', 'op_Division', 'op_Modulus', + 'op_UnaryNegation', 'op_Equality', 'op_Inequality', 'op_LessThan', 'op_LessThanOrEqual', + 'op_GreaterThan', 'op_GreaterThanOrEqual', 'op_BooleanAnd', 'op_BooleanOr', 'op_LogicalNot', +]; + +function applyOperator($method, $args) { + switch ($method) { + case 'op_Addition': return $args[0] + $args[1]; + case 'op_Subtraction': return $args[0] - $args[1]; + case 'op_Multiply': return $args[0] * $args[1]; + case 'op_Division': return intdiv($args[0], $args[1]); + case 'op_Modulus': return $args[0] % $args[1]; + case 'op_UnaryNegation': return -$args[0]; + case 'op_Equality': return $args[0] === $args[1]; + case 'op_Inequality': return $args[0] !== $args[1]; + case 'op_LessThan': return $args[0] < $args[1]; + case 'op_LessThanOrEqual': return $args[0] <= $args[1]; + case 'op_GreaterThan': return $args[0] > $args[1]; + case 'op_GreaterThanOrEqual': return $args[0] >= $args[1]; + case 'op_BooleanAnd': return $args[0] && $args[1]; + case 'op_BooleanOr': return $args[0] || $args[1]; + case 'op_LogicalNot': return !$args[0]; + default: throw new \Exception("Cannot evaluate method: $method"); + } +} + +function evaluate($e) { + $eval = function ($e, $env) use (&$eval) { + switch ($e['tag']) { + case 'Value': return $e['value']; + case 'Var': return $env[$e['var']['name']]; + case 'Lambda': + $var = $e['var']; $body = $e['body']; + return function ($arg) use (&$eval, $var, $body, $env) { + $newEnv = $env; $newEnv[$var['name']] = $arg; + return $eval($body, $newEnv); + }; + case 'Application': + $f = $eval($e['func'], $env); + return $f($eval($e['arg'], $env)); + case 'Let': + $newEnv = $env; $newEnv[$e['var']['name']] = $eval($e['value'], $env); + return $eval($e['body'], $newEnv); + case 'IfThenElse': + return $eval($e['guard'], $env) ? $eval($e['thenExpr'], $env) : $eval($e['elseExpr'], $env); + case 'Sequential': + $eval($e['first'], $env); + return $eval($e['second'], $env); + case 'NewTuple': + return array_map(function ($el) use (&$eval, $env) { return $eval($el, $env); }, $e['elements']); + case 'Call': + $evalArgs = array_map(function ($a) use (&$eval, $env) { return $eval($a, $env); }, $e['args']); + return applyOperator($e['method'], $evalArgs); + case 'TupleGet': + return $eval($e['expr'], $env)[$e['index']]; + default: + throw new \Exception("Cannot evaluate expression: " . $e['tag']); + } + }; + return $eval($e, []); +} diff --git a/src/fable-library-py/fable_library/quotation.py b/src/fable-library-py/fable_library/quotation.py index b3d817eec2..1809f17202 100644 --- a/src/fable-library-py/fable_library/quotation.py +++ b/src/fable-library-py/fable_library/quotation.py @@ -90,6 +90,7 @@ class ExprCall: instance: Any method: str args: Array[Any] + declaring_type: str = "" @dataclass @@ -190,6 +191,11 @@ def mk_value(value: Any, type: str) -> ExprValue: return ExprValue(value, type) +def mk_null(type: str) -> ExprValue: + # A null-value node (no instance / null literal / empty option or list). + return ExprValue(None, type) + + def mk_var(var: Var) -> ExprVarExpr: return ExprVarExpr(var) @@ -210,8 +216,8 @@ def mk_if_then_else(guard: Expr, then_expr: Expr, else_expr: Expr) -> ExprIfThen return ExprIfThenElse(guard, then_expr, else_expr) -def mk_call(instance: Any, method: str, args: Array[Any]) -> ExprCall: - return ExprCall(instance, method, args) +def mk_call(instance: Any, method: str, args: Array[Any], declaring_type: str = "") -> ExprCall: + return ExprCall(instance, method, args, declaring_type) def mk_sequential(first: Expr, second: Expr) -> ExprSequential: @@ -316,10 +322,19 @@ def is_if_then_else(expr: Expr) -> tuple[Expr, Expr, Expr] | None: def is_call(expr: Expr) -> tuple[Any, str, Array[Any]] | None: if isinstance(expr, ExprCall): - return (expr.instance, expr.method, expr.args) + # A static/operator call carries the null-value node as its instance + # (see QuotationEmitter). Expose it as None so Patterns.Call matches F#. + instance = None if (isinstance(expr.instance, ExprValue) and expr.instance.type == "null") else expr.instance + return (instance, expr.method, expr.args) return None +def call_declaring_type(expr: Expr) -> str: + if isinstance(expr, ExprCall): + return expr.declaring_type + return "" + + def is_sequential(expr: Expr) -> tuple[Expr, Expr] | None: if isinstance(expr, ExprSequential): return (expr.first, expr.second) @@ -601,7 +616,7 @@ def sub(e: Expr) -> Expr: 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): + case ExprCall(instance=instance, method=method, args=args, declaring_type=declaring_type): new_inst = ( sub(instance) if isinstance( @@ -620,7 +635,7 @@ def sub(e: Expr) -> Expr: ) else instance ) - return ExprCall(new_inst, method, type(args)([sub(a) for a in args])) + return ExprCall(new_inst, method, type(args)([sub(a) for a in args]), declaring_type) case ExprSequential(first=first, second=second): return ExprSequential(sub(first), sub(second)) case ExprNewTuple(elements=elements): diff --git a/src/fable-library-rust/src/Fable.Library.Rust.fsproj b/src/fable-library-rust/src/Fable.Library.Rust.fsproj index 0dffee4111..30ccc1242e 100644 --- a/src/fable-library-rust/src/Fable.Library.Rust.fsproj +++ b/src/fable-library-rust/src/Fable.Library.Rust.fsproj @@ -27,6 +27,8 @@ + + diff --git a/src/fable-library-rust/src/Quotation.fs b/src/fable-library-rust/src/Quotation.fs new file mode 100644 index 0000000000..711833bf8e --- /dev/null +++ b/src/fable-library-rust/src/Quotation.fs @@ -0,0 +1,248 @@ +// F# quotation runtime for the Fable Rust target (functions). +// +// Authored in F# and transpiled by Fable so the FSharpExpr/FSharpVar types land at the +// right namespace and every mk*/is* signature matches what generated user code expects. +// The module is named `quotation_` to match the `libModule = "quotation"` import path. +module quotation_ + +open Microsoft.FSharp.Quotations + +// --- Var constructor + accessors --- + +let mkQuotVar (name: string) (typ: string) (isMutable: bool) : FSharpVar = + { + Name = name + Type = typ + IsMutable = isMutable + } + +let varGetName (v: FSharpVar) : string = v.Name +let varGetType (v: FSharpVar) : string = v.Type +let varGetIsMutable (v: FSharpVar) : bool = v.IsMutable + +// --- Expr constructors --- + +// Generic so callers can pass an unboxed literal (i32/bool/string/...); `box` erases +// it to obj. A non-generic obj parameter would require the caller to box, which the +// emitter does not do on statically typed targets. +let mkValue<'T> (value: 'T) (typ: string) : FSharpExpr = ExprValue(box value, typ) + +// A null-value node (no instance / null literal / empty option or list). +// The payload is a boxed sentinel (not a null literal, which the Rust target can't +// represent for an untyped obj); consumers distinguish null nodes by the type tag. +let mkNull (typ: string) : FSharpExpr = ExprValue(box 0, typ) + +let mkVar (v: FSharpVar) : FSharpExpr = ExprVarExpr v +let mkLambda (v: FSharpVar) (body: FSharpExpr) : FSharpExpr = ExprLambda(v, body) +let mkApplication (func: FSharpExpr) (arg: FSharpExpr) : FSharpExpr = ExprApplication(func, arg) +let mkLet (v: FSharpVar) (value: FSharpExpr) (body: FSharpExpr) : FSharpExpr = ExprLet(v, value, body) + +let mkIfThenElse (guard: FSharpExpr) (thenExpr: FSharpExpr) (elseExpr: FSharpExpr) : FSharpExpr = + ExprIfThenElse(guard, thenExpr, elseExpr) + +let mkCall (instance: FSharpExpr) (method: string) (args: FSharpExpr[]) (declaringType: string) : FSharpExpr = + ExprCall(instance, method, args, declaringType) + +let mkSequential (first: FSharpExpr) (second: FSharpExpr) : FSharpExpr = ExprSequential(first, second) +let mkNewTuple (elements: FSharpExpr[]) : FSharpExpr = ExprNewTuple elements + +let mkNewUnion (typeName: string) (tag: int) (fields: FSharpExpr[]) : FSharpExpr = ExprNewUnion(typeName, tag, fields) + +let mkNewRecord (fieldNames: string[]) (values: FSharpExpr[]) : FSharpExpr = ExprNewRecord(fieldNames, values) +let mkNewList (head: FSharpExpr) (tail: FSharpExpr) : FSharpExpr = ExprNewList(head, tail) +let mkTupleGet (e: FSharpExpr) (index: int) : FSharpExpr = ExprTupleGet(e, index) +let mkUnionTag (e: FSharpExpr) : FSharpExpr = ExprUnionTag e +let mkUnionField (e: FSharpExpr) (fieldIndex: int) : FSharpExpr = ExprUnionField(e, fieldIndex) +let mkFieldGet (e: FSharpExpr) (fieldName: string) : FSharpExpr = ExprFieldGet(e, fieldName) +let mkFieldSet (e: FSharpExpr) (fieldName: string) (value: FSharpExpr) : FSharpExpr = ExprFieldSet(e, fieldName, value) +let mkVarSet (target: FSharpExpr) (value: FSharpExpr) : FSharpExpr = ExprVarSet(target, value) + +// --- Type accessor --- + +let getType (e: FSharpExpr) : string = + match e with + | ExprValue(_, t) -> t + | ExprLambda(v, _) -> v.Type + | _ -> "obj" + +// --- Pattern helpers (return option, following the active-pattern convention) --- + +let isValue (e: FSharpExpr) = + match e with + | ExprValue(v, t) -> Some(v, t) + | _ -> None + +let isVar (e: FSharpExpr) = + match e with + | ExprVarExpr v -> Some v + | _ -> None + +let isLambda (e: FSharpExpr) = + match e with + | ExprLambda(v, b) -> Some(v, b) + | _ -> None + +let isApplication (e: FSharpExpr) = + match e with + | ExprApplication(f, a) -> Some(f, a) + | _ -> None + +let isLet (e: FSharpExpr) = + match e with + | ExprLet(v, value, body) -> Some(v, value, body) + | _ -> None + +let isIfThenElse (e: FSharpExpr) = + match e with + | ExprIfThenElse(g, t, el) -> Some(g, t, el) + | _ -> None + +let isCall (e: FSharpExpr) = + match e with + | ExprCall(instance, m, args, _dt) -> + // A static/operator call carries the null-value node as its instance; + // expose it as None so Patterns.Call matches F#. + let inst = + match instance with + | ExprValue(_, "null") -> None + | _ -> Some instance + + Some(inst, m, List.ofArray args) + | _ -> None + +let isSequential (e: FSharpExpr) = + match e with + | ExprSequential(f, s) -> Some(f, s) + | _ -> None + +let isNewTuple (e: FSharpExpr) = + match e with + | ExprNewTuple els -> Some(List.ofArray els) + | _ -> None + +let isNewUnionCase (e: FSharpExpr) = + match e with + | ExprNewUnion(n, tag, fields) -> Some(n, tag, List.ofArray fields) + | _ -> None + +let isNewRecord (e: FSharpExpr) = + match e with + | ExprNewRecord(names, values) -> Some(List.ofArray names, List.ofArray values) + | _ -> None + +let isTupleGet (e: FSharpExpr) = + match e with + | ExprTupleGet(inner, i) -> Some(inner, i) + | _ -> None + +let isFieldGet (e: FSharpExpr) = + match e with + | ExprFieldGet(inner, n) -> Some(inner, n) + | _ -> None + +// --- Free variables --- + +let getFreeVars (e: FSharpExpr) : FSharpVar list = + let seen = System.Collections.Generic.HashSet() + let acc = ResizeArray() + + let rec walk (bound: Set) (e: FSharpExpr) = + match e with + | ExprVarExpr v -> + if not (bound.Contains v.Name) && seen.Add v.Name then + acc.Add v + | ExprLambda(v, body) -> walk (bound.Add v.Name) body + | ExprLet(v, value, body) -> + walk bound value + walk (bound.Add v.Name) body + | ExprApplication(f, a) -> + walk bound f + walk bound a + | ExprIfThenElse(g, t, el) -> + walk bound g + walk bound t + walk bound el + | ExprCall(_, _, args, _) -> + for a in args do + walk bound a + | ExprSequential(f, s) -> + walk bound f + walk bound s + | ExprNewTuple els -> + for el in els do + walk bound el + | ExprTupleGet(inner, _) -> walk bound inner + | ExprFieldGet(inner, _) -> walk bound inner + | _ -> () + + walk Set.empty e + List.ofSeq acc + +// --- Evaluation (structural cases + common operators; SQL translation deconstructs +// rather than evaluates, so this covers the tested subset). --- + +let private applyOperator (method: string) (args: obj list) : obj = + // Extract args positionally rather than binding them in the match: pattern-binding + // obj (Rc) variables makes Fable zero-initialize a fat pointer, which is + // invalid at runtime. Matching only on the method string avoids that. + // if/elif (not match): Fable-Rust would emit string literals as match patterns, + // which isn't valid Rust; an if-chain compiles to string == comparisons. + let i (n: int) : int = unbox (List.item n args) + let b (n: int) : bool = unbox (List.item n args) + + if method = "op_Addition" then + box (i 0 + i 1) + elif method = "op_Subtraction" then + box (i 0 - i 1) + elif method = "op_Multiply" then + box (i 0 * i 1) + elif method = "op_Division" then + box (i 0 / i 1) + elif method = "op_Modulus" then + box (i 0 % i 1) + elif method = "op_UnaryNegation" then + box (-(i 0)) + elif method = "op_Equality" then + box (i 0 = i 1) + elif method = "op_Inequality" then + box (i 0 <> i 1) + elif method = "op_LessThan" then + box (i 0 < i 1) + elif method = "op_LessThanOrEqual" then + box (i 0 <= i 1) + elif method = "op_GreaterThan" then + box (i 0 > i 1) + elif method = "op_GreaterThanOrEqual" then + box (i 0 >= i 1) + elif method = "op_BooleanAnd" then + box (b 0 && b 1) + elif method = "op_BooleanOr" then + box (b 0 || b 1) + elif method = "op_LogicalNot" then + box (not (b 0)) + else + failwithf "Cannot evaluate method: %s" method + +let evaluate (e: FSharpExpr) : obj = + let rec eval (env: Map) (e: FSharpExpr) : obj = + match e with + | ExprValue(v, _) -> v + | ExprVarExpr v -> Map.find v.Name env + | ExprLambda(v, body) -> box (fun (arg: obj) -> eval (Map.add v.Name arg env) body) + | ExprApplication(f, a) -> + let func = unbox obj> (eval env f) + func (eval env a) + | ExprLet(v, value, body) -> eval (Map.add v.Name (eval env value) env) body + | ExprIfThenElse(g, t, el) -> + if unbox (eval env g) then + eval env t + else + eval env el + | ExprSequential(a, b) -> + eval env a |> ignore + eval env b + | ExprCall(_, method, args, _) -> applyOperator method [ for a in args -> eval env a ] + | ExprTupleGet(inner, _) -> eval env inner + | _ -> failwith "Cannot evaluate expression" + + eval Map.empty e diff --git a/src/fable-library-rust/src/QuotationTypes.fs b/src/fable-library-rust/src/QuotationTypes.fs new file mode 100644 index 0000000000..56cc6f3f12 --- /dev/null +++ b/src/fable-library-rust/src/QuotationTypes.fs @@ -0,0 +1,35 @@ +// F# quotation AST types for the Fable Rust target. +// +// Declared in the Microsoft.FSharp.Quotations namespace with the *compiled* names +// (FSharpExpr / FSharpVar) that Fable maps the real FSharp.Core Quotations.Expr / .Var +// to on the Rust target. Using the compiled names (rather than Expr/Var) means no clash +// with FSharp.Core during F# type-checking, while still resolving to the same Rust path +// (Microsoft::FSharp::Quotations::FSharpExpr) that generated user code references. +namespace Microsoft.FSharp.Quotations + +type FSharpVar = + { + Name: string + Type: string + IsMutable: bool + } + +type FSharpExpr = + | ExprValue of value: obj * typ: string + | ExprVarExpr of var: FSharpVar + | ExprLambda of var: FSharpVar * body: FSharpExpr + | ExprApplication of func: FSharpExpr * arg: FSharpExpr + | ExprLet of var: FSharpVar * value: FSharpExpr * body: FSharpExpr + | ExprIfThenElse of guard: FSharpExpr * thenExpr: FSharpExpr * elseExpr: FSharpExpr + | ExprCall of instance: FSharpExpr * method: string * args: FSharpExpr[] * declaringType: string + | ExprSequential of first: FSharpExpr * second: FSharpExpr + | ExprNewTuple of elements: FSharpExpr[] + | ExprNewUnion of typeName: string * tag: int * fields: FSharpExpr[] + | ExprNewRecord of fieldNames: string[] * values: FSharpExpr[] + | ExprNewList of head: FSharpExpr * tail: FSharpExpr + | ExprTupleGet of expr: FSharpExpr * index: int + | ExprUnionTag of expr: FSharpExpr + | ExprUnionField of expr: FSharpExpr * fieldIndex: int + | ExprFieldGet of expr: FSharpExpr * fieldName: string + | ExprFieldSet of expr: FSharpExpr * fieldName: string * value: FSharpExpr + | ExprVarSet of target: FSharpExpr * value: FSharpExpr diff --git a/src/fable-library-ts/quotation.ts b/src/fable-library-ts/quotation.ts index 1a8e6c02a9..7777a99cdb 100644 --- a/src/fable-library-ts/quotation.ts +++ b/src/fable-library-ts/quotation.ts @@ -88,7 +88,8 @@ export class ExprCall { instance: Expr | null; method: string; args: Expr[]; - constructor(instance: Expr | null, method: string, args: Expr[]) { this.instance = instance; this.method = method; this.args = args; } + declaringType: string; + constructor(instance: Expr | null, method: string, args: Expr[], declaringType: string = "") { this.instance = instance; this.method = method; this.args = args; this.declaringType = declaringType; } toJSON() { return ["Call", this.instance, this.method, this.args]; } } @@ -208,6 +209,11 @@ export function mkValue(value: any, type: string): ExprValue { return new ExprValue(value, type); } +export function mkNull(type: string): ExprValue { + // A null-value node (no instance / null literal / empty option or list). + return new ExprValue(null, type); +} + export function mkVar(v: Var): ExprVarExpr { return new ExprVarExpr(v); } @@ -228,8 +234,8 @@ export function mkIfThenElse(guard: Expr, thenExpr: Expr, elseExpr: Expr): ExprI return new ExprIfThenElse(guard, thenExpr, elseExpr); } -export function mkCall(instance: Expr | null, method: string, args: Expr[]): ExprCall { - return new ExprCall(instance, method, args); +export function mkCall(instance: Expr | null, method: string, args: Expr[], declaringType: string = ""): ExprCall { + return new ExprCall(instance, method, args, declaringType); } export function mkSequential(first: Expr, second: Expr): ExprSequential { @@ -323,10 +329,19 @@ export function isIfThenElse(expr: Expr): [Expr, Expr, Expr] | undefined { } export function isCall(expr: Expr): [Expr | null, string, Expr[]] | undefined { - if (expr instanceof ExprCall) return [expr.instance, expr.method, expr.args]; + if (expr instanceof ExprCall) { + // A static/operator call carries the null-value node as its instance + // (see QuotationEmitter). Expose it as null so Patterns.Call matches F#. + const instance = (expr.instance instanceof ExprValue && expr.instance.type === "null") ? null : expr.instance; + return [instance, expr.method, expr.args]; + } return undefined; } +export function callDeclaringType(expr: Expr): string { + return expr instanceof ExprCall ? expr.declaringType : ""; +} + export function isSequential(expr: Expr): [Expr, Expr] | undefined { if (expr instanceof ExprSequential) return [expr.first, expr.second]; return undefined; @@ -590,7 +605,7 @@ export function substitute(expr: Expr, fn: (v: Var) => Expr | undefined): Expr { if (e instanceof ExprIfThenElse) return new ExprIfThenElse(sub(e.guard), sub(e.thenExpr), sub(e.elseExpr)); if (e instanceof ExprCall) { const newInst = e.instance != null ? sub(e.instance) : e.instance; - return new ExprCall(newInst, e.method, e.args.map(sub)); + return new ExprCall(newInst, e.method, e.args.map(sub), e.declaringType); } if (e instanceof ExprSequential) return new ExprSequential(sub(e.first), sub(e.second)); if (e instanceof ExprNewTuple) return new ExprNewTuple(e.elements.map(sub)); diff --git a/tests/Php/Fable.Tests.Php.fsproj b/tests/Php/Fable.Tests.Php.fsproj index 509b2813e4..8556323ce8 100644 --- a/tests/Php/Fable.Tests.Php.fsproj +++ b/tests/Php/Fable.Tests.Php.fsproj @@ -31,6 +31,7 @@ + diff --git a/tests/Php/TestQuotation.fs b/tests/Php/TestQuotation.fs new file mode 100644 index 0000000000..dee0fb81ad --- /dev/null +++ b/tests/Php/TestQuotation.fs @@ -0,0 +1,71 @@ +module Fable.Tests.Quotation + +open Util.Testing +open Microsoft.FSharp.Quotations +open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Linq.RuntimeHelpers + +[] +let ``test Integer value quotation`` () = + match <@ 42 @> with + | Value(v, _) -> equal 42 (v :?> int) + | _ -> failwith "Expected Value" + +[] +let ``test String value quotation`` () = + match <@ "hello" @> with + | Value(v, _) -> equal "hello" (v :?> string) + | _ -> failwith "Expected Value" + +[] +let ``test Lambda quotation`` () = + match <@ fun x -> x + 1 @> with + | Lambda(v, _body) -> equal "x" v.Name + | _ -> failwith "Expected Lambda" + +[] +let ``test Let binding quotation`` () = + match <@ let x = 5 in x @> with + | Let(v, Value(value, _), _) -> + equal "x" v.Name + equal 5 (value :?> int) + | _ -> failwith "Expected Let" + +[] +let ``test IfThenElse quotation`` () = + match <@ if true then 1 else 2 @> with + | IfThenElse(_, _, _) -> () + | _ -> failwith "Expected IfThenElse" + +[] +let ``test Application quotation`` () = + match <@ (fun x -> x) 42 @> with + | Application(Lambda _, Value _) -> () + | _ -> failwith "Expected Application of Lambda" + +[] +let ``test NewTuple quotation`` () = + match <@ (1, 2, 3) @> with + | NewTuple(exprs) -> equal 3 exprs.Length + | _ -> failwith "Expected NewTuple" + +[] +let ``test Option match deconstructs to IfThenElse`` () = + match <@ fun (o: int option) -> match o with Some v -> v | None -> 0 @> with + | Lambda(_, IfThenElse(_, _, _)) -> () + | _ -> failwith "Expected Lambda with IfThenElse body" + +[] +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 lambda application`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ (fun x -> x + 1) 5 @> + equal 6 (result :?> int) diff --git a/tests/Python/TestQuotation.fs b/tests/Python/TestQuotation.fs index 9d0cb8705a..3487d38089 100644 --- a/tests/Python/TestQuotation.fs +++ b/tests/Python/TestQuotation.fs @@ -161,3 +161,19 @@ let ``test Expr.GetFreeVars returns empty for closed expr`` () = let q = <@ fun x -> x + 1 @> let freeVars = q.GetFreeVars() |> Seq.length equal 0 freeVars + +// --- Pattern matches lower to IfThenElse (DecisionTree/Test handling) --- + +[] +let ``test Option match deconstructs to IfThenElse`` () = + let q = <@ fun (o: int option) -> match o with Some v -> v | None -> 0 @> + match q with + | Lambda(_, IfThenElse(_, _, _)) -> () + | _ -> failwith "Expected Lambda with IfThenElse body" + +[] +let ``test Literal match deconstructs to IfThenElse`` () = + let q = <@ fun (x: int) -> match x with 0 -> "zero" | _ -> "other" @> + match q with + | Lambda(_, IfThenElse(_, _, _)) -> () + | _ -> failwith "Expected Lambda with IfThenElse body" diff --git a/tests/Rust/Fable.Tests.Rust.fsproj b/tests/Rust/Fable.Tests.Rust.fsproj index 11f806fe01..7067f08f38 100644 --- a/tests/Rust/Fable.Tests.Rust.fsproj +++ b/tests/Rust/Fable.Tests.Rust.fsproj @@ -58,6 +58,7 @@ + diff --git a/tests/Rust/tests/src/QuotationTests.fs b/tests/Rust/tests/src/QuotationTests.fs new file mode 100644 index 0000000000..b228ef064a --- /dev/null +++ b/tests/Rust/tests/src/QuotationTests.fs @@ -0,0 +1,60 @@ +module Fable.Tests.QuotationTests + +open Util.Testing +open Microsoft.FSharp.Quotations +open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Linq.RuntimeHelpers + +// NOTE: quotations are deconstructed through an untyped `Expr` parameter. Matching a +// typed `Expr<'T>` binding directly is not yet supported on Rust (see quotation notes), +// and `obj` values are not bound out of patterns (only structure / Var names / arithmetic +// evaluation are exercised). + +let private lambdaVarName (e: Expr) = + match e with + | Lambda(v, _body) -> v.Name + | _ -> "?" + +let private letVarName (e: Expr) = + match e with + | Let(v, _value, _body) -> v.Name + | _ -> "?" + +let private isIfThenElseNode (e: Expr) = + match e with + | IfThenElse(_g, _t, _e) -> true + | _ -> false + +[] +let ``Evaluate a value`` () = + // Value nodes are exercised via evaluate: deconstructing them directly on Rust is + // limited (the obj payload trips getZero; the type element is System.Type, not string). + let r = LeafExpressionConverter.EvaluateQuotation <@ 42 @> + unbox r |> equal 42 + +[] +let ``Lambda quotation exposes the parameter name`` () = + lambdaVarName <@ fun (x: int) -> x + 1 @> |> equal "x" + +[] +let ``Let quotation exposes the bound name`` () = + letVarName <@ let y = 5 in y + 1 @> |> equal "y" + +[] +let ``IfThenElse quotation deconstructs`` () = + isIfThenElseNode <@ if true then 1 else 2 @> |> equal true + +[] +let ``Evaluate addition`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ 2 + 3 @> + unbox r |> equal 5 + +[] +let ``Evaluate subtraction`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ 10 - 4 @> + unbox r |> equal 6 + +[] +let ``Evaluate comparison`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ 5 > 3 @> + unbox r |> equal true From 76c8306e4e6f725b00ea136e2c97df5508e1c45a Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Tue, 14 Jul 2026 10:33:24 +0100 Subject: [PATCH 2/2] test: Test suite consistency improved. --- src/fable-library-rust/src/Map.fs | 37 ++++--- tests/Beam/QuotationTests.fs | 33 +++++- tests/Js/Main/QuotationTests.fs | 12 +++ tests/Php/TestQuotation.fs | 140 ++++++++++++++++++++++--- tests/Rust/tests/src/MapTests.fs | 11 ++ tests/Rust/tests/src/QuotationTests.fs | 15 +++ 6 files changed, 218 insertions(+), 30 deletions(-) diff --git a/src/fable-library-rust/src/Map.fs b/src/fable-library-rust/src/Map.fs index 326ca7366c..0c308b4434 100644 --- a/src/fable-library-rust/src/Map.fs +++ b/src/fable-library-rust/src/Map.fs @@ -156,21 +156,34 @@ let rec tryGetValue k (v: byref<'V>) (m: Map<'K, 'V>) = let throwKeyNotFound () = failwith SR.keyNotFound // [] -let find k (m: Map<'K, 'V>) = - let mutable v = Unchecked.defaultof<'V> +// Returns the value directly as an option rather than through a byref out-param. +// The out-param form (see tryGetValue) needs `Unchecked.defaultof<'V>`, which Fable-Rust +// lowers to getZero/mem::zeroed and panics for reference-type values (e.g. obj). This +// form works for both value and reference types. +let rec tryFindOpt k (m: Map<'K, 'V>) : 'V option = + match getRoot m with + | None -> None + | Some t -> + let c = compare k t.Key - if tryGetValue k &v m then - v - else - throwKeyNotFound () + if c = 0 then + Some t.Value + elif t.Height = 1 then + None + else + tryFindOpt + k + (if c < 0 then + t.Left + else + t.Right) -let tryFind k (m: Map<'K, 'V>) = - let mutable v = Unchecked.defaultof<'V> +let find k (m: Map<'K, 'V>) = + match tryFindOpt k m with + | Some v -> v + | None -> throwKeyNotFound () - if tryGetValue k &v m then - Some v - else - None +let tryFind k (m: Map<'K, 'V>) = tryFindOpt k m let item k (m: Map<'K, 'V>) = find k m diff --git a/tests/Beam/QuotationTests.fs b/tests/Beam/QuotationTests.fs index 8163afde08..3487d38089 100644 --- a/tests/Beam/QuotationTests.fs +++ b/tests/Beam/QuotationTests.fs @@ -62,6 +62,16 @@ 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 @> @@ -73,10 +83,15 @@ let ``test Evaluate let binding`` () = equal 15 (result :?> int) [] -let ``test Evaluate if then else`` () = +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 @> @@ -146,3 +161,19 @@ let ``test Expr.GetFreeVars returns empty for closed expr`` () = let q = <@ fun x -> x + 1 @> let freeVars = q.GetFreeVars() |> Seq.length equal 0 freeVars + +// --- Pattern matches lower to IfThenElse (DecisionTree/Test handling) --- + +[] +let ``test Option match deconstructs to IfThenElse`` () = + let q = <@ fun (o: int option) -> match o with Some v -> v | None -> 0 @> + match q with + | Lambda(_, IfThenElse(_, _, _)) -> () + | _ -> failwith "Expected Lambda with IfThenElse body" + +[] +let ``test Literal match deconstructs to IfThenElse`` () = + let q = <@ fun (x: int) -> match x with 0 -> "zero" | _ -> "other" @> + match q with + | Lambda(_, IfThenElse(_, _, _)) -> () + | _ -> failwith "Expected Lambda with IfThenElse body" diff --git a/tests/Js/Main/QuotationTests.fs b/tests/Js/Main/QuotationTests.fs index 7366cde640..9b8bfb01c6 100644 --- a/tests/Js/Main/QuotationTests.fs +++ b/tests/Js/Main/QuotationTests.fs @@ -135,6 +135,18 @@ let tests = let freeVars = q.GetFreeVars() |> Seq.length equal 0 freeVars + testCase "Option match deconstructs to IfThenElse" <| fun () -> + let q = <@ fun (o: int option) -> match o with Some v -> v | None -> 0 @> + match q with + | Lambda(_, IfThenElse(_, _, _)) -> () + | _ -> failwith "Expected Lambda with IfThenElse body" + + testCase "Literal match deconstructs to IfThenElse" <| fun () -> + let q = <@ fun (x: int) -> match x with 0 -> "zero" | _ -> "other" @> + match q with + | Lambda(_, IfThenElse(_, _, _)) -> () + | _ -> failwith "Expected Lambda with IfThenElse body" + testCase "JSON serialization produces Thoth Auto-compatible format" <| fun () -> let q = <@ 42 @> let json = Fable.Core.JS.JSON.stringify(q) diff --git a/tests/Php/TestQuotation.fs b/tests/Php/TestQuotation.fs index dee0fb81ad..679e7f921a 100644 --- a/tests/Php/TestQuotation.fs +++ b/tests/Php/TestQuotation.fs @@ -6,26 +6,37 @@ open Microsoft.FSharp.Quotations.Patterns open Microsoft.FSharp.Linq.RuntimeHelpers [] -let ``test Integer value quotation`` () = - match <@ 42 @> with - | Value(v, _) -> equal 42 (v :?> int) +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`` () = - match <@ "hello" @> with - | Value(v, _) -> equal "hello" (v :?> string) + let q = <@ "hello" @> + match q with + | Value(v, _t) -> equal "hello" (v :?> string) | _ -> failwith "Expected Value" [] let ``test Lambda quotation`` () = - match <@ fun x -> x + 1 @> with + let q = <@ fun x -> x + 1 @> + match q with | Lambda(v, _body) -> equal "x" v.Name | _ -> failwith "Expected Lambda" [] let ``test Let binding quotation`` () = - match <@ let x = 5 in x @> with + let q = <@ let x = 5 in x @> + match q with | Let(v, Value(value, _), _) -> equal "x" v.Name equal 5 (value :?> int) @@ -33,27 +44,32 @@ let ``test Let binding quotation`` () = [] let ``test IfThenElse quotation`` () = - match <@ if true then 1 else 2 @> with + let q = <@ if true then 1 else 2 @> + match q with | IfThenElse(_, _, _) -> () | _ -> failwith "Expected IfThenElse" [] let ``test Application quotation`` () = - match <@ (fun x -> x) 42 @> with + let q = <@ (fun x -> x) 42 @> + match q with | Application(Lambda _, Value _) -> () | _ -> failwith "Expected Application of Lambda" [] -let ``test NewTuple quotation`` () = - match <@ (1, 2, 3) @> with - | NewTuple(exprs) -> equal 3 exprs.Length - | _ -> failwith "Expected NewTuple" +let ``test Evaluate simple value`` () = + let result = LeafExpressionConverter.EvaluateQuotation <@ 42 @> + equal 42 (result :?> int) [] -let ``test Option match deconstructs to IfThenElse`` () = - match <@ fun (o: int option) -> match o with Some v -> v | None -> 0 @> with - | Lambda(_, IfThenElse(_, _, _)) -> () - | _ -> failwith "Expected Lambda with IfThenElse body" +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`` () = @@ -65,7 +81,97 @@ 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 + +// --- Pattern matches lower to IfThenElse (DecisionTree/Test handling) --- + +[] +let ``test Option match deconstructs to IfThenElse`` () = + let q = <@ fun (o: int option) -> match o with Some v -> v | None -> 0 @> + match q with + | Lambda(_, IfThenElse(_, _, _)) -> () + | _ -> failwith "Expected Lambda with IfThenElse body" + +[] +let ``test Literal match deconstructs to IfThenElse`` () = + let q = <@ fun (x: int) -> match x with 0 -> "zero" | _ -> "other" @> + match q with + | Lambda(_, IfThenElse(_, _, _)) -> () + | _ -> failwith "Expected Lambda with IfThenElse body" diff --git a/tests/Rust/tests/src/MapTests.fs b/tests/Rust/tests/src/MapTests.fs index f478598e31..2e456284ed 100644 --- a/tests/Rust/tests/src/MapTests.fs +++ b/tests/Rust/tests/src/MapTests.fs @@ -230,6 +230,17 @@ let ``Map.find with option works`` () = xs |> Map.find 1 |> (fun x -> x.Value) |> equal 1. +[] +let ``Map.find works with reference-type values`` () = + // Reference-type (Rc-wrapped) values used to panic: find/tryFind initialized a + // byref out-param with Unchecked.defaultof, which lowers to getZero/mem::zeroed. + let xs = Map [ 1, box "one"; 2, box 99 ] + xs |> Map.find 1 |> unbox |> equal "one" + xs |> Map.find 2 |> unbox |> equal 99 + let strs = Map [ "a", [ 10; 20 ]; "b", [ 30 ] ] + strs |> Map.find "a" |> List.sum |> equal 30 + strs |> Map.tryFind "b" |> Option.get |> List.head |> equal 30 + [] let ``Map.tryFind with option works`` () = let xs = Map [1,Some 1.; 2,None] diff --git a/tests/Rust/tests/src/QuotationTests.fs b/tests/Rust/tests/src/QuotationTests.fs index b228ef064a..af4d2f0a3e 100644 --- a/tests/Rust/tests/src/QuotationTests.fs +++ b/tests/Rust/tests/src/QuotationTests.fs @@ -58,3 +58,18 @@ let ``Evaluate subtraction`` () = let ``Evaluate comparison`` () = let r = LeafExpressionConverter.EvaluateQuotation <@ 5 > 3 @> unbox r |> equal true + +[] +let ``Evaluate let binding`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ let x = 10 in x + 5 @> + unbox r |> equal 15 + +[] +let ``Evaluate lambda application`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ (fun a -> a * 2) 21 @> + unbox r |> equal 42 + +[] +let ``Evaluate let-bound lambda`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ let f = (fun x -> x + 1) in f 41 @> + unbox r |> equal 42