diff --git a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs index 4d8d0a6254..c28508fd91 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs @@ -278,21 +278,38 @@ let transformTypeInfo (com: IPythonCompiler) ctx r (genMap: Map List.map (transformTypeInfo com ctx r genMap) |> Helpers.unzipArgs - // Check if the entity is actually declared in Python code - if - ent.IsInterface - || FSharp2Fable.Util.isErasedOrStringEnumEntity ent - || FSharp2Fable.Util.isGlobalOrImportedEntity ent - || FSharp2Fable.Util.isReplacementCandidate entRef - then - genericEntity ent.FullName generics, stmts - else - let reflectionMethodExpr = - FSharp2Fable.Util.entityIdentWithSuffix com entRef Naming.reflectionSuffix - let callee, stmts' = com.TransformAsExpr(ctx, reflectionMethodExpr) + let erasedFieldType = + if ent.IsFSharpUnion && FSharp2Fable.Util.hasAttribute Atts.erase ent then + match ent.UnionCases with + | [ uci ] when List.isSingle uci.UnionCaseFields -> Some uci.UnionCaseFields.Head.FieldType + | _ -> None + else + None + + match erasedFieldType with + | Some fieldType -> + let fieldGenMap = + Seq.zip (ent.GenericParameters |> Seq.map (fun p -> p.Name)) generics |> Map + + let expr, stmts' = transformTypeInfo com ctx r fieldGenMap fieldType + expr, stmts @ stmts' + | None -> + // Check if the entity is actually declared in Python code + if + ent.IsInterface + || FSharp2Fable.Util.isErasedOrStringEnumEntity ent + || FSharp2Fable.Util.isGlobalOrImportedEntity ent + || FSharp2Fable.Util.isReplacementCandidate entRef + then + genericEntity ent.FullName generics, stmts + else + let reflectionMethodExpr = + FSharp2Fable.Util.entityIdentWithSuffix com entRef Naming.reflectionSuffix + + let callee, stmts' = com.TransformAsExpr(ctx, reflectionMethodExpr) - Expression.call (callee, generics), stmts @ stmts' + Expression.call (callee, generics), stmts @ stmts' let transformReflectionInfo com ctx r (ent: Fable.Entity) generics = if ent.IsFSharpRecord then diff --git a/src/fable-library-py/fable_library/date.py b/src/fable-library-py/fable_library/date.py index 08dd229365..d8209c0500 100644 --- a/src/fable-library-py/fable_library/date.py +++ b/src/fable-library-py/fable_library/date.py @@ -50,6 +50,11 @@ ] +def _without_kind(d: datetime) -> datetime: + # .NET compares and subtracts the tick count and ignores DateTimeKind + return d.replace(tzinfo=None) + + @overload def subtract(x: datetime, y: datetime) -> TimeSpan: ... @overload @@ -58,7 +63,7 @@ def subtract(x: datetime, y: TimeSpan) -> datetime: ... def subtract(x: datetime, y: datetime | TimeSpan) -> datetime | TimeSpan: if isinstance(y, datetime): - delta = x - y + delta = _without_kind(x) - _without_kind(y) # ts.microseconds only contains the microseconds provided to the constructor # so we need to calculate the total microseconds ourselves delta_microseconds = delta.days * (24 * 3600 * 10**6) + delta.seconds * 10**6 + delta.microseconds @@ -604,7 +609,7 @@ def now() -> datetime: def utc_now() -> datetime: - return datetime.now(UTC).replace(tzinfo=None) + return datetime.now(UTC) def today() -> datetime: @@ -616,17 +621,19 @@ def to_local_time(date: datetime) -> datetime: def compare(x: datetime, y: datetime) -> int: - if x == y: + _x, _y = _without_kind(x), _without_kind(y) + + if _x == _y: return 0 - if x < y: + if _x < _y: return -1 return 1 def equals(x: datetime, y: datetime) -> bool: - return x == y + return _without_kind(x) == _without_kind(y) def max_value() -> datetime: diff --git a/src/fable-library-py/fable_library/decimal_.py b/src/fable-library-py/fable_library/decimal_.py index d9e92a2861..361c146f99 100644 --- a/src/fable-library-py/fable_library/decimal_.py +++ b/src/fable-library-py/fable_library/decimal_.py @@ -1,6 +1,6 @@ from decimal import MAX_EMAX, MIN_EMIN, Decimal, getcontext -from .core import FSharpRef, byte, float32, float64, int16, int64, sbyte, uint16, uint32, uint64 +from .core import Array, FSharpRef, array, byte, float32, float64, int16, int64, sbyte, uint16, uint32, uint64 from .types import IntegerTypes @@ -141,6 +141,48 @@ def from_parts( return value +def from_ints(low: IntegerTypes, mid: IntegerTypes, high: IntegerTypes, sign_exp: IntegerTypes) -> Decimal: + _sign_exp = int(sign_exp) + is_negative = 1 if _sign_exp < 0 else 0 + scale = (_sign_exp >> 16) & 0x7F + return from_parts(low, mid, high, is_negative, scale) + + +def from_int_array(bits: Array[int]) -> Decimal: + return from_ints(bits[0], bits[1], bits[2], bits[3]) + + +def _to_int32(value: int) -> int: + masked = value & 0xFFFFFFFF + return masked - 0x100000000 if masked >= 0x80000000 else masked + + +def get_bits(value: Decimal) -> Array[int]: + sign, digits, exponent = value.as_tuple() + + if not isinstance(exponent, int): + raise ValueError(f"The value {value} cannot be represented as a System.Decimal.") + + mantissa = int("".join(str(digit) for digit in digits)) + scale = 0 + + if exponent > 0: + mantissa *= 10**exponent + else: + scale = -exponent + + flags = ((scale & 0x7F) << 16) | (0x80000000 if sign else 0) + + return array.Int32Array( + [ + _to_int32(mantissa), + _to_int32(mantissa >> 32), + _to_int32(mantissa >> 64), + _to_int32(flags), + ] + ) + + def to_string(x: Decimal) -> str: return str(x) @@ -165,14 +207,26 @@ def try_parse(string: str, def_value: FSharpRef[Decimal]) -> bool: return False +def _from_float(value: float, significant_digits: int) -> Decimal: + rounded = Decimal(f"{value:.{significant_digits}G}") + + if rounded.is_zero(): + return get_zero + + # `Decimal` keeps the exponent it was parsed with, which would print as 1E+20 + return Decimal(format(rounded, "f")) + + def create(value: float | float32 | IntegerTypes | str) -> Decimal: match value: - # Int32 and Float64 are plain `int`/`float`, which `Decimal` already accepts - # via the last case + # Int32 is a plain `int`, which `Decimal` already accepts via the last case case sbyte() | byte() | int16() | uint16() | uint32() | int64() | uint64(): return Decimal(int(value)) + # .NET rounds to 7 significant digits for Single and 15 for Double case float32(): - return Decimal(float(value)) + return _from_float(float(value), 7) + case float(): + return _from_float(value, 15) case _: return Decimal(value) @@ -184,7 +238,10 @@ def create(value: float | float32 | IntegerTypes | str) -> Decimal: "compare", "divide", "equals", + "from_int_array", + "from_ints", "from_parts", + "get_bits", "max", "min", "multiply", diff --git a/src/fable-library-py/src/ints.rs b/src/fable-library-py/src/ints.rs index 6580ed1b2f..258c558b8e 100644 --- a/src/fable-library-py/src/ints.rs +++ b/src/fable-library-py/src/ints.rs @@ -1311,6 +1311,12 @@ pub fn parse_int64( // value, not a two's complement bit pattern, so u64 cannot parse it i64::from_str_radix(&final_string, actual_radix as u32) .map_err(|_| create_parse_error(string))? + } else if unsigned { + // The upper half of the u64 range does not fit in an i64, so it is carried + // as its two's complement bit pattern and cast back by the caller + u64::from_str_radix(&final_string, actual_radix as u32) + .map(|u_val| u_val as i64) + .map_err(|_| create_parse_error(string))? } else { // For decimal, use standard i64 parsing i64::from_str_radix(&final_string, actual_radix as u32) diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index c1a23027bc..171ada44ad 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -192,6 +192,12 @@ let tests = tryParse UInt64.TryParse 0UL "99" |> equal (true, 99UL) tryParse UInt64.TryParse 0UL "foo" |> equal (false, 0UL) + testCase "System.UInt64.TryParse works above Int64.MaxValue" <| fun () -> + tryParse UInt64.TryParse 0UL "9223372036854775808" |> equal (true, 9223372036854775808UL) + tryParse UInt64.TryParse 0UL "18446744073709551615" |> equal (true, UInt64.MaxValue) + tryParse UInt64.TryParse 0UL "18446744073709551616" |> equal (false, 0UL) + tryParse UInt64.TryParse 0UL "-1" |> equal (false, 0UL) + testCase "Parsing integers with different radices works" <| fun () -> equal 11 (int "11") equal 17 (int "0x11") diff --git a/tests/Python/TestArithmetic.fs b/tests/Python/TestArithmetic.fs index 1bd98a9604..60f5d5612c 100644 --- a/tests/Python/TestArithmetic.fs +++ b/tests/Python/TestArithmetic.fs @@ -419,23 +419,51 @@ let ``test Decimal.Negate works`` () = let ``test Decimal Evaluation order is preserved by generated code`` () = (4.4567M - 2.2234M) * 2.6492M + 1.2493M |> equal 7.16575836M -// [] -// let ``test Decimal constructors work`` () = -// let d = 1.2493M -// let bits = Decimal.GetBits(d) -// let d2 = Decimal(bits) -// let d3 = Decimal(bits[0], bits[1], bits[2], false, 4uy) -// d2 |> equal d -// d3 |> equal d - -// [] -// let ``test Decimal GetBits works`` () = -// let d = Decimal([| -1; -1; -2; 0 |]) -// let bits = Decimal.GetBits(d) -// let d2 = Decimal(bits) -// let d3 = Decimal(bits[0], bits[1], bits[2], true, 0uy) -// d2 |> equal d -// d3 |> equal -d +[] +let ``test Decimal constructors work`` () = + let d = 1.2493M + let bits = Decimal.GetBits(d) + let d2 = Decimal(bits) + let d3 = Decimal(bits[0], bits[1], bits[2], false, 4uy) + d2 |> equal d + d3 |> equal d + +[] +let ``test Decimal GetBits works`` () = + let d = Decimal([| -1; -1; -2; 0 |]) + let bits = Decimal.GetBits(d) + let d2 = Decimal(bits) + let d3 = Decimal(bits[0], bits[1], bits[2], true, 0uy) + d2 |> equal d + d3 |> equal -d + +[] +let ``test Decimal GetBits works with arithmetic operations`` () = + let check (d: decimal) = + let bits = Decimal.GetBits(d) + Decimal(bits) |> equal d + + check (10M + 10M) + check (100.5M - 0.5M) + check (5M * 200M) + check (-0.1M * 100M) + check (2000M / 10M) + check (1.5M + 0.5M) + +[] +let ``test Decimal from a float rounds like .NET`` () = + string (decimal 0.1) |> equal "0.1" + string (decimal -0.1) |> equal "-0.1" + string (decimal 0.3) |> equal "0.3" + string (decimal 1.1) |> equal "1.1" + string (decimal 1e20) |> equal "100000000000000000000" + string (decimal (1.0 / 3.0)) |> equal "0.333333333333333" + +[] +let ``test Decimal from a float32 rounds like .NET`` () = + string (decimal 0.1f) |> equal "0.1" + string (decimal -0.1f) |> equal "-0.1" + string (decimal (1.0f / 3.0f)) |> equal "0.3333333" [] let ``test Decimal abs works`` () = diff --git a/tests/Python/TestConvert.fs b/tests/Python/TestConvert.fs index 95dba9c4cb..781b711289 100644 --- a/tests/Python/TestConvert.fs +++ b/tests/Python/TestConvert.fs @@ -197,6 +197,13 @@ let ``test System.UInt64.TryParse works`` () = tryParse UInt64.TryParse 0UL "99" |> equal (true, 99UL) tryParse UInt64.TryParse 0UL "foo" |> equal (false, 0UL) +[] +let ``test System.UInt64.TryParse works above Int64.MaxValue`` () = + tryParse UInt64.TryParse 0UL "9223372036854775808" |> equal (true, 9223372036854775808UL) + tryParse UInt64.TryParse 0UL "18446744073709551615" |> equal (true, UInt64.MaxValue) + tryParse UInt64.TryParse 0UL "18446744073709551616" |> equal (false, 0UL) + tryParse UInt64.TryParse 0UL "-1" |> equal (false, 0UL) + [] let ``test Parsing integers with different radices works`` () = equal 11 (int "11") diff --git a/tests/Python/TestDateTime.fs b/tests/Python/TestDateTime.fs index a1d06ff962..76e9af4218 100644 --- a/tests/Python/TestDateTime.fs +++ b/tests/Python/TestDateTime.fs @@ -925,7 +925,8 @@ let ``test DateTime.Now works`` () = let ``test DateTime.UtcNow works`` () = let d = DateTime.UtcNow d > DateTime.MinValue |> equal true - // d.Kind |> equal DateTimeKind.Utc // currently we're not storing Kind + d.Kind |> equal DateTimeKind.Utc + d.Date.Kind |> equal DateTimeKind.Utc [] let ``test DateTime.AddYears works`` () = diff --git a/tests/Python/TestReflection.fs b/tests/Python/TestReflection.fs index 3ce9051c29..43c34cda20 100644 --- a/tests/Python/TestReflection.fs +++ b/tests/Python/TestReflection.fs @@ -664,5 +664,39 @@ let ``test Can get union values without type`` () = getCaseFields x |> equal [||] getCaseFields y |> equal [|5; "foo"|] +type ReactiveNode<'T>(value: 'T) = + member _.Value = value + +[] +type ErasedView<'T> = + | ErasedView of node: ReactiveNode<'T> + +type ModelWithErasedField = { Item: ErasedView } + +type DuWithErasedField = + | Wrapped of ErasedView + | Empty + +[] +let ``test An erased union reflects as the type it erases to`` () = + let erasedTyp = typeof> + let nodeTyp = typeof> + erasedTyp.FullName |> equal nodeTyp.FullName + FSharpType.IsUnion erasedTyp |> equal false + +[] +let ``test Reflection works for a record with an erased generic union field`` () = + let typ = typeof + FSharpType.IsRecord typ |> equal true + let fields = FSharpType.GetRecordFields typ + fields.Length |> equal 1 + fields.[0].PropertyType.GetGenericArguments().[0] |> equal typeof + +[] +let ``test Reflection works for a union with an erased generic union field`` () = + let typ = typeof + let case = (FSharpType.GetUnionCases typ).[0] + case.GetFields().[0].PropertyType.GetGenericArguments().[0] |> equal typeof + #endif #endif