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
168 changes: 159 additions & 9 deletions src/Fable.Transforms/Rust/Fable2Rust.fs
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,13 @@ module TypeInfo =
-> true
| _ -> false

// A reference-typed (non-struct) record or union. Such entities are Lrc-wrapped
// at the value level, so boxing/unboxing them to `obj` needs a smart-pointer
// coercion rather than the plain value-type box (see transformCast).
let isReferenceRecordOrUnion (com: IRustCompiler) (entRef: Fable.EntityRef) =
let ent = com.GetEntity(entRef)
(ent.IsFSharpRecord || ent.IsFSharpUnion) && not ent.IsValueType

// Checks whether the type needs a ref counted wrapper
// such as Rc<T> (or Arc<T> in a multithreaded context)
let shouldBeRefCountWrapped (com: IRustCompiler) ctx typ =
Expand All @@ -632,12 +639,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 +1138,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 +1198,52 @@ 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

// System.Reflection.MethodInfo (from a Call quotation deconstruction) is erased to
// the Rust-native FSharpMethodInfo carrier so mi.Name / mi.DeclaringType resolve to
// the quotation runtime accessors. Built directly by FullName (mirrors the
// UnionCaseInfo case) because that carrier type does not exist in FSharp.Core.
| Fable.DeclaredType(entRef, _) when entRef.FullName = "System.Reflection.MethodInfo" ->
let entName =
getEntityFullName com ctx { entRef with FullName = "Microsoft.FSharp.Quotations.FSharpMethodInfo" }

makeFullNamePathTy entName None

// System.Reflection.PropertyInfo is erased to the Rust-native FSharpPropertyInfo
// carrier, so p.Name / p.GetValue and FSharpValue.GetRecordField resolve to the
// reflection runtime. One carrier serves both reflection (GetRecordFields) and
// quotations (PropertyGet's propInfo), as in .NET. Built directly by FullName
// (mirrors the MethodInfo case) because that carrier does not exist in FSharp.Core.
| Fable.DeclaredType(entRef, _) when entRef.FullName = "System.Reflection.PropertyInfo" ->
let entName =
getEntityFullName
com
ctx
{ entRef with FullName = "Microsoft.FSharp.Quotations.FSharpPropertyInfo" }

makeFullNamePathTy entName None

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

Expand Down Expand Up @@ -1559,6 +1619,17 @@ module Util =
// unboxing value types or wrapped types
| Fable.Any, t when isValueType com t || isWrappedType com t -> expr |> unboxValue com ctx t

// boxing a reference-typed record/union (Lrc-wrapped) to obj: coerce the
// smart pointer to `dyn Any` so its concrete pointee stays the struct.
| Fable.DeclaredType(entRef, _), Fable.Any when isReferenceRecordOrUnion com entRef ->
[ expr ] |> makeLibCall com ctx None "Native" "box_lrc"

// unboxing obj back to a reference-typed record/union: downcast + re-wrap.
| Fable.Any, Fable.DeclaredType(entRef, genArgs) when isReferenceRecordOrUnion com entRef ->
let rawTy = transformEntityType com ctx entRef genArgs
let genArgsOpt = [ rawTy ] |> mkTypesGenericArgs
[ expr ] |> makeLibCall com ctx genArgsOpt "Native" "unbox_lrc"

// casts to generic param
| _, Fable.GenericParam(name, _isMeasure, _constraints) -> makeCall (name :: "from" :: []) None [ expr ] // e.g. T::from(value)

