Skip to content

Commit

Permalink
[JS/TS] Fix #4049: decimal/bigint to integer conversion checks (#4052)
Browse files Browse the repository at this point in the history
* Updated fable-library-rust dependencies

* Added decimal conversion tests

* [JS/TS] Fix #4049: decimal/bigint to integer conversion checks
  • Loading branch information
ncave authored Feb 18, 2025
1 parent 204d30d commit b1541eb
Show file tree
Hide file tree
Showing 13 changed files with 556 additions and 139 deletions.
4 changes: 4 additions & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Fixed

* [JS/TS] Fix #4049: decimal/bigint to integer conversion checks (by @ncave)

## 5.0.0-alpha.10 - 2025-02-16

### Added
Expand Down
4 changes: 4 additions & 0 deletions src/Fable.Compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Fixed

* [JS/TS] Fix #4049: decimal/bigint to integer conversion checks (by @ncave)

## 5.0.0-alpha.10 - 2025-02-16

### Added
Expand Down
21 changes: 2 additions & 19 deletions src/Fable.Transforms/Python/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@ let toFloat com (ctx: Context) r targetType (args: Expr list) : Expr =
| _ -> TypeCast(args.Head, targetType)
| _ ->
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"

TypeCast(args.Head, targetType)

let toDecimal com (ctx: Context) r targetType (args: Expr list) : Expr =
Expand All @@ -266,14 +265,9 @@ let toDecimal com (ctx: Context) r targetType (args: Expr list) : Expr =
match kind with
| Decimal -> args.Head
| BigInt -> Helper.LibCall(com, "big_int", castBigIntMethod targetType, targetType, args)
| Int64
| UInt64 ->
Helper.LibCall(com, "long", "toNumber", Float64.Number, args)
|> makeDecimalFromExpr com r targetType
| _ -> makeDecimalFromExpr com r targetType args.Head
| _ ->
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"

TypeCast(args.Head, targetType)

// Apparently ~~ is faster than Math.floor (see https://coderwall.com/p/9b6ksa/is-faster-than-math-floor)
Expand All @@ -288,11 +282,8 @@ let stringToInt com (ctx: Context) r targetType (args: Expr list) : Expr =
| x -> FableError $"Unexpected type in stringToInt: %A{x}" |> raise

let style = int System.Globalization.NumberStyles.Any

let _isFloatOrDecimal, numberModule, unsigned, bitsize = getParseParams kind

let parseArgs = [ makeIntConst style; makeBoolConst unsigned; makeIntConst bitsize ]

Helper.LibCall(com, numberModule, "parse", targetType, [ args.Head ] @ parseArgs @ args.Tail, ?loc = r)

let toLong com (ctx: Context) r (unsigned: bool) targetType (args: Expr list) : Expr =
Expand All @@ -311,10 +302,7 @@ let toLong com (ctx: Context) r (unsigned: bool) targetType (args: Expr list) :
| String -> stringToInt com ctx r targetType args
| Number(kind, _) ->
match kind with
| Decimal ->
let n = Helper.LibCall(com, "decimal", "toNumber", Float64.Number, args)

Helper.LibCall(com, "long", "fromNumber", targetType, [ n; makeBoolConst unsigned ])
| Decimal -> Helper.LibCall(com, "decimal", "toInt", targetType, args)
| BigInt -> Helper.LibCall(com, "big_int", castBigIntMethod targetType, targetType, args)
| Int64
| UInt64 -> Helper.LibCall(com, "long", "fromValue", targetType, args @ [ makeBoolConst unsigned ])
Expand All @@ -333,7 +321,6 @@ let toLong com (ctx: Context) r (unsigned: bool) targetType (args: Expr list) :
| UNativeInt -> FableError "Converting (u)nativeint to long is not supported" |> raise
| _ ->
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"

TypeCast(args.Head, targetType)

