Skip to content
Merged
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
74 changes: 55 additions & 19 deletions src/Fable.Transforms/Rust/Fable2Rust.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,10 @@ module Util =
| t1, t2 when t1 = t2 -> expr // no cast needed if types are the same
| Fable.Number _, Fable.Number _ -> expr |> mkCastExpr ty
| Fable.Char, Fable.Number(UInt32, Fable.NumberInfo.Empty) -> expr |> mkCastExpr ty
// bool -> integer (Rust allows `b as iN`/`b as uN`, yields 0/1)
| Fable.Boolean,
Fable.Number((Int8 | UInt8 | Int16 | UInt16 | Int32 | UInt32 | Int64 | UInt64 | Int128 | UInt128 | NativeInt | UNativeInt),
_) -> expr |> mkCastExpr ty
| Fable.Tuple(ga1, false), Fable.Tuple(ga2, true) when ga1 = ga2 -> expr |> makeAsRef |> makeClone //.ToValueTuple()
| Fable.Tuple(ga1, true), Fable.Tuple(ga2, false) when ga1 = ga2 -> expr |> makeLrcPtrValue com ctx //.ToTuple()

Expand Down Expand Up @@ -1840,8 +1844,33 @@ module Util =
makeLibCall com ctx genArgsOpt "Native" "null" []

let makeInit com ctx (typ: Fable.Type) =
let genArgsOpt = transformGenArgs com ctx [ typ ]
makeLibCall com ctx genArgsOpt "Native" "getZero" []
// `Native::getZero` uses `core::mem::zeroed`, which panics at runtime when the
// type is a reference type (Rc/Arc/Box pointer). For such pre-declared match
// 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 ->
// `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>`.
makeLibCall com ctx None "Native" "getZeroObj" []
| _ ->
// Only concrete (sized) `Lrc`-wrapped reference types can use the null-ref
// placeholder. Unsized refs (interfaces, enumerators) and Arc/Box-wrapped
// types keep `getZero` (they don't reach this path in practice).
let isConcreteLrcRef =
match shouldBeRefCountWrapped com ctx typ with
| Some Lrc ->
match typ with
| Fable.DeclaredType(entRef, _) -> not (com.GetEntity(entRef)).IsInterface
| Replacements.Util.IsEnumerator _ -> false
| _ -> true
| _ -> false

if isConcreteLrcRef then
makeNull com ctx typ
else
let genArgsOpt = transformGenArgs com ctx [ typ ]
makeLibCall com ctx genArgsOpt "Native" "getZero" []

let makeDefault com ctx (typ: Fable.Type) =
let genArgsOpt = transformGenArgs com ctx [ typ ]
Expand Down Expand Up @@ -2370,6 +2399,12 @@ module Util =
| UnaryOperator.UnaryNotBitwise -> mkNotExpr expr // ?loc=range)
| UnaryOperator.UnaryAddressOf -> expr |> mkAddrOfExpr

| Fable.Binary(BinaryOperator.BinaryExponent, leftExpr, rightExpr) ->
// Rust has no `**` operator; F# (**) is floating-point exponentiation.
let left = transformLeaveContext com ctx None leftExpr |> maybeAddParens leftExpr
let right = transformLeaveContext com ctx None rightExpr |> maybeAddParens rightExpr
mkMethodCallExpr "powf" None left [ right ]