Expand Down Expand Up @@ -1849,9 +1920,13 @@ module Util =
// bindings we must emit a valid placeholder value instead (it is only there to
// satisfy definite-assignment and gets overwritten before it is ever read).
match typ with
| Fable.Any ->
| Fable.Any
| Fable.MetaType ->
// `dyn Any` is unsized, so `Native::null` (needs a sized `NullableRef`) does
// not apply; use a valid boxed-unit placeholder of type `LrcPtr<dyn Any>`.
// `MetaType` (System.Type) is now also represented as `LrcPtr<dyn Any>`,
// so it must take this path too (the `_` branch would emit a null of an
// unsized `dyn Any`, which does not compile).
makeLibCall com ctx None "Native" "getZeroObj" []
| _ ->
// Only concrete (sized) `Lrc`-wrapped reference types can use the null-ref
Expand Down Expand Up @@ -2060,10 +2135,84 @@ module Util =
let fmt = makeFormatString parts
makeFormatExpr com ctx fmt values

// Builds a rich reflection value for `typeof<Record>`. The emitted expression
// registers the record's field metadata + constructor/getter closures keyed by
// its concrete TypeId and returns a boxed RecordTypeInfo (the runtime value that
// a `System.Type` holds on the Rust target). This backs FSharpValue.MakeRecord,
// FSharpValue.GetRecordFields/GetRecordField and FSharpType.GetRecordFields.
let makeRecordTypeInfo (com: IRustCompiler) ctx r (entRef: Fable.EntityRef) genArgs (typ: Fable.Type) : Rust.Expr =
let ent = com.GetEntity(entRef)
let idents = getEntityFieldsAsIdents com ent
let objType = Fable.Any
let arrObjType = Fable.Array(objType, Fable.MutableArray)

// tid = Reflection::type_id::<Foo>() (concrete, unwrapped struct type)
let rawTy = transformEntityType com ctx entRef genArgs
let tidGenArgs = [ rawTy ] |> mkTypesGenericArgs
let tidExpr = makeLibCall com ctx tidGenArgs "Reflection" "type_id" []

// name
let nameExpr = makeStrConst ent.FullName |> transformExpr com ctx

// field names: string[]
let fieldNamesExpr =
let names = idents |> List.map (fun ident -> makeStrConst ident.Name)

Fable.Value(Fable.NewArray(Fable.ArrayValues names, Fable.String, Fable.MutableArray), None)
|> transformExpr com ctx

// make: |vs: obj[]| box (Foo { X = unbox vs.[0]; Y = unbox vs.[1] })
let makeExpr =
let vsIdent = makeTypedIdent arrObjType "vs"

let ctorValues =
idents
|> List.mapi (fun i ident ->
let elem =
Fable.Get(Fable.IdentExpr vsIdent, Fable.ExprGet(makeIntConst i), objType, None)

Fable.TypeCast(elem, ident.Type) // unbox to field type
)

let recordExpr = Fable.Value(Fable.NewRecord(ctorValues, entRef, genArgs), None)
let body = Fable.TypeCast(recordExpr, objType) // box the record

Fable.Delegate([ vsIdent ], body, None, Fable.Tags.empty)
|> transformExpr com ctx

// getters: (obj -> obj)[], each |o| box ((unbox<Foo> o).Field)
let gettersExpr =
let getterFnType = Fable.DelegateType([ objType ], objType)

let getters =
idents
|> List.map (fun ident ->
let oIdent = makeTypedIdent objType "o"
let recExpr = Fable.TypeCast(Fable.IdentExpr oIdent, typ) // unbox obj -> Foo
let fieldInfo = Fable.FieldInfo.Create(ident.Name, ident.Type)
let fieldGet = Fable.Get(recExpr, fieldInfo, ident.Type, None)
let body = Fable.TypeCast(fieldGet, objType) // box the field value
Fable.Delegate([ oIdent ], body, None, Fable.Tags.empty)
)

Fable.Value(Fable.NewArray(Fable.ArrayValues getters, getterFnType, Fable.MutableArray), None)
|> transformExpr com ctx

makeLibCall com ctx None "Reflection" "recordType" [ tidExpr; nameExpr; fieldNamesExpr; makeExpr; gettersExpr ]