/// Conversion to integers (excluding longs and bigints)
Expand Down Expand Up @@ -361,27 +348,23 @@ let toInt com (ctx: Context) r targetType (args: Expr list) =
match typeFrom with
| Int64
| UInt64 -> Helper.LibCall(com, "Long", "to_int", targetType, args) // TODO: make no-op
| Decimal -> Helper.LibCall(com, "Decimal", "to_number", targetType, args)
| Decimal -> Helper.LibCall(com, "Decimal", "to_int", targetType, args)
| _ -> args.Head
|> emitCast typeTo
else
TypeCast(args.Head, targetType)
| _ ->
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"

TypeCast(args.Head, targetType)

let round com (args: Expr list) =
match args.Head.Type with
| Number(Decimal, _) ->
let n = Helper.LibCall(com, "decimal", "toNumber", Float64.Number, [ args.Head ])

let rounded = Helper.LibCall(com, "util", "round", Float64.Number, [ n ])

rounded :: args.Tail
| Number((Float32 | Float64), _) ->
let rounded = Helper.LibCall(com, "util", "round", Float64.Number, [ args.Head ])

rounded :: args.Tail
| _ -> args

Expand Down
38 changes: 24 additions & 14 deletions src/Fable.Transforms/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ let toFloat com (ctx: Context) r targetType (args: Expr list) : Expr =
| String -> Helper.LibCall(com, "Double", "parse", targetType, args)
| Number(kind, _) ->
match kind with
| Decimal -> Helper.LibCall(com, "Decimal", "toNumber", targetType, args)
| Decimal -> Helper.LibCall(com, "Decimal", "toFloat64", targetType, args)
| BigIntegers _ -> Helper.LibCall(com, "BigInt", "toFloat64", targetType, args)
| _ -> TypeCast(args.Head, targetType)
| _ ->
Expand All @@ -254,7 +254,7 @@ let toDecimal com (ctx: Context) r targetType (args: Expr list) : Expr =
| Number(kind, _) ->
match kind with
| Decimal -> args.Head
| BigIntegers _ -> Helper.LibCall(com, "BigInt", "toDecimal", Float64.Number, args)
| BigIntegers _ -> Helper.LibCall(com, "BigInt", "toDecimal", targetType, args)
| _ -> makeDecimalFromExpr com r targetType args.Head
| _ ->
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"
Expand All @@ -281,8 +281,9 @@ let stringToInt com (ctx: Context) r targetType (args: Expr list) : Expr =

let wrapLong com (ctx: Context) r t (arg: Expr) : Expr =
match t with
| Number(BigInt, _) -> arg
| Number(kind, _) ->
let toMeth = "to" + kind.ToString()
let toMeth = "to" + kind.ToString() + "_unchecked"
Helper.LibCall(com, "BigInt", toMeth, t, [ arg ])
| _ ->
addWarning com ctx.InlinePath r "Unexpected conversion to long"
Expand All @@ -297,10 +298,18 @@ let toLong com (ctx: Context) r targetType (args: Expr list) : Expr =
|> wrapLong com ctx r targetType
| String, _ -> stringToInt com ctx r targetType args |> wrapLong com ctx r targetType
| Number(fromKind, _), Number(toKind, _) ->
let fromMeth = "from" + fromKind.ToString()
match fromKind with
| BigInt ->
let toMeth = "to" + toKind.ToString()
Helper.LibCall(com, "BigInt", toMeth, targetType, args)
| Decimal ->
let toMeth = "to" + toKind.ToString()
Helper.LibCall(com, "Decimal", toMeth, targetType, args)
| _ ->
let fromMeth = "from" + fromKind.ToString()

