Skip to content

Commit b1541eb

Browse files
authored
[JS/TS] Fix #4049: decimal/bigint to integer conversion checks (#4052)
* Updated fable-library-rust dependencies * Added decimal conversion tests * [JS/TS] Fix #4049: decimal/bigint to integer conversion checks
1 parent 204d30d commit b1541eb

File tree

13 files changed

+556
-139
lines changed

13 files changed

+556
-139
lines changed

src/Fable.Cli/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Fixed
11+
12+
* [JS/TS] Fix #4049: decimal/bigint to integer conversion checks (by @ncave)
13+
1014
## 5.0.0-alpha.10 - 2025-02-16
1115

1216
### Added

src/Fable.Compiler/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Fixed
11+
12+
* [JS/TS] Fix #4049: decimal/bigint to integer conversion checks (by @ncave)
13+
1014
## 5.0.0-alpha.10 - 2025-02-16
1115

1216
### Added

src/Fable.Transforms/Python/Replacements.fs

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,6 @@ let toFloat com (ctx: Context) r targetType (args: Expr list) : Expr =
252252
| _ -> TypeCast(args.Head, targetType)
253253
| _ ->
254254
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"
255-
256255
TypeCast(args.Head, targetType)
257256

258257
let toDecimal com (ctx: Context) r targetType (args: Expr list) : Expr =
@@ -266,14 +265,9 @@ let toDecimal com (ctx: Context) r targetType (args: Expr list) : Expr =
266265
match kind with
267266
| Decimal -> args.Head
268267
| BigInt -> Helper.LibCall(com, "big_int", castBigIntMethod targetType, targetType, args)
269-
| Int64
270-
| UInt64 ->
271-
Helper.LibCall(com, "long", "toNumber", Float64.Number, args)
272-
|> makeDecimalFromExpr com r targetType
273268
| _ -> makeDecimalFromExpr com r targetType args.Head
274269
| _ ->
275270
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"
276-
277271
TypeCast(args.Head, targetType)
278272

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

290284
let style = int System.Globalization.NumberStyles.Any
291-
292285
let _isFloatOrDecimal, numberModule, unsigned, bitsize = getParseParams kind
293-
294286
let parseArgs = [ makeIntConst style; makeBoolConst unsigned; makeIntConst bitsize ]
295-
296287
Helper.LibCall(com, numberModule, "parse", targetType, [ args.Head ] @ parseArgs @ args.Tail, ?loc = r)
297288

298289
let toLong com (ctx: Context) r (unsigned: bool) targetType (args: Expr list) : Expr =
@@ -311,10 +302,7 @@ let toLong com (ctx: Context) r (unsigned: bool) targetType (args: Expr list) :
311302
| String -> stringToInt com ctx r targetType args
312303
| Number(kind, _) ->
313304
match kind with
314-
| Decimal ->
315-
let n = Helper.LibCall(com, "decimal", "toNumber", Float64.Number, args)
316-
317-
Helper.LibCall(com, "long", "fromNumber", targetType, [ n; makeBoolConst unsigned ])
305+
| Decimal -> Helper.LibCall(com, "decimal", "toInt", targetType, args)
318306
| BigInt -> Helper.LibCall(com, "big_int", castBigIntMethod targetType, targetType, args)
319307
| Int64
320308
| UInt64 -> Helper.LibCall(com, "long", "fromValue", targetType, args @ [ makeBoolConst unsigned ])
@@ -333,7 +321,6 @@ let toLong com (ctx: Context) r (unsigned: bool) targetType (args: Expr list) :
333321
| UNativeInt -> FableError "Converting (u)nativeint to long is not supported" |> raise
334322
| _ ->
335323
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"
336-
337324
TypeCast(args.Head, targetType)
338325

339326
/// Conversion to integers (excluding longs and bigints)
@@ -361,27 +348,23 @@ let toInt com (ctx: Context) r targetType (args: Expr list) =
361348
match typeFrom with
362349
| Int64
363350
| UInt64 -> Helper.LibCall(com, "Long", "to_int", targetType, args) // TODO: make no-op
364-
| Decimal -> Helper.LibCall(com, "Decimal", "to_number", targetType, args)
351+
| Decimal -> Helper.LibCall(com, "Decimal", "to_int", targetType, args)
365352
| _ -> args.Head
366353
|> emitCast typeTo
367354
else
368355
TypeCast(args.Head, targetType)
369356
| _ ->
370357
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"
371-
372358
TypeCast(args.Head, targetType)
373359