let makeTypeInfo (com: IRustCompiler) ctx r (typ: Fable.Type) : Rust.Expr =
let importName = getLibraryImportName com ctx "Reflection" "TypeId"
let genArgsOpt = transformGenArgs com ctx [ typ ]
makeFullNamePathExpr importName genArgsOpt
match typ with
| Fable.DeclaredType(entRef, genArgs) when (com.GetEntity(entRef)).IsFSharpRecord ->
makeRecordTypeInfo com ctx r entRef genArgs typ
| _ ->
// Non-record `System.Type` value: carry the concrete `TypeId`, boxed as
// `LrcPtr<dyn Any>` so it flows through the same `obj` slot as record type
// infos. Emitting a bare `Reflection_::TypeId::<T>` path is not a valid
// value expression, and comparing the boxed carrier's runtime `type_id()`
// (as `typeEquals` did) would make all non-record types compare equal.
let genArgsOpt = transformGenArgs com ctx [ typ ]
let tidExpr = makeLibCall com ctx genArgsOpt "Reflection" "type_id" []
makeLibCall com ctx None "Native" "box_" [ tidExpr ]

let transformValue (com: IRustCompiler) (ctx: Context) r value : Rust.Expr =
let unimplemented () =
Expand Down Expand Up @@ -3698,9 +3847,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
46 changes: 37 additions & 9 deletions src/Fable.Transforms/Rust/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -490,8 +490,9 @@ let equals (com: ICompiler) ctx r (left: Expr) (right: Expr) =
| Array _ -> Helper.LibCall(com, "Array", "equals", t, [ left; right ], ?loc = r)
| List _ -> Helper.LibCall(com, "List", "equals", t, [ left; right ], ?loc = r)
| IEnumerable -> Helper.LibCall(com, "Seq", "equals", t, [ left; right ], ?loc = r)
// | MetaType ->
// Helper.LibCall(com, "Reflection", "equals", t, [left; right], ?loc=r)
// System.Type is erased to a boxed reflection object (dyn Any, no PartialEq),
// so type-identity equality is compared on the carried TypeId at runtime.
| MetaType -> Helper.LibCall(com, "Reflection", "typeEquals", t, [ left; right ], ?loc = r)
| HasReferenceEquality com _ -> referenceEquals com ctx r left right
| Nullable _ ->
// transforms null checks into option tests
Expand Down Expand Up @@ -1298,10 +1299,13 @@ let objects (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt
| "GetHashCode", Some arg, _ -> objectHash com ctx r arg |> Some
| "GetType", Some arg, _ ->
if arg.Type = Any then
"Types can only be resolved at compile time. At runtime this will be same as `typeof<obj>`"
|> addWarning com ctx.InlinePath r

makeTypeInfo r arg.Type |> Some
// Dynamic type of a boxed value: resolve via the runtime reflection
// registry (populated by typeof<T>). Returns the concrete type's info
// when registered, else the value as an opaque placeholder.
Helper.LibCall(com, "Reflection", "getTypeFromObj", t, [ arg ], ?loc = r)
|> Some
else
makeTypeInfo r arg.Type |> Some
| _ -> None

let valueTypes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) =
Expand Down Expand Up @@ -3695,13 +3699,34 @@ 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"
| "System.Reflection.MethodInfo"
| "System.Reflection.MemberInfo" ->
// Name/DeclaringType are inherited from MemberInfo, so `mi.Name` / `mi.DeclaringType`
// on a Call quotation binding arrive here even though the binding's type is MethodInfo.
// Detect that carrier via the thisArg's Fable type and route to the quotation runtime:
// get_Name -> a dedicated accessor (not the generic reflection name<T>()); DeclaringType
// -> the declaring-type fullname boxed as System.Type, so mi.DeclaringType.FullName works.
let isMethodInfoCarrier (c: Expr) =
match c.Type with
| DeclaredType(e, _) -> e.FullName = "System.Reflection.MethodInfo"
| _ -> false