Helper.LibCall(com, "BigInt", fromMeth, BigInt.Number, args, ?loc = r)
|> wrapLong com ctx r targetType
Helper.LibCall(com, "BigInt", fromMeth, BigInt.Number, args, ?loc = r)
|> wrapLong com ctx r targetType
| _ ->
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"
TypeCast(args.Head, targetType)
Expand All @@ -327,10 +336,15 @@ let toInt com (ctx: Context) r targetType (args: Expr list) =
| Number(fromKind, _), Number(toKind, _) ->
if needToCast fromKind toKind then
match fromKind with
| BigInt ->
let toMeth = "to" + toKind.ToString()
Helper.LibCall(com, "BigInt", toMeth, targetType, args)
| BigIntegers _ ->
let meth = "to" + toKind.ToString()
Helper.LibCall(com, "BigInt", meth, targetType, args)
| Decimal -> Helper.LibCall(com, "Decimal", "toNumber", targetType, args)
let toMeth = "to" + toKind.ToString() + "_unchecked"
Helper.LibCall(com, "BigInt", toMeth, targetType, args)
| Decimal ->
let toMeth = "to" + toKind.ToString()
Helper.LibCall(com, "Decimal", toMeth, targetType, args)
| _ -> args.Head
|> emitIntCast toKind
else
Expand All @@ -342,14 +356,10 @@ let toInt com (ctx: Context) r targetType (args: Expr list) =
let round com (args: Expr list) =
match args.Head.Type with
| Number(Decimal, _) ->
let n = Helper.LibCall(com, "Decimal", "toNumber", Float64.Number, [ args.Head ])

let rounded = Helper.LibCall(com, "Util", "round", Float64.Number, [ n ])

let rounded = Helper.LibCall(com, "Decimal", "round", Decimal.Number, [ args.Head ])
rounded :: args.Tail
| Number(Floats _, _) ->
let rounded = Helper.LibCall(com, "Util", "round", Float64.Number, [ args.Head ])

rounded :: args.Tail
| _ -> args