374360
let round com (args: Expr list) =
375361
match args.Head.Type with
376362
| Number(Decimal, _) ->
377363
let n = Helper.LibCall(com, "decimal", "toNumber", Float64.Number, [ args.Head ])
378-
379364
let rounded = Helper.LibCall(com, "util", "round", Float64.Number, [ n ])
380-
381365
rounded :: args.Tail
382366
| Number((Float32 | Float64), _) ->
383367
let rounded = Helper.LibCall(com, "util", "round", Float64.Number, [ args.Head ])
384-
385368
rounded :: args.Tail
386369
| _ -> args
387370

src/Fable.Transforms/Replacements.fs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ let toFloat com (ctx: Context) r targetType (args: Expr list) : Expr =
237237
| String -> Helper.LibCall(com, "Double", "parse", targetType, args)
238238
| Number(kind, _) ->
239239
match kind with
240-
| Decimal -> Helper.LibCall(com, "Decimal", "toNumber", targetType, args)
240+
| Decimal -> Helper.LibCall(com, "Decimal", "toFloat64", targetType, args)
241241
| BigIntegers _ -> Helper.LibCall(com, "BigInt", "toFloat64", targetType, args)
242242
| _ -> TypeCast(args.Head, targetType)
243243
| _ ->
@@ -254,7 +254,7 @@ let toDecimal com (ctx: Context) r targetType (args: Expr list) : Expr =
254254
| Number(kind, _) ->
255255
match kind with
256256
| Decimal -> args.Head
257-
| BigIntegers _ -> Helper.LibCall(com, "BigInt", "toDecimal", Float64.Number, args)
257+
| BigIntegers _ -> Helper.LibCall(com, "BigInt", "toDecimal", targetType, args)
258258
| _ -> makeDecimalFromExpr com r targetType args.Head
259259
| _ ->
260260
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"
@@ -281,8 +281,9 @@ let stringToInt com (ctx: Context) r targetType (args: Expr list) : Expr =
281281

282282
let wrapLong com (ctx: Context) r t (arg: Expr) : Expr =
283283
match t with
284+
| Number(BigInt, _) -> arg
284285
| Number(kind, _) ->
285-
let toMeth = "to" + kind.ToString()
286+
let toMeth = "to" + kind.ToString() + "_unchecked"
286287
Helper.LibCall(com, "BigInt", toMeth, t, [ arg ])
287288
| _ ->
288289
addWarning com ctx.InlinePath r "Unexpected conversion to long"
@@ -297,10 +298,18 @@ let toLong com (ctx: Context) r targetType (args: Expr list) : Expr =
297298
|> wrapLong com ctx r targetType
298299
| String, _ -> stringToInt com ctx r targetType args |> wrapLong com ctx r targetType
299300
| Number(fromKind, _), Number(toKind, _) ->
300-
let fromMeth = "from" + fromKind.ToString()
301+
match fromKind with
302+
| BigInt ->
303+
let toMeth = "to" + toKind.ToString()
304+
Helper.LibCall(com, "BigInt", toMeth, targetType, args)
305+
| Decimal ->
306+
let toMeth = "to" + toKind.ToString()
307+
Helper.LibCall(com, "Decimal", toMeth, targetType, args)
308+
| _ ->
309+
let fromMeth = "from" + fromKind.ToString()
301310