match thisArg, info.CompiledName with
| Some c, "get_Name" when isMethodInfoCarrier c ->
Helper.LibCall(com, "quotation", "methodName", t, [ c ], ?loc = r) |> Some
| Some c, "get_DeclaringType" when isMethodInfoCarrier c ->
Helper.LibCall(com, "quotation", "methodDeclaringType", t, [ c ], ?loc = r)
|> Some
| Some c, "get_Tag" -> makeStrConst "tag" |> getExpr r t c |> Some
| Some c, "get_ReturnType" -> makeStrConst "returnType" |> getExpr r t c |> Some
| Some c, "GetParameters" -> makeStrConst "parameters" |> getExpr r t c |> Some
Expand All @@ -3714,9 +3739,12 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr
match c with
| Value(TypeInfo(exprType, _), loc) ->
getTypeName com ctx loc exprType |> StringConstant |> makeValue r |> Some
| c -> Helper.LibCall(com, "Reflection", "name", t, [ c ], ?loc = r) |> Some
// Runtime PropertyInfo (e.g. a record field info): read its carried name.
// Distinct from the generic type-name helper `name<T>()` to avoid a clash.
| c -> Helper.LibCall(com, "Reflection", "propertyName", 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
11 changes: 7 additions & 4 deletions src/fable-library-rust/src/Async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,11 @@ pub mod Monitor_ {
LOCKS.get_or_init(|| RwLock::new(HashSet::new()))
}

pub fn enter<T>(o: LrcPtr<T>) {
let p = Arc::<T>::as_ptr(&o) as usize;
// `?Sized` so a boxed `obj` (LrcPtr<dyn Any>) can be locked on: casting to
// `*const ()` first discards the vtable of a fat pointer, leaving the data
// address, which is what identifies the lock.
pub fn enter<T: ?Sized>(o: LrcPtr<T>) {
let p = Arc::<T>::as_ptr(&o) as *const () as usize;
loop {
let otherHasLock = try_init_and_get_locks().read().unwrap().get(&p).is_some();
if otherHasLock {
Expand All @@ -178,8 +181,8 @@ pub mod Monitor_ {
}
}

pub fn exit<T>(o: LrcPtr<T>) {
let p = Arc::<T>::as_ptr(&o) as usize;
pub fn exit<T: ?Sized>(o: LrcPtr<T>) {
let p = Arc::<T>::as_ptr(&o) as *const () as usize;
let hasRemoved = try_init_and_get_locks().write().unwrap().remove(&p);
if (!hasRemoved) {
panic!("Not removed {}", p)
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
18 changes: 18 additions & 0 deletions src/fable-library-rust/src/Native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,24 @@ pub mod Native_ {
try_downcast::<_, T>(&o).unwrap().clone()
}

// Boxing/unboxing for reference-typed (Lrc-wrapped) declared types such as
// records and unions. Unlike `box_`, which wraps the value in a fresh Lrc,
// these coerce the *same* smart pointer to `dyn Any`, so the boxed value's
// concrete pointee is the record/union struct itself. That lets a plain
// downcast recover it, and lets `TypeId::of::<T>()` (computed at the typeof
// site) match the pointee's runtime type id for value-based reflection.
// NOTE: relies on `LrcPtr = Rc/Arc` CoerceUnsized; the `lrc_ptr` feature
// (custom smart pointer without CoerceUnsized) is not supported here.
#[cfg(not(feature = "lrc_ptr"))]
pub fn box_lrc<T: 'static>(o: LrcPtr<T>) -> LrcPtr<dyn Any> {
o
}

#[cfg(not(feature = "lrc_ptr"))]
pub fn unbox_lrc<T: Clone + 'static>(o: LrcPtr<dyn Any>) -> LrcPtr<T> {
LrcPtr::new((*o).downcast_ref::<T>().expect("Invalid unbox").clone())
}

pub fn ofObj<T: Clone + NullableRef + 'static>(value: T) -> Option<T> {
if is_null(&value) {
None::<T>
Expand Down
Loading
Loading