| Fable.Binary(op, leftExpr, rightExpr) ->
let kind =
match op with
Expand All @@ -2387,7 +2422,7 @@ module Util =
| BinaryOperator.BinaryMultiply -> Rust.BinOpKind.Mul
| BinaryOperator.BinaryDivide -> Rust.BinOpKind.Div
| BinaryOperator.BinaryModulus -> Rust.BinOpKind.Rem
| BinaryOperator.BinaryExponent -> failwithf "BinaryExponent not supported. TODO: implement with pow."
| BinaryOperator.BinaryExponent -> failwith "unreachable: BinaryExponent handled above"
| BinaryOperator.BinaryOrBitwise -> Rust.BinOpKind.BitOr
| BinaryOperator.BinaryXorBitwise -> Rust.BinOpKind.BitXor
| BinaryOperator.BinaryAndBitwise -> Rust.BinOpKind.BitAnd
Expand Down Expand Up @@ -3360,12 +3395,15 @@ module Util =
function
| Fable.Operation(Fable.Binary(BinaryEqual, left, right), _, _, _) ->
match left, right with
| _, Fable.Value((Fable.CharConstant _ | Fable.StringConstant _ | Fable.NumberConstant _), _) ->
Some(left, right)
| Fable.Value((Fable.CharConstant _ | Fable.StringConstant _ | Fable.NumberConstant _), _), _ ->
Some(right, left)
// NOTE: StringConstant is intentionally excluded — Rust match patterns can't
// represent a fable-library string (it lowers to a `string("...")` call), so
// string matches must use the if/else decision-tree path with real `==`.
| _, Fable.Value((Fable.CharConstant _ | Fable.NumberConstant _), _) -> Some(left, right)
| Fable.Value((Fable.CharConstant _ | Fable.NumberConstant _), _), _ -> Some(right, left)
| _ -> None
| Fable.Test(expr, Fable.OptionTest isSome, r) ->
// Only a plain-ident scrutinee is convertible: transformSwitch can't recover the
// `Option` type from anything else, and would emit bogus `0_i32` patterns.
| Fable.Test(Fable.IdentExpr _ as expr, Fable.OptionTest isSome, r) ->
let evalExpr =
Fable.Get(expr, Fable.UnionTag, Fable.Number(Int32, Fable.NumberInfo.Empty), r)

Expand Down Expand Up @@ -3545,18 +3583,16 @@ module Util =
let ctx = { ctx with DecisionTargets = targets }
transformSwitch com ctx (targetId |> Fable.IdentExpr) cases (Some defaultCase)

match tryTransformAsSwitch treeExpr with
| Some(evalExpr, cases, defaultCase) ->
// let cases = groupSwitchCases typ cases defaultCase
let switch1 = transformSwitch com ctx evalExpr cases (Some defaultCase)

varDecls @ [ switch1 |> mkSemiStmt ] @ [ switch2 |> mkExprStmt ]
|> mkStmtBlockExpr
| None ->
let decisionTree = com.TransformExpr(ctx, treeExpr)
// NOTE: switch1 must ASSIGN matchResult (+ bound idents) via DecisionTreeSuccess,
// not execute the target bodies directly. Transforming the tree as a switch here
// (via tryTransformAsSwitch) would run the target bodies with still-unassigned
// bindings and leave matchResult = 0, so switch2 would always dispatch to target 0.
// Instead, transform the tree directly so DecisionTreeSuccess nodes become
// assignments (via transformDecisionTreeSuccess), then let switch2 dispatch.
let decisionTree = com.TransformExpr(ctx, treeExpr)

varDecls @ [ decisionTree |> mkSemiStmt ] @ [ switch2 |> mkExprStmt ]
|> mkStmtBlockExpr
varDecls @ [ decisionTree |> mkSemiStmt ] @ [ switch2 |> mkExprStmt ]
|> mkStmtBlockExpr

let transformDecisionTree (com: IRustCompiler) ctx targets (treeExpr: Fable.Expr) : Rust.Expr =
let treeExpr = simplifyDecisionTree treeExpr
Expand Down
13 changes: 12 additions & 1 deletion src/Fable.Transforms/Rust/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ let convertTo com (ctx: Context) r t (args: Expr list) =
match t with
| Boolean ->
match sourceType with
| Boolean -> args.Head
| Number(Decimal, _) -> Helper.LibCall(com, "Decimal", "toBoolean", t, args, ?loc = r)
| Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "toBoolean", t, args, ?loc = r)
| Number(_kind, _) -> Helper.LibCall(com, "Convert", "toBoolean", t, args, ?loc = r)
Expand Down Expand Up @@ -239,6 +240,10 @@ let convertTo com (ctx: Context) r t (args: Expr list) =

| Number(kind, _) ->
match sourceType with
| Boolean ->
// .NET Convert.ToXxx(bool) yields 0/1; route via i32 so float targets also work
let code = TypeCast(args.Head, Int32.Number)
TypeCast(code, t)
| Char ->
let code = TypeCast(args.Head, UInt32.Number)
TypeCast(code, t)
Expand Down Expand Up @@ -1640,7 +1645,10 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt
Helper.LibCall(com, "String", "splitChars", t, [ c; arg1; arg2; arg3 ], ?loc = r)
|> Some

