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
40 changes: 35 additions & 5 deletions src/Fable.Transforms/Rust/Fable2Rust.fs
Original file line number Diff line number Diff line change
Expand Up @@ -632,12 +632,15 @@ module TypeInfo =
// always not Rc-wrapped
| Fable.Unit
| Fable.Measure _
| Fable.MetaType
| Fable.Boolean
| Fable.Char
| Fable.Number _ -> None

// should be Rc-wrapped
// MetaType (System.Type) is erased to Any and carries a boxed reflection
// object at runtime, so it must be Lrc-wrapped like Any (a bare `dyn Any`
// is unsized and cannot appear as a value/argument).
| Fable.MetaType
| Fable.Any
| Fable.Regex
| Replacements.Util.Builtin(Replacements.Util.FSharpReference _)
Expand Down Expand Up @@ -1128,7 +1131,11 @@ module TypeInfo =
| Fable.Char -> primitiveType "char"
| Fable.Boolean -> primitiveType "bool"
| Fable.String -> transformStringType com ctx
| Fable.MetaType -> transformMetaType com ctx
// System.Type has no faithful runtime on Rust (reflection is a stub), so erase it
// to Any. This lets the NewRecord quotation deconstruction bind its type slot to the
// boxed value the quotation runtime returns; typeof-based reflection is unsupported
// regardless (see the disabled ReflectionTests).
| Fable.MetaType -> transformAnyType com ctx
| Fable.Number(kind, _) -> transformNumberType com ctx kind
| Fable.LambdaType(argType, returnType) ->
let argTypes, returnType = ([ argType ], returnType)
Expand Down Expand Up @@ -1184,6 +1191,28 @@ module TypeInfo =
// built-in types
| Replacements.Util.Builtin kind -> transformBuiltinType com ctx typ kind

// Typed quotation Expr<'T>: erase the type argument so it shares the untyped
// FSharpExpr runtime representation. This lets a typed quotation (e.g. <@ 42 @>)
// be matched directly against the Patterns active patterns, which operate on
// the untyped Expr.
| Fable.DeclaredType(entRef, _) when entRef.FullName = Types.fsharpExprGeneric ->
transformEntityType com ctx { entRef with FullName = Types.fsharpExpr } []

// UnionCaseInfo (from a NewUnionCase quotation deconstruction) is erased to the
// Rust-native FSharpUnionCaseInfo carrier so uci.Name/uci.Tag resolve to the
// quotation runtime accessors. We build the import path directly (rather than via
// transformEntityType) because that carrier type does not exist in FSharp.Core, so
// com.GetEntity on the renamed ref would fail; getEntityFullName imports the type
// from fable_library_rust purely by FullName.
| Fable.DeclaredType(entRef, _) when entRef.FullName = "Microsoft.FSharp.Reflection.UnionCaseInfo" ->
let entName =
getEntityFullName
com
ctx
{ entRef with FullName = "Microsoft.FSharp.Quotations.FSharpUnionCaseInfo" }

makeFullNamePathTy entName None

// other declared types
| Fable.DeclaredType(entRef, genArgs) -> transformEntityType com ctx entRef genArgs

Expand Down Expand Up @@ -3698,9 +3727,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
11 changes: 9 additions & 2 deletions src/Fable.Transforms/Rust/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3695,7 +3695,13 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr
fsharpType com methName r t info args
else
fsharpValue com methName r t info args
| "Microsoft.FSharp.Reflection.UnionCaseInfo"
| "Microsoft.FSharp.Reflection.UnionCaseInfo" ->
// UnionCaseInfo from a NewUnionCase quotation deconstruction is a Rust-native
// FSharpUnionCaseInfo carrier; route member access to the quotation runtime.
match thisArg, info.CompiledName with
| Some c, "get_Name" -> Helper.LibCall(com, "quotation", "unionCaseName", t, [ c ], ?loc = r) |> Some
| Some c, "get_Tag" -> Helper.LibCall(com, "quotation", "unionCaseTag", t, [ c ], ?loc = r) |> Some
| _ -> None
| "System.Reflection.PropertyInfo"
| "System.Reflection.ParameterInfo"
| "System.Reflection.MethodBase"
Expand All @@ -3716,7 +3722,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
2 changes: 2 additions & 0 deletions src/fable-library-rust/src/Fable.Library.Rust.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
<Compile Include="System.Text.fs" />
<Compile Include="System.Threading.fs" />
<Compile Include="Interfaces.fs" />
<Compile Include="QuotationTypes.fs" />
<Compile Include="Quotation.fs" />
<Compile Include="lib.fs" />
</ItemGroup>

