@@ -2,6 +2,11 @@ import { addHashCode, hashCodeInit, hashString } from "collection-utils";
22
33import { inferTransformedStringTypeKindForString } from "../attributes/StringTypes.js" ;
44import type { DateTimeRecognizer } from "../DateTime.js" ;
5+ import {
6+ INT64_RANGE ,
7+ type IntegerRange ,
8+ integerStringInRange ,
9+ } from "../support/IntegerRange.js" ;
510import { assert , defined , panic } from "../support/Support.js" ;
611import {
712 type TransformedStringTypeKind ,
@@ -66,13 +71,50 @@ export abstract class CompressedJSON<T> {
6671
6772 private readonly _arrays : Value [ ] [ ] = [ ] ;
6873
74+ /**
75+ * `supportedIntegerRange` is the range of whole numbers in the input
76+ * that get inferred as `integer`; whole numbers outside it are inferred
77+ * as `double`, because the target language's integer type could not
78+ * round-trip them. `null` means the target's integers are
79+ * arbitrary-precision. See `TargetLanguage.getSupportedIntegerRange`.
80+ */
6981 public constructor (
7082 public readonly dateTimeRecognizer : DateTimeRecognizer ,
7183 public readonly handleRefs : boolean ,
84+ public readonly supportedIntegerRange : IntegerRange | null = INT64_RANGE ,
7285 ) { }
7386
7487 public abstract parse ( input : T ) : Promise < Value > ;
7588
89+ /**
90+ * Whether a whole number in the input, given as the decimal string of
91+ * its JSON literal, fits `supportedIntegerRange`. Works on the digit
92+ * string because such literals can exceed what a JavaScript number can
93+ * represent exactly.
94+ */
95+ protected integerStringFits ( integerString : string ) : boolean {
96+ const range = this . supportedIntegerRange ;
97+ if ( range === null ) return true ;
98+ return integerStringInRange ( integerString , range ) ;
99+ }
100+
101+ /**
102+ * Whether a number that `JSON.parse` produced should be inferred as
103+ * `double`. The original literal is gone at this point, but for whole
104+ * numbers below 1e21, `toFixed(0)` gives the exact decimal value of the
105+ * double, and it errs on the right side at range boundaries: a literal
106+ * like 9223372036854775807 (INT64_MAX) parses to the double
107+ * 9223372036854775808, which is correctly outside the int64 range. At
108+ * 1e21 doubles are far beyond any fixed-size integer type and `toFixed`
109+ * switches to exponential notation, so those are doubles outright.
110+ */
111+ protected parsedNumberIsDouble ( n : number ) : boolean {
112+ if ( n !== Math . floor ( n ) ) return true ;
113+ if ( this . supportedIntegerRange === null ) return false ;
114+ if ( Math . abs ( n ) >= 1e21 ) return true ;
115+ return ! this . integerStringFits ( n . toFixed ( 0 ) ) ;
116+ }
117+
76118 public parseSync ( _input : T ) : Value {
77119 return panic ( "parseSync not implemented in CompressedJSON" ) ;
78120 }
@@ -340,11 +382,7 @@ export class CompressedJSONFromString extends CompressedJSON<string> {
340382 } else if ( typeof json === "string" ) {
341383 this . commitString ( json ) ;
342384 } else if ( typeof json === "number" ) {
343- const isDouble =
344- json !== Math . floor ( json ) ||
345- json < Number . MIN_SAFE_INTEGER ||
346- json > Number . MAX_SAFE_INTEGER ;
347- this . commitNumber ( isDouble ) ;
385+ this . commitNumber ( this . parsedNumberIsDouble ( json ) ) ;
348386 } else if ( Array . isArray ( json ) ) {
349387 this . pushArrayContext ( ) ;
350388 for ( const v of json ) {
0 commit comments