From 68ab3835c7069e6ab18c8d789db61357ef843633 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Tue, 14 Jul 2026 21:11:09 +0100 Subject: [PATCH 1/3] feat(rust): codegen and runtime improvements --- src/Fable.Transforms/Rust/Fable2Rust.fs | 82 +++++++++++++++------ src/Fable.Transforms/Rust/Replacements.fs | 13 +++- src/fable-library-rust/src/Char.rs | 18 ++++- src/fable-library-rust/src/Decimal.rs | 8 +- src/fable-library-rust/src/Map.fs | 37 +++++++--- src/fable-library-rust/src/Native.rs | 7 ++ src/fable-library-rust/src/Random.rs | 2 +- tests/Rust/tests/src/ArithmeticTests.fs | 11 +++ tests/Rust/tests/src/CharTests.fs | 11 +++ tests/Rust/tests/src/ControlFlowTests.fs | 89 +++++++++++++++++++++++ tests/Rust/tests/src/ConvertTests.fs | 20 +++++ tests/Rust/tests/src/MapTests.fs | 11 +++ tests/Rust/tests/src/StringTests.fs | 6 ++ 13 files changed, 269 insertions(+), 46 deletions(-) diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 58aeba6934..78dc568e81 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -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() @@ -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`. + 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 ] @@ -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 @@ -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 @@ -3360,17 +3395,18 @@ 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) -> - let evalExpr = - Fable.Get(expr, Fable.UnionTag, Fable.Number(Int32, Fable.NumberInfo.Empty), r) - - let right = makeIntConst (makeTest isSome 0 1) - Some(evalExpr, right) + // NOTE: `OptionTest` is intentionally NOT converted into a UnionTag switch. + // When the option scrutinee is not a plain ident (e.g. a field access or an + // active-pattern result), `transformSwitch` cannot recover the `Option` type + // and emits integer-literal patterns (`0_i32`) against a native `Option`, + // which is a type error. Letting these fall through to the if/else decision + // tree path (is_some/is_none) compiles and runs correctly. | Fable.Test(expr, Fable.UnionCaseTest tag, r) -> let evalExpr = Fable.Get(expr, Fable.UnionTag, Fable.Number(Int32, Fable.NumberInfo.Empty), r) @@ -3545,18 +3581,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 diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index fba43b41c7..0af218b01a 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -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) @@ -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) @@ -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 @@ -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 -> diff --git a/src/fable-library-rust/src/Char.rs b/src/fable-library-rust/src/Char.rs index 580b4ce4a3..de0e15da59 100644 --- a/src/fable-library-rust/src/Char.rs +++ b/src/fable-library-rust/src/Char.rs @@ -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, + } } } @@ -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, + } } } diff --git a/src/fable-library-rust/src/Decimal.rs b/src/fable-library-rust/src/Decimal.rs index d2a0a1ece9..84d517018c 100644 --- a/src/fable-library-rust/src/Decimal.rs +++ b/src/fable-library-rust/src/Decimal.rs @@ -60,10 +60,10 @@ 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 } + pub fn isCanonical(x: decimal) -> bool { true } pub fn toInt8(x: decimal) -> i8 { x.to_i8().unwrap() } pub fn toUInt8(x: decimal) -> u8 { x.to_u8().unwrap() } diff --git a/src/fable-library-rust/src/Map.fs b/src/fable-library-rust/src/Map.fs index fbf1858960..e7e09ed839 100644 --- a/src/fable-library-rust/src/Map.fs +++ b/src/fable-library-rust/src/Map.fs @@ -157,21 +157,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/src/fable-library-rust/src/Native.rs b/src/fable-library-rust/src/Native.rs index 12ff5aa9ea..42a249daf3 100644 --- a/src/fable-library-rust/src/Native.rs +++ b/src/fable-library-rust/src/Native.rs @@ -314,6 +314,13 @@ pub mod Native_ { unsafe { core::mem::zeroed() } // will panic on Rc/Arc/Box } + // A valid placeholder value of type `LrcPtr`. 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 { + box_(()) + } + pub fn defaultOf() -> T { Default::default() } diff --git a/src/fable-library-rust/src/Random.rs b/src/fable-library-rust/src/Random.rs index 8c5f39bb59..2d38180625 100644 --- a/src/fable-library-rust/src/Random.rs +++ b/src/fable-library-rust/src/Random.rs @@ -108,7 +108,7 @@ pub mod Random_ { } pub fn nextDouble(&self) -> f64 { - unimplemented!("MSG") + unimplemented!("{}", MSG) } pub fn nextBytes(&self, buffer: Array) { diff --git a/tests/Rust/tests/src/ArithmeticTests.fs b/tests/Rust/tests/src/ArithmeticTests.fs index 1067e99a29..d09786fd51 100644 --- a/tests/Rust/tests/src/ArithmeticTests.fs +++ b/tests/Rust/tests/src/ArithmeticTests.fs @@ -173,6 +173,17 @@ let ``Decimal literals can be generated`` () = 79228162514264337593543950335M |> equal Decimal.MaxValue -79228162514264337593543950335M |> equal Decimal.MinValue +[] +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 + [] let ``Decimal.ToString works`` () = string 001.23456M |> equal "1.23456" diff --git a/tests/Rust/tests/src/CharTests.fs b/tests/Rust/tests/src/CharTests.fs index 678547e930..b8de96f1a0 100644 --- a/tests/Rust/tests/src/CharTests.fs +++ b/tests/Rust/tests/src/CharTests.fs @@ -12,6 +12,17 @@ let ``Char.ToUpper works`` () = let ``Char.ToLower works`` () = Char.ToLower('B') |> equal 'b' +[] +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 'É' + +[] +let ``Char.ToLower with non-ASCII single mapping works`` () = + Char.ToLower('É') |> equal 'é' + [] let ``Char.ToUpperInvariant works`` () = Char.ToUpperInvariant('b') |> equal 'B' diff --git a/tests/Rust/tests/src/ControlFlowTests.fs b/tests/Rust/tests/src/ControlFlowTests.fs index b53381e13b..f906be25d4 100644 --- a/tests/Rust/tests/src/ControlFlowTests.fs +++ b/tests/Rust/tests/src/ControlFlowTests.fs @@ -66,3 +66,92 @@ let ``while loop works`` () = i <- i + 1 total <- total + i total |> equal 55 + +// --- Regression tests for the Rust decision-tree/switch backend --- + +type private OptRec = { Field: int option } + +let private (|Positive|_|) x = + if x > 0 then Some() else None + +// Bug 2: bare-wildcard option match where the scrutinee is NOT a plain ident +// (field access / active-pattern result) used to emit `0_i32` int patterns +// against a native Option, a type error. +let private optFieldNoBind (r: OptRec) = + match r.Field with + | Some _ -> "some" + | None -> "none" + +let private optApNoBind n = + match n with + | Positive _ -> "pos" + | _ -> "other" + +[] +let ``option wildcard match on field access works`` () = + optFieldNoBind { Field = Some 3 } |> equal "some" + optFieldNoBind { Field = None } |> equal "none" + +[] +let ``option wildcard match on active pattern works`` () = + optApNoBind 5 |> equal "pos" + optApNoBind -1 |> equal "other" + +let private optFieldBind (r: OptRec) = + match r.Field with + | Some v -> v * 10 + | None -> 0 + +[] +let ``option binding match on field access works`` () = + optFieldBind { Field = Some 4 } |> equal 40 + optFieldBind { Field = None } |> equal 0 + +// Bug 1: reference-typed bindings reaching the two-switch decision-tree path +// used to be pre-declared via getZero (mem::zeroed), which panics on Rc pointers. + +type private RefC(v: int) = + member _.V = v + +// when-guard causes a shared default target -> two-switch path, ref binding `c` +let private twoSwitchRef (x: RefC option) (b: bool) = + match x with + | Some c when b -> c.V + 1 + | Some c -> c.V + | None -> -1 + +[] +let ``two-switch with reference binding works`` () = + twoSwitchRef (Some(RefC 5)) true |> equal 6 + twoSwitchRef (Some(RefC 5)) false |> equal 5 + twoSwitchRef None false |> equal -1 + +type private RefU = + | RA of RefC + | RB of RefC + +// or-pattern shares one target with a binding -> two-switch path +let private orPatRef (u: RefU) = + match u with + | RA c + | RB c -> c.V + +[] +let ``two-switch or-pattern with reference binding works`` () = + orPatRef (RA(RefC 5)) |> equal 5 + orPatRef (RB(RefC 7)) |> equal 7 + +type private ObjU = + | OA of obj + | OB of obj + +let private orPatObj (u: ObjU) : obj = + match u with + | OA o + | OB o -> o + +[] +let ``two-switch or-pattern with obj binding works`` () = + // exercises the getZeroObj placeholder for a `dyn Any` binding on the two-switch path + orPatObj (OA(box 9)) :? int |> equal true + orPatObj (OB(box 11)) :? string |> equal false diff --git a/tests/Rust/tests/src/ConvertTests.fs b/tests/Rust/tests/src/ConvertTests.fs index 4b930baa8c..33f69d37de 100644 --- a/tests/Rust/tests/src/ConvertTests.fs +++ b/tests/Rust/tests/src/ConvertTests.fs @@ -1564,3 +1564,23 @@ let ``Encoding.UTF8.GetString for range works`` () = let bytes = [| 0x7Auy; 0x61uy; 0xCCuy; 0x86uy; 0xC7uy; 0xBDuy; 0xCEuy; 0xB2uy; 0xF1uy; 0x8Fuy; 0xB3uy; 0xBFuy |] System.Text.Encoding.UTF8.GetString(bytes, 6, 6) |> equal "\u03B2\uD8FF\uDCFF" + +[] +let ``System.Convert.ToInteger from bool works`` () = + Convert.ToSByte(true) |> equal 1y + Convert.ToSByte(false) |> equal 0y + Convert.ToByte(true) |> equal 1uy + Convert.ToInt16(true) |> equal 1s + Convert.ToUInt16(true) |> equal 1us + Convert.ToInt32(true) |> equal 1 + Convert.ToInt32(false) |> equal 0 + Convert.ToUInt32(true) |> equal 1u + Convert.ToInt64(true) |> equal 1L + Convert.ToUInt64(true) |> equal 1uL + +[] +let ``System.Convert.ToFloat from bool works`` () = + Convert.ToSingle(true) |> equal 1.f + Convert.ToSingle(false) |> equal 0.f + Convert.ToDouble(true) |> equal 1. + Convert.ToDouble(false) |> equal 0. 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/StringTests.fs b/tests/Rust/tests/src/StringTests.fs index 2e57732910..7f67365f7a 100644 --- a/tests/Rust/tests/src/StringTests.fs +++ b/tests/Rust/tests/src/StringTests.fs @@ -952,6 +952,12 @@ let ``String.Split with single separator works`` () = let ``String.Split with multiple char args works`` () = "a;b,c".Split(',', ';') |> equal [|"a"; "b"; "c"|] +[] +let ``String.Split with multiple string separators works`` () = + "1-2_3".Split([|"-"; "_"|], StringSplitOptions.None) |> equal [|"1"; "2"; "3"|] + "a::b;;c".Split([|"::"; ";;"|], StringSplitOptions.None) |> equal [|"a"; "b"; "c"|] + "a--b".Split([|"--"|], StringSplitOptions.None) |> equal [|"a"; "b"|] + [] let ``String.Split with string array works`` () = "a;b,c".Split([|","; ";"|], StringSplitOptions.None) From 21bb2a92aaf92577b5f9ed23c666b67237469458 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Wed, 15 Jul 2026 20:49:56 +0200 Subject: [PATCH 2/3] feat(rust): add `Decimal.IsCanonical` --- src/fable-library-rust/src/Decimal.rs | 4 +++- tests/Rust/tests/src/ArithmeticTests.fs | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/fable-library-rust/src/Decimal.rs b/src/fable-library-rust/src/Decimal.rs index 84d517018c..f1d6bc2226 100644 --- a/src/fable-library-rust/src/Decimal.rs +++ b/src/fable-library-rust/src/Decimal.rs @@ -63,7 +63,9 @@ pub mod Decimal_ { 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 } - pub fn isCanonical(x: decimal) -> bool { true } + // 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() } diff --git a/tests/Rust/tests/src/ArithmeticTests.fs b/tests/Rust/tests/src/ArithmeticTests.fs index d09786fd51..3acf821fe8 100644 --- a/tests/Rust/tests/src/ArithmeticTests.fs +++ b/tests/Rust/tests/src/ArithmeticTests.fs @@ -184,6 +184,19 @@ let ``Decimal integer predicates work`` () = System.Decimal.IsOddInteger(-3M) |> equal true System.Decimal.IsOddInteger(3.5M) |> equal false +[] +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 + [] let ``Decimal.ToString works`` () = string 001.23456M |> equal "1.23456" From 9f4af0a510d8855bb261614ddcb2794727ca0076 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Wed, 15 Jul 2026 21:30:47 +0200 Subject: [PATCH 3/3] fix: OptionTest branch --- src/Fable.Transforms/Rust/Fable2Rust.fs | 14 +++++----- src/quicktest-rust/src/main.fs | 35 +++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 78dc568e81..632adbd4f9 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -3401,12 +3401,14 @@ module Util = | _, Fable.Value((Fable.CharConstant _ | Fable.NumberConstant _), _) -> Some(left, right) | Fable.Value((Fable.CharConstant _ | Fable.NumberConstant _), _), _ -> Some(right, left) | _ -> None - // NOTE: `OptionTest` is intentionally NOT converted into a UnionTag switch. - // When the option scrutinee is not a plain ident (e.g. a field access or an - // active-pattern result), `transformSwitch` cannot recover the `Option` type - // and emits integer-literal patterns (`0_i32`) against a native `Option`, - // which is a type error. Letting these fall through to the if/else decision - // tree path (is_some/is_none) compiles and runs correctly. + // 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) + + let right = makeIntConst (makeTest isSome 0 1) + Some(evalExpr, right) | Fable.Test(expr, Fable.UnionCaseTest tag, r) -> let evalExpr = Fable.Get(expr, Fable.UnionTag, Fable.Number(Int32, Fable.NumberInfo.Empty), r) diff --git a/src/quicktest-rust/src/main.fs b/src/quicktest-rust/src/main.fs index 5f68f2f6be..deef3d37b3 100644 --- a/src/quicktest-rust/src/main.fs +++ b/src/quicktest-rust/src/main.fs @@ -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 [] 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