302-
Helper.LibCall(com, "BigInt", fromMeth, BigInt.Number, args, ?loc = r)
303-
|> wrapLong com ctx r targetType
311+
Helper.LibCall(com, "BigInt", fromMeth, BigInt.Number, args, ?loc = r)
312+
|> wrapLong com ctx r targetType
304313
| _ ->
305314
addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown"
306315
TypeCast(args.Head, targetType)
@@ -327,10 +336,15 @@ let toInt com (ctx: Context) r targetType (args: Expr list) =
327336
| Number(fromKind, _), Number(toKind, _) ->
328337
if needToCast fromKind toKind then
329338
match fromKind with
339+
| BigInt ->
340+
let toMeth = "to" + toKind.ToString()
341+
Helper.LibCall(com, "BigInt", toMeth, targetType, args)
330342
| BigIntegers _ ->
331-
let meth = "to" + toKind.ToString()
332-
Helper.LibCall(com, "BigInt", meth, targetType, args)
333-
| Decimal -> Helper.LibCall(com, "Decimal", "toNumber", targetType, args)
343+
let toMeth = "to" + toKind.ToString() + "_unchecked"
344+
Helper.LibCall(com, "BigInt", toMeth, targetType, args)
345+
| Decimal ->
346+
let toMeth = "to" + toKind.ToString()
347+
Helper.LibCall(com, "Decimal", toMeth, targetType, args)
334348
| _ -> args.Head
335349
|> emitIntCast toKind
336350
else
@@ -342,14 +356,10 @@ let toInt com (ctx: Context) r targetType (args: Expr list) =
342356
let round com (args: Expr list) =
343357
match args.Head.Type with
344358
| Number(Decimal, _) ->
345-
let n = Helper.LibCall(com, "Decimal", "toNumber", Float64.Number, [ args.Head ])
346-
347-
let rounded = Helper.LibCall(com, "Util", "round", Float64.Number, [ n ])
348-
359+
let rounded = Helper.LibCall(com, "Decimal", "round", Decimal.Number, [ args.Head ])
349360
rounded :: args.Tail
350361
| Number(Floats _, _) ->
351362
let rounded = Helper.LibCall(com, "Util", "round", Float64.Number, [ args.Head ])
352-
353363
rounded :: args.Tail
354364
| _ -> args
355365

src/Fable.Transforms/Rust/Replacements.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2485,7 +2485,7 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (
24852485
| "ToString", [ arg ] -> toString com ctx r args |> Some
24862486
| "ToString", [ arg; ExprType(Number(Int32, _)) ] ->
24872487
Helper.LibCall(com, "Convert", "toStringRadix", t, args, ?loc = r) |> Some
2488-
| ("ToHexString" | "FromHexString" | "ToBase64String" | "FromBase64String"), [ arg ] ->
2488+
| ("ToHexString" | "ToHexStringLower" | "FromHexString" | "ToBase64String" | "FromBase64String"), [ arg ] ->
24892489
Helper.LibCall(com, "Convert", (Naming.lowerFirst i.CompiledName), t, args, ?loc = r)
24902490
|> Some
24912491
| _ -> None

src/fable-library-py/fable_library/decimal_.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ def to_number(x: Decimal) -> float:
144144
return float(x)
145145

146146

147+
def to_int(x: Decimal) -> int:
148+
return int(x)
149+
150+
147151
def parse(string: str) -> Decimal:
148152
return Decimal(string)
149153

@@ -185,6 +189,7 @@ def try_parse(string: str, def_value: FSharpRef[Decimal]) -> bool:
185189
"from_parts",
186190
"to_string",
187191
"to_number",
192+
"to_int",
188193
"try_parse",
189194
"parse",
190195
]

src/fable-library-rust/Cargo.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ hashbrown = { version = "0.14", optional = true }
2626
num-bigint = { version = "0.4", optional = true }
2727
num-integer = { version = "0.1", optional = true }
2828
num-traits = { version = "0.2", optional = true }
29-
regex = { version = "1.10", optional = true }
29+
regex = { version = "1.11", optional = true }
3030
rust_decimal = { version = "1.36", features = ["maths"], default-features = false, optional = true }
3131
startup = { version = "0.1", path = "vendored/startup", optional = true }
32-
uuid = { version = "1.10", features = ["v4"], default-features = false, optional = true }
32+
33+
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
34+
uuid = { version = "1.13", features = ["v4"], default-features = false, optional = true }
3335

3436
[target.'cfg(target_arch = "wasm32")'.dependencies]
35-
getrandom = { version = "0.2", features = ["js"] }
37+
uuid = { version = "1.13", features = ["v4", "js"], default-features = false, optional = true }

src/fable-library-rust/src/Convert.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ pub mod Convert_ {
254254
fromString(s)
255255
}
256256

