Integer parsing accepts several inputs that .NET rejects with FormatException. These are all cases where Fable is more permissive than .NET, so unlike #4823 nothing crashes and no valid program misbehaves — but the targets disagree with .NET, and with each other in one case.
Splitting this out of the work on #4823/#4825: the neighbouring bugs where Fable was less capable than .NET (int " 0x11" and int "-0x11" failing to parse) are being fixed separately. These remaining ones are left alone deliberately, because tightening them could break code that currently works, so it seemed worth a maintainer decision rather than a drive-by change.
Divergences
All values below were measured, not inferred — .NET via dotnet fsi, Python via fable_library.core, JS via temp/fable-library-js/Int32.js.
| Input |
.NET |
Python |
JS/TS |
Int32.Parse("-1F", NumberStyles.HexNumber) |
FormatException |
-31 |
-31 |
Int32.Parse("0x1F", NumberStyles.HexNumber) |
FormatException |
31 |
31 |
int "+0x11" |
FormatException |
ValueError ✅ |
17 ❌ |
- Sign accepted with
HexNumber. .NET's NumberStyles.HexNumber does not include AllowLeadingSign, so a sign is a format error. Both targets accept it.
0x prefix accepted with HexNumber. .NET never accepts a 0x prefix in Int32.Parse at all — the prefix forms are an FSharp.Core feature of int/int64/etc. on strings, not a BCL one. Both targets strip the prefix whenever the radix is non-decimal, so it is accepted here too.
+ before a radix specifier. FSharp.Core consumes only - before looking for the 0x/0o/0b specifier, so int "+0x11" is a format error on .NET while int "+11" is 11. Python matches .NET; JS does not — its regex (^\s*([\+\-])?(0[xXoObB])?([0-9a-fA-F]+)\s*$, src/fable-library-ts/Int32.ts) allows either sign before the prefix group.
Reproduction
open System
open System.Globalization
printfn "%A" (try box (Int32.Parse("-1F", NumberStyles.HexNumber)) with e -> box e.GetType().Name)
printfn "%A" (try box (Int32.Parse("0x1F", NumberStyles.HexNumber)) with e -> box e.GetType().Name)
printfn "%A" (try box (int "+0x11") with e -> box e.GetType().Name)
dotnet run # FormatException, FormatException, FormatException
dotnet fable Repro.fsproj --lang python -o out && python out/program.py
dotnet fable Repro.fsproj --lang javascript -o out && node out/program.js
Where the behavior lives
- Python —
remove_prefix in src/fable-library-py/src/ints.rs strips a 0x/0o/0b prefix whenever radix != 10, without consulting whether the radix came from AllowHexSpecifier or from the prefix itself. The sign is likewise not checked against the style flags.
- JS/TS —
isValid in src/fable-library-ts/Int32.ts matches sign and prefix with one regex regardless of style, then getRadix returns 16 for AllowHexSpecifier without rejecting a prefix that was also present.
Making either strict would mean threading the style flags into the prefix/sign handling so that AllowHexSpecifier rejects both, and restricting the pre-specifier sign to -.
Scope checked
Verified on Python and JS/TS (which share the fable-library-ts implementation). Rust, Dart, PHP, and BEAM were not checked — they have separate parsing implementations and may or may not have the same gaps.
Worth noting
Fixing these is a behavioral tightening: code that currently parses "0x1F" with HexNumber would start throwing. Might be worth confirming the desired semantics (strict .NET parity vs. current lenience) before changing anything, and it may be reasonable to close this as intended behavior.
Integer parsing accepts several inputs that .NET rejects with
FormatException. These are all cases where Fable is more permissive than .NET, so unlike #4823 nothing crashes and no valid program misbehaves — but the targets disagree with .NET, and with each other in one case.Splitting this out of the work on #4823/#4825: the neighbouring bugs where Fable was less capable than .NET (
int " 0x11"andint "-0x11"failing to parse) are being fixed separately. These remaining ones are left alone deliberately, because tightening them could break code that currently works, so it seemed worth a maintainer decision rather than a drive-by change.Divergences
All values below were measured, not inferred — .NET via
dotnet fsi, Python viafable_library.core, JS viatemp/fable-library-js/Int32.js.Int32.Parse("-1F", NumberStyles.HexNumber)FormatException-31-31Int32.Parse("0x1F", NumberStyles.HexNumber)FormatException3131int "+0x11"FormatExceptionValueError✅17❌HexNumber. .NET'sNumberStyles.HexNumberdoes not includeAllowLeadingSign, so a sign is a format error. Both targets accept it.0xprefix accepted withHexNumber. .NET never accepts a0xprefix inInt32.Parseat all — the prefix forms are an FSharp.Core feature ofint/int64/etc. on strings, not a BCL one. Both targets strip the prefix whenever the radix is non-decimal, so it is accepted here too.+before a radix specifier. FSharp.Core consumes only-before looking for the0x/0o/0bspecifier, soint "+0x11"is a format error on .NET whileint "+11"is11. Python matches .NET; JS does not — its regex (^\s*([\+\-])?(0[xXoObB])?([0-9a-fA-F]+)\s*$,src/fable-library-ts/Int32.ts) allows either sign before the prefix group.Reproduction
Where the behavior lives
remove_prefixinsrc/fable-library-py/src/ints.rsstrips a0x/0o/0bprefix wheneverradix != 10, without consulting whether the radix came fromAllowHexSpecifieror from the prefix itself. The sign is likewise not checked against the style flags.isValidinsrc/fable-library-ts/Int32.tsmatches sign and prefix with one regex regardless of style, thengetRadixreturns 16 forAllowHexSpecifierwithout rejecting a prefix that was also present.Making either strict would mean threading the style flags into the prefix/sign handling so that
AllowHexSpecifierrejects both, and restricting the pre-specifier sign to-.Scope checked
Verified on Python and JS/TS (which share the
fable-library-tsimplementation). Rust, Dart, PHP, and BEAM were not checked — they have separate parsing implementations and may or may not have the same gaps.Worth noting
Fixing these is a behavioral tightening: code that currently parses
"0x1F"withHexNumberwould start throwing. Might be worth confirming the desired semantics (strict .NET parity vs. current lenience) before changing anything, and it may be reasonable to close this as intended behavior.