// TODO: handle arrays of string separators with more than one element
// Remaining gap: the count-bearing string[] overload Split(string[], int, options)
// for multi-element / non-literal arrays. splitStrings has no count parameter, and a
// correct global left-to-right count across multiple separators is non-trivial.
// The common Split(string[], options) form is handled above via splitStrings.
| _ -> None
| "StartsWith", Some c, _ ->
match args with
Expand Down Expand Up @@ -2240,6 +2248,9 @@ let decimals (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg:
| ("get_Zero" | "get_One" | "get_MinusOne" | "get_MinValue" | "get_MaxValue"), _ ->
Helper.LibValue(com, "Decimal", Naming.removeGetSetPrefix i.CompiledName, t)
|> Some
| ("IsInteger" | "IsEvenInteger" | "IsOddInteger" | "IsCanonical" | "IsNegative" | "IsPositive"), _ ->
Helper.LibCall(com, "Decimal", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r)
|> Some
| "get_Scale", [] ->
match thisArg with
| Some c ->
Expand Down
18 changes: 14 additions & 4 deletions src/fable-library-rust/src/Char.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,13 @@ pub mod Char_ {
if (IsAscii(c)) {
c.to_ascii_uppercase()
} else {
//TODO: when Uppercase is more than one character
c.to_uppercase().next().unwrap()
// .NET Char.ToUpper never expands: when the full-case mapping is not a
// single char (e.g. 'ß' -> "SS"), the original char is returned unchanged.
let mut it = c.to_uppercase();
match (it.next(), it.next()) {
(Some(u), None) => u,
_ => c,
}
}
}

Expand All @@ -422,8 +427,13 @@ pub mod Char_ {
if (IsAscii(c)) {
c.to_ascii_lowercase()
} else {
//TODO: when Lowercase is more than one character
c.to_lowercase().next().unwrap()
// .NET Char.ToLower never expands: when the full-case mapping is not a
// single char, the original char is returned unchanged.
let mut it = c.to_lowercase();
match (it.next(), it.next()) {
(Some(l), None) => l,
_ => c,
}
}
}

Expand Down
10 changes: 6 additions & 4 deletions src/fable-library-rust/src/Decimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ pub mod Decimal_ {

pub fn isNegative(x: decimal) -> bool { x.is_sign_negative() }
pub fn isPositive(x: decimal) -> bool { x.is_sign_positive() }
// pub fn isInteger(x: decimal) -> bool { false } //TODO:
// pub fn isEvenInteger(x: decimal) -> bool { false } //TODO:
// pub fn isOddInteger(x: decimal) -> bool { false } //TODO:
// pub fn isCanonical(x: decimal) -> bool { false } //TODO:
pub fn isInteger(x: decimal) -> bool { x.fract() == Decimal::ZERO }
pub fn isEvenInteger(x: decimal) -> bool { isInteger(x) && (x % Decimal::from(2)) == Decimal::ZERO }
pub fn isOddInteger(x: decimal) -> bool { isInteger(x) && (x % Decimal::from(2)) != Decimal::ZERO }
// Canonical means the stored scale is already minimal (no representable trailing
// zero digits), e.g. 1.0M and 1.20M are non-canonical, but 1M and 3.14M are.
pub fn isCanonical(x: decimal) -> bool { x.scale() == x.normalize().scale() }

pub fn toInt8(x: decimal) -> i8 { x.to_i8().unwrap() }
pub fn toUInt8(x: decimal) -> u8 { x.to_u8().unwrap() }
Expand Down
37 changes: 25 additions & 12 deletions src/fable-library-rust/src/Map.fs
Original file line number Diff line number Diff line change
Expand Up @@ -157,21 +157,34 @@ let rec tryGetValue k (v: byref<'V>) (m: Map<'K, 'V>) =
let throwKeyNotFound () = failwith SR.keyNotFound

// [<MethodImpl(MethodImplOptions.NoInlining)>]
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

Expand Down
7 changes: 7 additions & 0 deletions src/fable-library-rust/src/Native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,13 @@ pub mod Native_ {
unsafe { core::mem::zeroed() } // will panic on Rc/Arc/Box
}

// A valid placeholder value of type `LrcPtr<dyn Any>`. Used to pre-declare
// reference-typed (`obj`) match bindings, as `getZero`/`mem::zeroed` panics on
// the `Rc` fat pointer and `null` is not available for the unsized `dyn Any`.
pub fn getZeroObj() -> LrcPtr<dyn Any> {
box_(())
}

pub fn defaultOf<T: Default>() -> T {
Default::default()
}
Expand Down
2 changes: 1 addition & 1 deletion src/fable-library-rust/src/Random.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ pub mod Random_ {
}

pub fn nextDouble(&self) -> f64 {
unimplemented!("MSG")
unimplemented!("{}", MSG)
}

pub fn nextBytes(&self, buffer: Array<u8>) {
Expand Down
35 changes: 33 additions & 2 deletions src/quicktest-rust/src/main.fs
Original file line number Diff line number Diff line change
@@ -1,8 +1,39 @@
open System

let user = "World"
type RefC(v: int) =
member _.V = v

let twoSwitchRef (x: RefC option) (b: bool) =
match x with
| Some c when b -> c.V + 1
| Some c -> c.V
| None -> -1

type RefU =
| RA of RefC
| RB of RefC

let orPatRef (u: RefU) =
match u with
| RA c
| RB c -> c.V

type ObjU =
| OA of obj
| OB of obj

let orPatObj (u: ObjU) : obj =
match u with
| OA o
| OB o -> o

[<EntryPoint>]
let main argv =
Console.WriteLine("Hello {0}!", user)
Console.WriteLine(twoSwitchRef (Some(RefC 5)) true)
Console.WriteLine(twoSwitchRef (Some(RefC 5)) false)
Console.WriteLine(twoSwitchRef None false)
Console.WriteLine(orPatRef (RA(RefC 5)))
Console.WriteLine(orPatRef (RB(RefC 7)))
Console.WriteLine((orPatObj (OA(box 9))) :? int)
Console.WriteLine((orPatObj (OB(box 11))) :? string)
0
24 changes: 24 additions & 0 deletions tests/Rust/tests/src/ArithmeticTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,30 @@ let ``Decimal literals can be generated`` () =
79228162514264337593543950335M |> equal Decimal.MaxValue
-79228162514264337593543950335M |> equal Decimal.MinValue

[<Fact>]
let ``Decimal integer predicates work`` () =
System.Decimal.IsInteger(3M) |> equal true
System.Decimal.IsInteger(3.5M) |> equal false
System.Decimal.IsEvenInteger(4M) |> equal true
System.Decimal.IsEvenInteger(3M) |> equal false
System.Decimal.IsOddInteger(3M) |> equal true
System.Decimal.IsOddInteger(4M) |> equal false
System.Decimal.IsOddInteger(-3M) |> equal true
System.Decimal.IsOddInteger(3.5M) |> equal false

[<Fact>]
let ``Decimal.IsCanonical works`` () =
// Canonical means the value's stored scale is already minimal (no representable
// trailing zero digits); non-canonical values have a scale larger than needed.
System.Decimal.IsCanonical(1M) |> equal true
System.Decimal.IsCanonical(100M) |> equal true
System.Decimal.IsCanonical(3.14M) |> equal true
System.Decimal.IsCanonical(-5M) |> equal true
System.Decimal.IsCanonical(0M) |> equal true
System.Decimal.IsCanonical(1.0M) |> equal false
System.Decimal.IsCanonical(1.20M) |> equal false
System.Decimal.IsCanonical(0.0M) |> equal false

[<Fact>]
let ``Decimal.ToString works`` () =
string 001.23456M |> equal "1.23456"
Expand Down
11 changes: 11 additions & 0 deletions tests/Rust/tests/src/CharTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ let ``Char.ToUpper works`` () =
let ``Char.ToLower works`` () =
Char.ToLower('B') |> equal 'b'

[<Fact>]
let ``Char.ToUpper with 1-to-many mapping returns original char`` () =
// .NET Char.ToUpper never expands: 'ß' upper-maps to "SS", so the char is unchanged.
Char.ToUpper('ß') |> equal 'ß'
// A non-ASCII char with a single-char mapping still converts.
Char.ToUpper('é') |> equal 'É'

[<Fact>]
let ``Char.ToLower with non-ASCII single mapping works`` () =
Char.ToLower('É') |> equal 'é'

[<Fact>]
let ``Char.ToUpperInvariant works`` () =
Char.ToUpperInvariant('b') |> equal 'B'
Expand Down
Loading
Loading