Skip to content

Commit ec8255d

Browse files
authored
Merge branch 'main' into repo-assist/improve-nameutils-alloc-fastpath-2026-04-13-5985c2920c3bc39b
2 parents 8bbff8a + a1ee141 commit ec8255d

2 files changed

Lines changed: 21 additions & 11 deletions

File tree

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 8.1.8 - Apr 13 2026
44

55
- Code: `NameUtils.niceCamelName` now short-circuits with zero allocations when the result already starts with a lower-case letter; the general case now uses `StringBuilder.Append(string, int, int)` to avoid creating an intermediate `Substring`. `capitalizeFirstLetter` similarly short-circuits when the first letter is already upper-case. Also fixes `Pluralizer` to avoid `Substring(0,1)` in its capitalize path.
6+
- Performance: JSON parser avoids one `Substring` allocation per `\uXXXX` escape by directly indexing into the source string; also uses span-based `Decimal.TryParse`/`Double.TryParse` for number tokens on .NET 8 to avoid a `Substring` allocation per number
67

78
## 8.1.7 - Apr 7 2026
89

src/FSharp.Data.Json.Core/JsonValue.fs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -349,24 +349,21 @@ type private JsonParser(jsonText: string) =
349349
| 'u' ->
350350
ensure (i + 5 < s.Length)
351351

352-
let hexdigit d =
352+
let inline hexdigit (d: char) =
353353
if d >= '0' && d <= '9' then int32 d - int32 '0'
354354
elif d >= 'a' && d <= 'f' then int32 d - int32 'a' + 10
355355
elif d >= 'A' && d <= 'F' then int32 d - int32 'A' + 10
356356
else failwith "hexdigit"
357357

358-
let unicodeChar (s: string) =
359-
if s.Length <> 4 then
360-
failwith "unicodeChar"
361-
358+
// Direct indexing avoids a Substring allocation per \uXXXX escape.
359+
let ch =
362360
char (
363-
hexdigit s.[0] * 4096
364-
+ hexdigit s.[1] * 256
365-
+ hexdigit s.[2] * 16
366-
+ hexdigit s.[3]
361+
hexdigit s.[i + 2] * 4096
362+
+ hexdigit s.[i + 3] * 256
363+
+ hexdigit s.[i + 4] * 16
364+
+ hexdigit s.[i + 5]
367365
)
368366

369-
let ch = unicodeChar (s.Substring(i + 2, 4))
370367
buf.Append(ch) |> ignore
371368
i <- i + 4 // the \ and u will also be skipped past further below
372369
| 'U' ->
@@ -376,7 +373,7 @@ type private JsonParser(jsonText: string) =
376373
if s.Length <> 8 then
377374
failwithf "unicodeChar (%O)" s
378375

379-
if s.[0..1] <> "00" then
376+
if s.[0] <> '0' || s.[1] <> '0' then
380377
failwithf "unicodeChar (%O)" s
381378

382379
UnicodeHelper.getUnicodeSurrogatePair
@@ -408,6 +405,7 @@ type private JsonParser(jsonText: string) =
408405
i <- i + 1
409406

410407
let len = i - start
408+
#if NETSTANDARD2_0
411409
let sub = s.Substring(start, len)
412410

413411
match TextConversions.AsDecimal CultureInfo.InvariantCulture sub with
@@ -416,6 +414,17 @@ type private JsonParser(jsonText: string) =
416414
match TextConversions.AsFloat [||] false CultureInfo.InvariantCulture sub with
417415
| Some x -> JsonValue.Float x
418416
| _ -> throw ()
417+
#else
418+
// Span-based parsing avoids a Substring allocation per number token.
419+
let span = s.AsSpan(start, len)
420+
421+
match Decimal.TryParse(span, NumberStyles.Currency, CultureInfo.InvariantCulture) with
422+
| true, x -> JsonValue.Number x
423+
| false, _ ->
424+
match Double.TryParse(span, NumberStyles.Any, CultureInfo.InvariantCulture) with
425+
| true, x -> JsonValue.Float x
426+
| false, _ -> throw ()
427+
#endif
419428

420429
and parsePair cont =
421430
let key = parseString ()

0 commit comments

Comments
 (0)