Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/Fable.Transforms/FSharp2Fable.Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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) =
Expand All @@ -555,6 +559,7 @@ type Context =
InlinePath = []
CaptureBaseConsCall = None
Witnesses = []
CapturingQuotation = false
}

type IFableCompiler =
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/Fable.Transforms/FSharp2Fable.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion src/Fable.Transforms/Php/Fable2Php.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
199 changes: 178 additions & 21 deletions src/Fable.Transforms/QuotationEmitter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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" ])

Expand All @@ -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
Expand All @@ -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" ])
Expand All @@ -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" ])
Expand Down Expand Up @@ -295,30 +413,69 @@ 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
| 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" ])
| 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"

Expand Down
7 changes: 4 additions & 3 deletions src/Fable.Transforms/Rust/Fable2Rust.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/Fable.Transforms/Rust/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type list>) genArgs args =
match ent.FullName with
Expand Down
Loading
Loading