Expand Down
287 changes: 287 additions & 0 deletions src/fable-library-rust/src/Quotation.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
// 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)

// A correctly-typed empty argument array. Rust is statically typed, so an empty
// obj[] built by the emitter won't unify with the FSharpExpr[] parameters of
// mkCall/mkNewUnion/mkNewTuple (zero-arg calls like property getters, nullary
// union cases). The emitter routes empty expr-arrays here for the Rust target.
let emptyExprArray () : FSharpExpr[] = [||]

let mkNewTuple (elements: FSharpExpr[]) : FSharpExpr = ExprNewTuple elements

let mkNewUnion (typeName: string) (tag: int) (caseName: string) (fields: FSharpExpr[]) : FSharpExpr =
ExprNewUnion(typeName, tag, caseName, fields)

let mkNewRecord (typeName: string) (fieldNames: string[]) (values: FSharpExpr[]) : FSharpExpr =
ExprNewRecord(typeName, fieldNames, values)

// --- UnionCaseInfo accessors (backing uci.Name / uci.Tag on a NewUnionCase binding) ---

let unionCaseName (u: FSharpUnionCaseInfo) : string = u.Name
let unionCaseTag (u: FSharpUnionCaseInfo) : int = u.Tag
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 "novalue" node as its instance;
// expose it as None so Patterns.Call matches F#. The tag is distinct from
// "null", which is a genuine quoted null value.
let inst =
match instance with
| ExprValue(_, "novalue") -> 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(typeName, tag, caseName, fields) ->
// Return F#'s (UnionCaseInfo * Expr list) shape. The UnionCaseInfo carrier
// backs uci.Name/uci.Tag via unionCaseName/unionCaseTag.
let uci: FSharpUnionCaseInfo =
{
Name = caseName
Tag = tag
DeclaringType = typeName
}

Some(uci, List.ofArray fields)
| _ -> None

let isNewRecord (e: FSharpExpr) =
match e with
| ExprNewRecord(typeName, _names, values) ->
// Return F#'s (Type * Expr list) shape. Rust has no faithful System.Type runtime,
// so the type slot is erased to a boxed value (the record type name). The Rust
// compiler erases the pattern's System.Type slot to Any to match (see Replacements).
Some(box typeName, 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<string>()
let acc = ResizeArray<FSharpVar>()

let rec walk (bound: Set<string>) (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<dyn Any>) 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<int> (List.item n args)
let b (n: int) : bool = unbox<bool> (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<string, obj>) (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 -> 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<bool> (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 ]
// A tuple literal evaluates to a boxed obj[] of its evaluated elements, mirroring
// PHP (which produces a plain array). TupleGet then indexes into that array; without
// this arm `eval inner` on a tuple literal would fall through to failwith and there
// would be nothing indexable, so both arms are needed together.
| ExprNewTuple els -> box [| for el in els -> eval env el |]
| ExprTupleGet(inner, index) ->
// The match-bound `index` is a borrowed &i32 on Rust while the array Index
// operator wants an owned i32; `index + 0` forces an owned i32 rvalue (the
// arithmetic dereferences the borrow) so the indexing type-checks.
let i = index + 0
(unbox<obj[]> (eval env inner)).[i]
| _ -> failwith "Cannot evaluate expression"

eval Map.empty e
Loading
Loading