257+
pub fn toHexStringLower(bytes: Array<u8>) -> string {
258+
fromString(toHexString(bytes).to_lowercase())
259+
}
260+
257261
pub fn fromHexString(s: string) -> Array<u8> {
258262
fn decode(c: u8) -> u8 {
259263
match c {

src/fable-library-ts/BigInt.ts

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -130,20 +130,46 @@ export function toByteArray(value: bigint): number[] {
130130
return toSignedBytes(value, isBigEndian) as any as number[];
131131
}
132132

133-
export function toInt8(x: bigint): int8 { return Number(BigInt.asIntN(8, x)); }
134-
export function toUInt8(x: bigint): uint8 { return Number(BigInt.asUintN(8, x)); }
135-
export function toInt16(x: bigint): int16 { return Number(BigInt.asIntN(16, x)); }
136-
export function toUInt16(x: bigint): uint16 { return Number(BigInt.asUintN(16, x)); }
137-
export function toInt32(x: bigint): int32 { return Number(BigInt.asIntN(32, x)); }
138-
export function toUInt32(x: bigint): uint32 { return Number(BigInt.asUintN(32, x)); }
139-
export function toInt64(x: bigint): int64 { return BigInt.asIntN(64, x); }
140-
export function toUInt64(x: bigint): uint64 { return BigInt.asUintN(64, x); }
141-
export function toInt128(x: bigint): int128 { return BigInt.asIntN(128, x); }
142-
export function toUInt128(x: bigint): uint128 { return BigInt.asUintN(128, x); }
143-
export function toNativeInt(x: bigint): nativeint { return BigInt.asIntN(64, x); }
144-
export function toUNativeInt(x: bigint): unativeint { return BigInt.asUintN(64, x); }
145-
146-
export function toFloat16(x: bigint): float32 { return Number(x); }
133+
export function toIntN_unchecked(bits: number, x: bigint, signed: boolean): bigint {
134+
return signed ? BigInt.asIntN(bits, x) : BigInt.asUintN(bits, x);
135+
}
136+
137+
export function toIntN(bits: number, x: bigint, signed: boolean): bigint {
138+
let higher_bits = abs(x) >> BigInt(bits);
139+
if (higher_bits !== 0n) {
140+
const s = signed ? "a signed" : "an unsigned";
141+
throw new Error(`Value was either too large or too small for ${s} ${bits}-bit integer.`);
142+
}
143+
return signed ? BigInt.asIntN(bits, x) : BigInt.asUintN(bits, x);
144+
}
145+
146+
export function toInt8(x: bigint): int8 { return Number(toIntN(8, x, true)); }
147+
export function toUInt8(x: bigint): uint8 { return Number(toIntN(8, x, false)); }
148+
export function toInt16(x: bigint): int16 { return Number(toIntN(16, x, true)); }
149+
export function toUInt16(x: bigint): uint16 { return Number(toIntN(16, x, false)); }
150+
export function toInt32(x: bigint): int32 { return Number(toIntN(32, x, true)); }
151+
export function toUInt32(x: bigint): uint32 { return Number(toIntN(32, x, false)); }
152+
export function toInt64(x: bigint): int64 { return toIntN(64, x, true); }
153+
export function toUInt64(x: bigint): uint64 { return toIntN(64, x, false); }
154+
export function toInt128(x: bigint): int128 { return toIntN(128, x, true); }
155+
export function toUInt128(x: bigint): uint128 { return toIntN(128, x, false); }
156+
export function toNativeInt(x: bigint): nativeint { return toIntN(64, x, true); }
157+
export function toUNativeInt(x: bigint): unativeint { return toIntN(64, x, false); }
158+
159+
export function toInt8_unchecked(x: bigint): int8 { return Number(toIntN_unchecked(8, x, true)); }
160+
export function toUInt8_unchecked(x: bigint): uint8 { return Number(toIntN_unchecked(8, x, false)); }
161+
export function toInt16_unchecked(x: bigint): int16 { return Number(toIntN_unchecked(16, x, true)); }
162+
export function toUInt16_unchecked(x: bigint): uint16 { return Number(toIntN_unchecked(16, x, false)); }
163+
export function toInt32_unchecked(x: bigint): int32 { return Number(toIntN_unchecked(32, x, true)); }
164+
export function toUInt32_unchecked(x: bigint): uint32 { return Number(toIntN_unchecked(32, x, false)); }
165+
export function toInt64_unchecked(x: bigint): int64 { return toIntN_unchecked(64, x, true); }
166+
export function toUInt64_unchecked(x: bigint): uint64 { return toIntN_unchecked(64, x, false); }
167+
export function toInt128_unchecked(x: bigint): int128 { return toIntN_unchecked(128, x, true); }
168+
export function toUInt128_unchecked(x: bigint): uint128 { return toIntN_unchecked(128, x, false); }
169+
export function toNativeInt_unchecked(x: bigint): nativeint { return toIntN_unchecked(64, x, true); }
170+
export function toUNativeInt_unchecked(x: bigint): unativeint { return toIntN_unchecked(64, x, false); }
171+
172+
export function toFloat16(x: bigint): float16 { return Number(x); }
147173
export function toFloat32(x: bigint): float32 { return Number(x); }
148174
export function toFloat64(x: bigint): float64 { return Number(x); }
149175

@@ -191,14 +217,14 @@ export function modPow(x: bigint, e: bigint, m: bigint): bigint {
191217
export function divRem(x: bigint, y: bigint): [bigint, bigint];
192218
export function divRem(x: bigint, y: bigint, out: FSharpRef<bigint>): bigint;
193219
export function divRem(x: bigint, y: bigint, out?: FSharpRef<bigint>): bigint | [bigint, bigint] {
194-
const div = x / y;
195-
const rem = x % y;
196-
if (out === void 0) {
197-
return [div, rem];
198-
} else {
199-
out.contents = rem;
200-
return div;
201-
}
220+
const div = x / y;
221+
const rem = x % y;
222+
if (out === void 0) {
223+
return [div, rem];
224+
} else {
225+
out.contents = rem;
226+
return div;
227+
}
202228
}
203229

204230
export function greatestCommonDivisor(x: bigint, y: bigint): bigint {

src/fable-library-ts/Decimal.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import Decimal, { BigSource } from "./lib/big.js";
22
import { Numeric, symbol } from "./Numeric.js";
33
import { FSharpRef } from "./Types.js";
44
import { combineHashCodes } from "./Util.js";
5+
import { int8, uint8, int16, uint16, int32, uint32, float16, float32, float64 } from "./Int32.js";
6+
import { fromDecimal, int64, uint64, int128, uint128, nativeint, unativeint } from "./BigInt.js";
7+
import * as bigInt from "./BigInt.js";
58

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

142-
export function toNumber(x: Decimal) {
145+
export function toNumber(x: Decimal): number {
143146
return +x;
144147
}
145148

149+
export function toInt8(x: Decimal): int8 { return bigInt.toInt8(fromDecimal(x)); }
150+
export function toUInt8(x: Decimal): uint8 { return bigInt.toUInt8(fromDecimal(x)); }
151+
export function toInt16(x: Decimal): int16 { return bigInt.toInt16(fromDecimal(x)); }
152+
export function toUInt16(x: Decimal): uint16 { return bigInt.toUInt16(fromDecimal(x)); }
153+
export function toInt32(x: Decimal): int32 { return bigInt.toInt32(fromDecimal(x)); }
154+
export function toUInt32(x: Decimal): uint32 { return bigInt.toUInt32(fromDecimal(x)); }
155+
export function toInt64(x: Decimal): int64 { return bigInt.toInt64(fromDecimal(x)); }
156+
export function toUInt64(x: Decimal): uint64 { return bigInt.toUInt64(fromDecimal(x)); }
157+
export function toInt128(x: Decimal): int128 { return bigInt.toInt128(fromDecimal(x)); }
158+
export function toUInt128(x: Decimal): uint128 { return bigInt.toUInt128(fromDecimal(x)); }
159+
export function toNativeInt(x: Decimal): nativeint { return bigInt.toNativeInt(fromDecimal(x)); }
160+
export function toUNativeInt(x: Decimal): unativeint { return bigInt.toUNativeInt(fromDecimal(x)); }
161+
162+
export function toFloat16(x: Decimal): float16 { return toNumber(x); }
163+
export function toFloat32(x: Decimal): float32 { return toNumber(x); }
164+
export function toFloat64(x: Decimal): float64 { return toNumber(x); }
165+
146166
function decimalToHex(dec: Uint8Array, bitSize: number) {
147167
const hex = new Uint8Array(bitSize / 4 | 0);
148168
let hexCount = 1;

0 commit comments

Comments
 (0)