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
43 changes: 30 additions & 13 deletions src/Fable.Transforms/Python/Fable2Python.Reflection.fs
Original file line number Diff line number Diff line change
Expand Up @@ -278,21 +278,38 @@ let transformTypeInfo (com: IPythonCompiler) ctx r (genMap: Map<string, Expressi

let generics, stmts =
generics |> 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
Expand Down
17 changes: 12 additions & 5 deletions src/fable-library-py/fable_library/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
65 changes: 61 additions & 4 deletions src/fable-library-py/fable_library/decimal_.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions src/fable-library-py/src/ints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions tests/Js/Main/ConvertTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
62 changes: 45 additions & 17 deletions tests/Python/TestArithmetic.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

// [<Fact>]
// 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

// [<Fact>]
// 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
[<Fact>]
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

[<Fact>]
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

[<Fact>]
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)

[<Fact>]
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"

[<Fact>]
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"

[<Fact>]
let ``test Decimal abs works`` () =
Expand Down
7 changes: 7 additions & 0 deletions tests/Python/TestConvert.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

[<Fact>]
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)

[<Fact>]
let ``test Parsing integers with different radices works`` () =
equal 11 (int "11")
Expand Down
3 changes: 2 additions & 1 deletion tests/Python/TestDateTime.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

[<Fact>]
let ``test DateTime.AddYears works`` () =
Expand Down
34 changes: 34 additions & 0 deletions tests/Python/TestReflection.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

[<Erase>]
type ErasedView<'T> =
| ErasedView of node: ReactiveNode<'T>

type ModelWithErasedField = { Item: ErasedView<int> }

type DuWithErasedField =
| Wrapped of ErasedView<int>
| Empty

[<Fact>]
let ``test An erased union reflects as the type it erases to`` () =
let erasedTyp = typeof<ErasedView<int>>
let nodeTyp = typeof<ReactiveNode<int>>
erasedTyp.FullName |> equal nodeTyp.FullName
FSharpType.IsUnion erasedTyp |> equal false

[<Fact>]
let ``test Reflection works for a record with an erased generic union field`` () =
let typ = typeof<ModelWithErasedField>
FSharpType.IsRecord typ |> equal true
let fields = FSharpType.GetRecordFields typ
fields.Length |> equal 1
fields.[0].PropertyType.GetGenericArguments().[0] |> equal typeof<int>

[<Fact>]
let ``test Reflection works for a union with an erased generic union field`` () =
let typ = typeof<DuWithErasedField>
let case = (FSharpType.GetUnionCases typ).[0]
case.GetFields().[0].PropertyType.GetGenericArguments().[0] |> equal typeof<int>

#endif
#endif
Loading