Expand Down
2 changes: 1 addition & 1 deletion src/Fable.Transforms/Rust/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2485,7 +2485,7 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (
| "ToString", [ arg ] -> toString com ctx r args |> Some
| "ToString", [ arg; ExprType(Number(Int32, _)) ] ->
Helper.LibCall(com, "Convert", "toStringRadix", t, args, ?loc = r) |> Some
| ("ToHexString" | "FromHexString" | "ToBase64String" | "FromBase64String"), [ arg ] ->
| ("ToHexString" | "ToHexStringLower" | "FromHexString" | "ToBase64String" | "FromBase64String"), [ arg ] ->
Helper.LibCall(com, "Convert", (Naming.lowerFirst i.CompiledName), t, args, ?loc = r)
|> Some
| _ -> None
Expand Down
5 changes: 5 additions & 0 deletions src/fable-library-py/fable_library/decimal_.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ def to_number(x: Decimal) -> float:
return float(x)


def to_int(x: Decimal) -> int:
return int(x)


def parse(string: str) -> Decimal:
return Decimal(string)

Expand Down Expand Up @@ -185,6 +189,7 @@ def try_parse(string: str, def_value: FSharpRef[Decimal]) -> bool:
"from_parts",
"to_string",
"to_number",
"to_int",
"try_parse",
"parse",
]
8 changes: 5 additions & 3 deletions src/fable-library-rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ hashbrown = { version = "0.14", optional = true }
num-bigint = { version = "0.4", optional = true }
num-integer = { version = "0.1", optional = true }
num-traits = { version = "0.2", optional = true }
regex = { version = "1.10", optional = true }
regex = { version = "1.11", optional = true }
rust_decimal = { version = "1.36", features = ["maths"], default-features = false, optional = true }
startup = { version = "0.1", path = "vendored/startup", optional = true }
uuid = { version = "1.10", features = ["v4"], default-features = false, optional = true }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
uuid = { version = "1.13", features = ["v4"], default-features = false, optional = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
uuid = { version = "1.13", features = ["v4", "js"], default-features = false, optional = true }
4 changes: 4 additions & 0 deletions src/fable-library-rust/src/Convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ pub mod Convert_ {
fromString(s)
}

pub fn toHexStringLower(bytes: Array<u8>) -> string {
fromString(toHexString(bytes).to_lowercase())
}

pub fn fromHexString(s: string) -> Array<u8> {
fn decode(c: u8) -> u8 {
match c {
Expand Down
70 changes: 48 additions & 22 deletions src/fable-library-ts/BigInt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,20 +130,46 @@ export function toByteArray(value: bigint): number[] {
return toSignedBytes(value, isBigEndian) as any as number[];
}

export function toInt8(x: bigint): int8 { return Number(BigInt.asIntN(8, x)); }
export function toUInt8(x: bigint): uint8 { return Number(BigInt.asUintN(8, x)); }
export function toInt16(x: bigint): int16 { return Number(BigInt.asIntN(16, x)); }
export function toUInt16(x: bigint): uint16 { return Number(BigInt.asUintN(16, x)); }
export function toInt32(x: bigint): int32 { return Number(BigInt.asIntN(32, x)); }
export function toUInt32(x: bigint): uint32 { return Number(BigInt.asUintN(32, x)); }
export function toInt64(x: bigint): int64 { return BigInt.asIntN(64, x); }
export function toUInt64(x: bigint): uint64 { return BigInt.asUintN(64, x); }
export function toInt128(x: bigint): int128 { return BigInt.asIntN(128, x); }
export function toUInt128(x: bigint): uint128 { return BigInt.asUintN(128, x); }
export function toNativeInt(x: bigint): nativeint { return BigInt.asIntN(64, x); }
export function toUNativeInt(x: bigint): unativeint { return BigInt.asUintN(64, x); }

export function toFloat16(x: bigint): float32 { return Number(x); }
export function toIntN_unchecked(bits: number, x: bigint, signed: boolean): bigint {
return signed ? BigInt.asIntN(bits, x) : BigInt.asUintN(bits, x);
}

export function toIntN(bits: number, x: bigint, signed: boolean): bigint {
let higher_bits = abs(x) >> BigInt(bits);
if (higher_bits !== 0n) {
const s = signed ? "a signed" : "an unsigned";
throw new Error(`Value was either too large or too small for ${s} ${bits}-bit integer.`);
}
return signed ? BigInt.asIntN(bits, x) : BigInt.asUintN(bits, x);
}

export function toInt8(x: bigint): int8 { return Number(toIntN(8, x, true)); }
export function toUInt8(x: bigint): uint8 { return Number(toIntN(8, x, false)); }
export function toInt16(x: bigint): int16 { return Number(toIntN(16, x, true)); }
export function toUInt16(x: bigint): uint16 { return Number(toIntN(16, x, false)); }
export function toInt32(x: bigint): int32 { return Number(toIntN(32, x, true)); }
export function toUInt32(x: bigint): uint32 { return Number(toIntN(32, x, false)); }
export function toInt64(x: bigint): int64 { return toIntN(64, x, true); }
export function toUInt64(x: bigint): uint64 { return toIntN(64, x, false); }
export function toInt128(x: bigint): int128 { return toIntN(128, x, true); }
export function toUInt128(x: bigint): uint128 { return toIntN(128, x, false); }
export function toNativeInt(x: bigint): nativeint { return toIntN(64, x, true); }
export function toUNativeInt(x: bigint): unativeint { return toIntN(64, x, false); }

export function toInt8_unchecked(x: bigint): int8 { return Number(toIntN_unchecked(8, x, true)); }
export function toUInt8_unchecked(x: bigint): uint8 { return Number(toIntN_unchecked(8, x, false)); }
export function toInt16_unchecked(x: bigint): int16 { return Number(toIntN_unchecked(16, x, true)); }
export function toUInt16_unchecked(x: bigint): uint16 { return Number(toIntN_unchecked(16, x, false)); }
export function toInt32_unchecked(x: bigint): int32 { return Number(toIntN_unchecked(32, x, true)); }
export function toUInt32_unchecked(x: bigint): uint32 { return Number(toIntN_unchecked(32, x, false)); }
export function toInt64_unchecked(x: bigint): int64 { return toIntN_unchecked(64, x, true); }
export function toUInt64_unchecked(x: bigint): uint64 { return toIntN_unchecked(64, x, false); }
export function toInt128_unchecked(x: bigint): int128 { return toIntN_unchecked(128, x, true); }
export function toUInt128_unchecked(x: bigint): uint128 { return toIntN_unchecked(128, x, false); }
export function toNativeInt_unchecked(x: bigint): nativeint { return toIntN_unchecked(64, x, true); }
export function toUNativeInt_unchecked(x: bigint): unativeint { return toIntN_unchecked(64, x, false); }

export function toFloat16(x: bigint): float16 { return Number(x); }
export function toFloat32(x: bigint): float32 { return Number(x); }
export function toFloat64(x: bigint): float64 { return Number(x); }

Expand Down Expand Up @@ -191,14 +217,14 @@ export function modPow(x: bigint, e: bigint, m: bigint): bigint {
export function divRem(x: bigint, y: bigint): [bigint, bigint];
export function divRem(x: bigint, y: bigint, out: FSharpRef<bigint>): bigint;
export function divRem(x: bigint, y: bigint, out?: FSharpRef<bigint>): bigint | [bigint, bigint] {
const div = x / y;
const rem = x % y;
if (out === void 0) {
return [div, rem];
} else {
out.contents = rem;
return div;
}
const div = x / y;
const rem = x % y;
if (out === void 0) {
return [div, rem];
} else {
out.contents = rem;
return div;
}
}

export function greatestCommonDivisor(x: bigint, y: bigint): bigint {
Expand Down
22 changes: 21 additions & 1 deletion src/fable-library-ts/Decimal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import Decimal, { BigSource } from "./lib/big.js";
import { Numeric, symbol } from "./Numeric.js";
import { FSharpRef } from "./Types.js";
import { combineHashCodes } from "./Util.js";
import { int8, uint8, int16, uint16, int32, uint32, float16, float32, float64 } from "./Int32.js";
import { fromDecimal, int64, uint64, int128, uint128, nativeint, unativeint } from "./BigInt.js";
import * as bigInt from "./BigInt.js";

Decimal.prototype.GetHashCode = function () {
return combineHashCodes([this.s, this.e].concat(this.c))
Expand Down Expand Up @@ -139,10 +142,27 @@ export function parse(str: string): Decimal {
}
}

export function toNumber(x: Decimal) {
export function toNumber(x: Decimal): number {
return +x;
}

export function toInt8(x: Decimal): int8 { return bigInt.toInt8(fromDecimal(x)); }
export function toUInt8(x: Decimal): uint8 { return bigInt.toUInt8(fromDecimal(x)); }
export function toInt16(x: Decimal): int16 { return bigInt.toInt16(fromDecimal(x)); }
export function toUInt16(x: Decimal): uint16 { return bigInt.toUInt16(fromDecimal(x)); }
export function toInt32(x: Decimal): int32 { return bigInt.toInt32(fromDecimal(x)); }
export function toUInt32(x: Decimal): uint32 { return bigInt.toUInt32(fromDecimal(x)); }
export function toInt64(x: Decimal): int64 { return bigInt.toInt64(fromDecimal(x)); }
export function toUInt64(x: Decimal): uint64 { return bigInt.toUInt64(fromDecimal(x)); }
export function toInt128(x: Decimal): int128 { return bigInt.toInt128(fromDecimal(x)); }
export function toUInt128(x: Decimal): uint128 { return bigInt.toUInt128(fromDecimal(x)); }
export function toNativeInt(x: Decimal): nativeint { return bigInt.toNativeInt(fromDecimal(x)); }
export function toUNativeInt(x: Decimal): unativeint { return bigInt.toUNativeInt(fromDecimal(x)); }

export function toFloat16(x: Decimal): float16 { return toNumber(x); }
export function toFloat32(x: Decimal): float32 { return toNumber(x); }
export function toFloat64(x: Decimal): float64 { return toNumber(x); }

function decimalToHex(dec: Uint8Array, bitSize: number) {
const hex = new Uint8Array(bitSize / 4 | 0);
let hexCount = 1;
Expand Down
Loading

0 comments on commit b1541eb

Please sign in to comment.