From 29e8f120925407f70da17f0189337febacce5f13 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 10 Sep 2026 16:31:31 +0800 Subject: [PATCH] [Java] Accelerate JSON floating-point conversion with integer bounds --- .agents/languages/java.md | 4 + .../apache/fory/json/codec/ArrayCodec.java | 19 +- .../fory/json/reader/DecimalPowers.java | 247 ++++++ .../apache/fory/json/reader/JsonReader.java | 765 ++++-------------- .../apache/fory/json/JsonContainerTest.java | 13 + .../org/apache/fory/json/JsonScalarTest.java | 25 + 6 files changed, 433 insertions(+), 640 deletions(-) create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/reader/DecimalPowers.java diff --git a/.agents/languages/java.md b/.agents/languages/java.md index d1005216a1..9277920f39 100644 --- a/.agents/languages/java.md +++ b/.agents/languages/java.md @@ -190,6 +190,10 @@ Load this file when changing anything under `java/` or when Java drives a cross- at least the requested capacity. Callers must reject invalid or overflowed requests before the call, but must not recheck the allocator postcondition afterward; fix a violating allocator at the allocator implementation. +- Fory JSON floating-point parsing may materialize uncommon numeric tokens for JDK conversion + when that improves measured performance. Preserve direct compact-decimal paths and JSON grammar + validation; use `Float.parseFloat` for float fallbacks to avoid double rounding. Do not restore + expensive decimal-boundary construction merely to eliminate temporary allocations. - In JDK 25 Fory JSON C2-sensitive code, preserve measured, naturally large hot-method boundaries. A method that exceeds HotSpot's 325-byte hot-inline limit through real representation, scalar, array, collection, or generated-schema work is an independent subtree owner. Generated group diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 6372628f05..e4fd1d6384 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -1620,10 +1620,10 @@ public float[] readLatin1(Latin1JsonReader reader) { finishPrimitiveArray(reader, 0, Float.BYTES); return new float[0]; } + // The primitive token reader already rejects null; probing it again rescans every element. float[] values = new float[8]; int size = 0; do { - rejectNull(reader); if ((size & ARRAY_BATCH_MASK) == ARRAY_BATCH_MASK) { reader.reserveGraphMemory(ARRAY_BATCH_SIZE * Float.BYTES); } @@ -1633,7 +1633,7 @@ public float[] readLatin1(Latin1JsonReader reader) { values[size++] = reader.readNextFloatValue(); } while (reader.consumeNextCommaOrEndArray()); finishPrimitiveArray(reader, size, Float.BYTES); - return Arrays.copyOf(values, size); + return size == values.length ? values : Arrays.copyOf(values, size); } @Override @@ -1650,7 +1650,6 @@ public float[] readUtf16(Utf16JsonReader reader) { float[] values = new float[8]; int size = 0; do { - rejectNull(reader); if ((size & ARRAY_BATCH_MASK) == ARRAY_BATCH_MASK) { reader.reserveGraphMemory(ARRAY_BATCH_SIZE * Float.BYTES); } @@ -1660,7 +1659,7 @@ public float[] readUtf16(Utf16JsonReader reader) { values[size++] = reader.readNextFloatValue(); } while (reader.consumeNextCommaOrEndArray()); finishPrimitiveArray(reader, size, Float.BYTES); - return Arrays.copyOf(values, size); + return size == values.length ? values : Arrays.copyOf(values, size); } @Override @@ -1677,7 +1676,6 @@ public float[] readUtf8(Utf8JsonReader reader) { float[] values = new float[8]; int size = 0; do { - rejectNull(reader); if ((size & ARRAY_BATCH_MASK) == ARRAY_BATCH_MASK) { reader.reserveGraphMemory(ARRAY_BATCH_SIZE * Float.BYTES); } @@ -1687,7 +1685,7 @@ public float[] readUtf8(Utf8JsonReader reader) { values[size++] = reader.readNextFloatValue(); } while (reader.consumeNextCommaOrEndArray()); finishPrimitiveArray(reader, size, Float.BYTES); - return Arrays.copyOf(values, size); + return size == values.length ? values : Arrays.copyOf(values, size); } } @@ -1742,7 +1740,6 @@ public double[] readLatin1(Latin1JsonReader reader) { double[] values = new double[8]; int size = 0; do { - rejectNull(reader); if ((size & ARRAY_BATCH_MASK) == ARRAY_BATCH_MASK) { reader.reserveGraphMemory(ARRAY_BATCH_SIZE * Double.BYTES); } @@ -1752,7 +1749,7 @@ public double[] readLatin1(Latin1JsonReader reader) { values[size++] = reader.readNextDoubleValue(); } while (reader.consumeNextCommaOrEndArray()); finishPrimitiveArray(reader, size, Double.BYTES); - return Arrays.copyOf(values, size); + return size == values.length ? values : Arrays.copyOf(values, size); } @Override @@ -1769,7 +1766,6 @@ public double[] readUtf16(Utf16JsonReader reader) { double[] values = new double[8]; int size = 0; do { - rejectNull(reader); if ((size & ARRAY_BATCH_MASK) == ARRAY_BATCH_MASK) { reader.reserveGraphMemory(ARRAY_BATCH_SIZE * Double.BYTES); } @@ -1779,7 +1775,7 @@ public double[] readUtf16(Utf16JsonReader reader) { values[size++] = reader.readNextDoubleValue(); } while (reader.consumeNextCommaOrEndArray()); finishPrimitiveArray(reader, size, Double.BYTES); - return Arrays.copyOf(values, size); + return size == values.length ? values : Arrays.copyOf(values, size); } @Override @@ -1796,7 +1792,6 @@ public double[] readUtf8(Utf8JsonReader reader) { double[] values = new double[8]; int size = 0; do { - rejectNull(reader); if ((size & ARRAY_BATCH_MASK) == ARRAY_BATCH_MASK) { reader.reserveGraphMemory(ARRAY_BATCH_SIZE * Double.BYTES); } @@ -1806,7 +1801,7 @@ public double[] readUtf8(Utf8JsonReader reader) { values[size++] = reader.readNextDoubleValue(); } while (reader.consumeNextCommaOrEndArray()); finishPrimitiveArray(reader, size, Double.BYTES); - return Arrays.copyOf(values, size); + return size == values.length ? values : Arrays.copyOf(values, size); } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/DecimalPowers.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/DecimalPowers.java new file mode 100644 index 0000000000..34aa672427 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/DecimalPowers.java @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.json.reader; + +/** Normalized lower bounds for decimal powers used by binary floating-point conversion. */ +final class DecimalPowers { + private DecimalPowers() {} + + // Entry e + 342 is floor(10^e * 2^(63 - floor(log2(10^e)))), for -342 <= e <= 308. + // The constants are derived from exact integer powers and division, not rounded floating values. + static final long[] MANTISSAS = { + 0xeef453d6923bd65aL, 0x9558b4661b6565f8L, 0xbaaee17fa23ebf76L, + 0xe95a99df8ace6f53L, 0x91d8a02bb6c10594L, 0xb64ec836a47146f9L, + 0xe3e27a444d8d98b7L, 0x8e6d8c6ab0787f72L, 0xb208ef855c969f4fL, + 0xde8b2b66b3bc4723L, 0x8b16fb203055ac76L, 0xaddcb9e83c6b1793L, + 0xd953e8624b85dd78L, 0x87d4713d6f33aa6bL, 0xa9c98d8ccb009506L, + 0xd43bf0effdc0ba48L, 0x84a57695fe98746dL, 0xa5ced43b7e3e9188L, + 0xcf42894a5dce35eaL, 0x818995ce7aa0e1b2L, 0xa1ebfb4219491a1fL, + 0xca66fa129f9b60a6L, 0xfd00b897478238d0L, 0x9e20735e8cb16382L, + 0xc5a890362fddbc62L, 0xf712b443bbd52b7bL, 0x9a6bb0aa55653b2dL, + 0xc1069cd4eabe89f8L, 0xf148440a256e2c76L, 0x96cd2a865764dbcaL, + 0xbc807527ed3e12bcL, 0xeba09271e88d976bL, 0x93445b8731587ea3L, + 0xb8157268fdae9e4cL, 0xe61acf033d1a45dfL, 0x8fd0c16206306babL, + 0xb3c4f1ba87bc8696L, 0xe0b62e2929aba83cL, 0x8c71dcd9ba0b4925L, + 0xaf8e5410288e1b6fL, 0xdb71e91432b1a24aL, 0x892731ac9faf056eL, + 0xab70fe17c79ac6caL, 0xd64d3d9db981787dL, 0x85f0468293f0eb4eL, + 0xa76c582338ed2621L, 0xd1476e2c07286faaL, 0x82cca4db847945caL, + 0xa37fce126597973cL, 0xcc5fc196fefd7d0cL, 0xff77b1fcbebcdc4fL, + 0x9faacf3df73609b1L, 0xc795830d75038c1dL, 0xf97ae3d0d2446f25L, + 0x9becce62836ac577L, 0xc2e801fb244576d5L, 0xf3a20279ed56d48aL, + 0x9845418c345644d6L, 0xbe5691ef416bd60cL, 0xedec366b11c6cb8fL, + 0x94b3a202eb1c3f39L, 0xb9e08a83a5e34f07L, 0xe858ad248f5c22c9L, + 0x91376c36d99995beL, 0xb58547448ffffb2dL, 0xe2e69915b3fff9f9L, + 0x8dd01fad907ffc3bL, 0xb1442798f49ffb4aL, 0xdd95317f31c7fa1dL, + 0x8a7d3eef7f1cfc52L, 0xad1c8eab5ee43b66L, 0xd863b256369d4a40L, + 0x873e4f75e2224e68L, 0xa90de3535aaae202L, 0xd3515c2831559a83L, + 0x8412d9991ed58091L, 0xa5178fff668ae0b6L, 0xce5d73ff402d98e3L, + 0x80fa687f881c7f8eL, 0xa139029f6a239f72L, 0xc987434744ac874eL, + 0xfbe9141915d7a922L, 0x9d71ac8fada6c9b5L, 0xc4ce17b399107c22L, + 0xf6019da07f549b2bL, 0x99c102844f94e0fbL, 0xc0314325637a1939L, + 0xf03d93eebc589f88L, 0x96267c7535b763b5L, 0xbbb01b9283253ca2L, + 0xea9c227723ee8bcbL, 0x92a1958a7675175fL, 0xb749faed14125d36L, + 0xe51c79a85916f484L, 0x8f31cc0937ae58d2L, 0xb2fe3f0b8599ef07L, + 0xdfbdcece67006ac9L, 0x8bd6a141006042bdL, 0xaecc49914078536dL, + 0xda7f5bf590966848L, 0x888f99797a5e012dL, 0xaab37fd7d8f58178L, + 0xd5605fcdcf32e1d6L, 0x855c3be0a17fcd26L, 0xa6b34ad8c9dfc06fL, + 0xd0601d8efc57b08bL, 0x823c12795db6ce57L, 0xa2cb1717b52481edL, + 0xcb7ddcdda26da268L, 0xfe5d54150b090b02L, 0x9efa548d26e5a6e1L, + 0xc6b8e9b0709f109aL, 0xf867241c8cc6d4c0L, 0x9b407691d7fc44f8L, + 0xc21094364dfb5636L, 0xf294b943e17a2bc4L, 0x979cf3ca6cec5b5aL, + 0xbd8430bd08277231L, 0xece53cec4a314ebdL, 0x940f4613ae5ed136L, + 0xb913179899f68584L, 0xe757dd7ec07426e5L, 0x9096ea6f3848984fL, + 0xb4bca50b065abe63L, 0xe1ebce4dc7f16dfbL, 0x8d3360f09cf6e4bdL, + 0xb080392cc4349decL, 0xdca04777f541c567L, 0x89e42caaf9491b60L, + 0xac5d37d5b79b6239L, 0xd77485cb25823ac7L, 0x86a8d39ef77164bcL, + 0xa8530886b54dbdebL, 0xd267caa862a12d66L, 0x8380dea93da4bc60L, + 0xa46116538d0deb78L, 0xcd795be870516656L, 0x806bd9714632dff6L, + 0xa086cfcd97bf97f3L, 0xc8a883c0fdaf7df0L, 0xfad2a4b13d1b5d6cL, + 0x9cc3a6eec6311a63L, 0xc3f490aa77bd60fcL, 0xf4f1b4d515acb93bL, + 0x991711052d8bf3c5L, 0xbf5cd54678eef0b6L, 0xef340a98172aace4L, + 0x9580869f0e7aac0eL, 0xbae0a846d2195712L, 0xe998d258869facd7L, + 0x91ff83775423cc06L, 0xb67f6455292cbf08L, 0xe41f3d6a7377eecaL, + 0x8e938662882af53eL, 0xb23867fb2a35b28dL, 0xdec681f9f4c31f31L, + 0x8b3c113c38f9f37eL, 0xae0b158b4738705eL, 0xd98ddaee19068c76L, + 0x87f8a8d4cfa417c9L, 0xa9f6d30a038d1dbcL, 0xd47487cc8470652bL, + 0x84c8d4dfd2c63f3bL, 0xa5fb0a17c777cf09L, 0xcf79cc9db955c2ccL, + 0x81ac1fe293d599bfL, 0xa21727db38cb002fL, 0xca9cf1d206fdc03bL, + 0xfd442e4688bd304aL, 0x9e4a9cec15763e2eL, 0xc5dd44271ad3cdbaL, + 0xf7549530e188c128L, 0x9a94dd3e8cf578b9L, 0xc13a148e3032d6e7L, + 0xf18899b1bc3f8ca1L, 0x96f5600f15a7b7e5L, 0xbcb2b812db11a5deL, + 0xebdf661791d60f56L, 0x936b9fcebb25c995L, 0xb84687c269ef3bfbL, + 0xe65829b3046b0afaL, 0x8ff71a0fe2c2e6dcL, 0xb3f4e093db73a093L, + 0xe0f218b8d25088b8L, 0x8c974f7383725573L, 0xafbd2350644eeacfL, + 0xdbac6c247d62a583L, 0x894bc396ce5da772L, 0xab9eb47c81f5114fL, + 0xd686619ba27255a2L, 0x8613fd0145877585L, 0xa798fc4196e952e7L, + 0xd17f3b51fca3a7a0L, 0x82ef85133de648c4L, 0xa3ab66580d5fdaf5L, + 0xcc963fee10b7d1b3L, 0xffbbcfe994e5c61fL, 0x9fd561f1fd0f9bd3L, + 0xc7caba6e7c5382c8L, 0xf9bd690a1b68637bL, 0x9c1661a651213e2dL, + 0xc31bfa0fe5698db8L, 0xf3e2f893dec3f126L, 0x986ddb5c6b3a76b7L, + 0xbe89523386091465L, 0xee2ba6c0678b597fL, 0x94db483840b717efL, + 0xba121a4650e4ddebL, 0xe896a0d7e51e1566L, 0x915e2486ef32cd60L, + 0xb5b5ada8aaff80b8L, 0xe3231912d5bf60e6L, 0x8df5efabc5979c8fL, + 0xb1736b96b6fd83b3L, 0xddd0467c64bce4a0L, 0x8aa22c0dbef60ee4L, + 0xad4ab7112eb3929dL, 0xd89d64d57a607744L, 0x87625f056c7c4a8bL, + 0xa93af6c6c79b5d2dL, 0xd389b47879823479L, 0x843610cb4bf160cbL, + 0xa54394fe1eedb8feL, 0xce947a3da6a9273eL, 0x811ccc668829b887L, + 0xa163ff802a3426a8L, 0xc9bcff6034c13052L, 0xfc2c3f3841f17c67L, + 0x9d9ba7832936edc0L, 0xc5029163f384a931L, 0xf64335bcf065d37dL, + 0x99ea0196163fa42eL, 0xc06481fb9bcf8d39L, 0xf07da27a82c37088L, + 0x964e858c91ba2655L, 0xbbe226efb628afeaL, 0xeadab0aba3b2dbe5L, + 0x92c8ae6b464fc96fL, 0xb77ada0617e3bbcbL, 0xe55990879ddcaabdL, + 0x8f57fa54c2a9eab6L, 0xb32df8e9f3546564L, 0xdff9772470297ebdL, + 0x8bfbea76c619ef36L, 0xaefae51477a06b03L, 0xdab99e59958885c4L, + 0x88b402f7fd75539bL, 0xaae103b5fcd2a881L, 0xd59944a37c0752a2L, + 0x857fcae62d8493a5L, 0xa6dfbd9fb8e5b88eL, 0xd097ad07a71f26b2L, + 0x825ecc24c873782fL, 0xa2f67f2dfa90563bL, 0xcbb41ef979346bcaL, + 0xfea126b7d78186bcL, 0x9f24b832e6b0f436L, 0xc6ede63fa05d3143L, + 0xf8a95fcf88747d94L, 0x9b69dbe1b548ce7cL, 0xc24452da229b021bL, + 0xf2d56790ab41c2a2L, 0x97c560ba6b0919a5L, 0xbdb6b8e905cb600fL, + 0xed246723473e3813L, 0x9436c0760c86e30bL, 0xb94470938fa89bceL, + 0xe7958cb87392c2c2L, 0x90bd77f3483bb9b9L, 0xb4ecd5f01a4aa828L, + 0xe2280b6c20dd5232L, 0x8d590723948a535fL, 0xb0af48ec79ace837L, + 0xdcdb1b2798182244L, 0x8a08f0f8bf0f156bL, 0xac8b2d36eed2dac5L, + 0xd7adf884aa879177L, 0x86ccbb52ea94baeaL, 0xa87fea27a539e9a5L, + 0xd29fe4b18e88640eL, 0x83a3eeeef9153e89L, 0xa48ceaaab75a8e2bL, + 0xcdb02555653131b6L, 0x808e17555f3ebf11L, 0xa0b19d2ab70e6ed6L, + 0xc8de047564d20a8bL, 0xfb158592be068d2eL, 0x9ced737bb6c4183dL, + 0xc428d05aa4751e4cL, 0xf53304714d9265dfL, 0x993fe2c6d07b7fabL, + 0xbf8fdb78849a5f96L, 0xef73d256a5c0f77cL, 0x95a8637627989aadL, + 0xbb127c53b17ec159L, 0xe9d71b689dde71afL, 0x9226712162ab070dL, + 0xb6b00d69bb55c8d1L, 0xe45c10c42a2b3b05L, 0x8eb98a7a9a5b04e3L, + 0xb267ed1940f1c61cL, 0xdf01e85f912e37a3L, 0x8b61313bbabce2c6L, + 0xae397d8aa96c1b77L, 0xd9c7dced53c72255L, 0x881cea14545c7575L, + 0xaa242499697392d2L, 0xd4ad2dbfc3d07787L, 0x84ec3c97da624ab4L, + 0xa6274bbdd0fadd61L, 0xcfb11ead453994baL, 0x81ceb32c4b43fcf4L, + 0xa2425ff75e14fc31L, 0xcad2f7f5359a3b3eL, 0xfd87b5f28300ca0dL, + 0x9e74d1b791e07e48L, 0xc612062576589ddaL, 0xf79687aed3eec551L, + 0x9abe14cd44753b52L, 0xc16d9a0095928a27L, 0xf1c90080baf72cb1L, + 0x971da05074da7beeL, 0xbce5086492111aeaL, 0xec1e4a7db69561a5L, + 0x9392ee8e921d5d07L, 0xb877aa3236a4b449L, 0xe69594bec44de15bL, + 0x901d7cf73ab0acd9L, 0xb424dc35095cd80fL, 0xe12e13424bb40e13L, + 0x8cbccc096f5088cbL, 0xafebff0bcb24aafeL, 0xdbe6fecebdedd5beL, + 0x89705f4136b4a597L, 0xabcc77118461cefcL, 0xd6bf94d5e57a42bcL, + 0x8637bd05af6c69b5L, 0xa7c5ac471b478423L, 0xd1b71758e219652bL, + 0x83126e978d4fdf3bL, 0xa3d70a3d70a3d70aL, 0xccccccccccccccccL, + 0x8000000000000000L, 0xa000000000000000L, 0xc800000000000000L, + 0xfa00000000000000L, 0x9c40000000000000L, 0xc350000000000000L, + 0xf424000000000000L, 0x9896800000000000L, 0xbebc200000000000L, + 0xee6b280000000000L, 0x9502f90000000000L, 0xba43b74000000000L, + 0xe8d4a51000000000L, 0x9184e72a00000000L, 0xb5e620f480000000L, + 0xe35fa931a0000000L, 0x8e1bc9bf04000000L, 0xb1a2bc2ec5000000L, + 0xde0b6b3a76400000L, 0x8ac7230489e80000L, 0xad78ebc5ac620000L, + 0xd8d726b7177a8000L, 0x878678326eac9000L, 0xa968163f0a57b400L, + 0xd3c21bcecceda100L, 0x84595161401484a0L, 0xa56fa5b99019a5c8L, + 0xcecb8f27f4200f3aL, 0x813f3978f8940984L, 0xa18f07d736b90be5L, + 0xc9f2c9cd04674edeL, 0xfc6f7c4045812296L, 0x9dc5ada82b70b59dL, + 0xc5371912364ce305L, 0xf684df56c3e01bc6L, 0x9a130b963a6c115cL, + 0xc097ce7bc90715b3L, 0xf0bdc21abb48db20L, 0x96769950b50d88f4L, + 0xbc143fa4e250eb31L, 0xeb194f8e1ae525fdL, 0x92efd1b8d0cf37beL, + 0xb7abc627050305adL, 0xe596b7b0c643c719L, 0x8f7e32ce7bea5c6fL, + 0xb35dbf821ae4f38bL, 0xe0352f62a19e306eL, 0x8c213d9da502de45L, + 0xaf298d050e4395d6L, 0xdaf3f04651d47b4cL, 0x88d8762bf324cd0fL, + 0xab0e93b6efee0053L, 0xd5d238a4abe98068L, 0x85a36366eb71f041L, + 0xa70c3c40a64e6c51L, 0xd0cf4b50cfe20765L, 0x82818f1281ed449fL, + 0xa321f2d7226895c7L, 0xcbea6f8ceb02bb39L, 0xfee50b7025c36a08L, + 0x9f4f2726179a2245L, 0xc722f0ef9d80aad6L, 0xf8ebad2b84e0d58bL, + 0x9b934c3b330c8577L, 0xc2781f49ffcfa6d5L, 0xf316271c7fc3908aL, + 0x97edd871cfda3a56L, 0xbde94e8e43d0c8ecL, 0xed63a231d4c4fb27L, + 0x945e455f24fb1cf8L, 0xb975d6b6ee39e436L, 0xe7d34c64a9c85d44L, + 0x90e40fbeea1d3a4aL, 0xb51d13aea4a488ddL, 0xe264589a4dcdab14L, + 0x8d7eb76070a08aecL, 0xb0de65388cc8ada8L, 0xdd15fe86affad912L, + 0x8a2dbf142dfcc7abL, 0xacb92ed9397bf996L, 0xd7e77a8f87daf7fbL, + 0x86f0ac99b4e8dafdL, 0xa8acd7c0222311bcL, 0xd2d80db02aabd62bL, + 0x83c7088e1aab65dbL, 0xa4b8cab1a1563f52L, 0xcde6fd5e09abcf26L, + 0x80b05e5ac60b6178L, 0xa0dc75f1778e39d6L, 0xc913936dd571c84cL, + 0xfb5878494ace3a5fL, 0x9d174b2dcec0e47bL, 0xc45d1df942711d9aL, + 0xf5746577930d6500L, 0x9968bf6abbe85f20L, 0xbfc2ef456ae276e8L, + 0xefb3ab16c59b14a2L, 0x95d04aee3b80ece5L, 0xbb445da9ca61281fL, + 0xea1575143cf97226L, 0x924d692ca61be758L, 0xb6e0c377cfa2e12eL, + 0xe498f455c38b997aL, 0x8edf98b59a373fecL, 0xb2977ee300c50fe7L, + 0xdf3d5e9bc0f653e1L, 0x8b865b215899f46cL, 0xae67f1e9aec07187L, + 0xda01ee641a708de9L, 0x884134fe908658b2L, 0xaa51823e34a7eedeL, + 0xd4e5e2cdc1d1ea96L, 0x850fadc09923329eL, 0xa6539930bf6bff45L, + 0xcfe87f7cef46ff16L, 0x81f14fae158c5f6eL, 0xa26da3999aef7749L, + 0xcb090c8001ab551cL, 0xfdcb4fa002162a63L, 0x9e9f11c4014dda7eL, + 0xc646d63501a1511dL, 0xf7d88bc24209a565L, 0x9ae757596946075fL, + 0xc1a12d2fc3978937L, 0xf209787bb47d6b84L, 0x9745eb4d50ce6332L, + 0xbd176620a501fbffL, 0xec5d3fa8ce427affL, 0x93ba47c980e98cdfL, + 0xb8a8d9bbe123f017L, 0xe6d3102ad96cec1dL, 0x9043ea1ac7e41392L, + 0xb454e4a179dd1877L, 0xe16a1dc9d8545e94L, 0x8ce2529e2734bb1dL, + 0xb01ae745b101e9e4L, 0xdc21a1171d42645dL, 0x899504ae72497ebaL, + 0xabfa45da0edbde69L, 0xd6f8d7509292d603L, 0x865b86925b9bc5c2L, + 0xa7f26836f282b732L, 0xd1ef0244af2364ffL, 0x8335616aed761f1fL, + 0xa402b9c5a8d3a6e7L, 0xcd036837130890a1L, 0x802221226be55a64L, + 0xa02aa96b06deb0fdL, 0xc83553c5c8965d3dL, 0xfa42a8b73abbf48cL, + 0x9c69a97284b578d7L, 0xc38413cf25e2d70dL, 0xf46518c2ef5b8cd1L, + 0x98bf2f79d5993802L, 0xbeeefb584aff8603L, 0xeeaaba2e5dbf6784L, + 0x952ab45cfa97a0b2L, 0xba756174393d88dfL, 0xe912b9d1478ceb17L, + 0x91abb422ccb812eeL, 0xb616a12b7fe617aaL, 0xe39c49765fdf9d94L, + 0x8e41ade9fbebc27dL, 0xb1d219647ae6b31cL, 0xde469fbd99a05fe3L, + 0x8aec23d680043beeL, 0xada72ccc20054ae9L, 0xd910f7ff28069da4L, + 0x87aa9aff79042286L, 0xa99541bf57452b28L, 0xd3fa922f2d1675f2L, + 0x847c9b5d7c2e09b7L, 0xa59bc234db398c25L, 0xcf02b2c21207ef2eL, + 0x8161afb94b44f57dL, 0xa1ba1ba79e1632dcL, 0xca28a291859bbf93L, + 0xfcb2cb35e702af78L, 0x9defbf01b061adabL, 0xc56baec21c7a1916L, + 0xf6c69a72a3989f5bL, 0x9a3c2087a63f6399L, 0xc0cb28a98fcf3c7fL, + 0xf0fdf2d3f3c30b9fL, 0x969eb7c47859e743L, 0xbc4665b596706114L, + 0xeb57ff22fc0c7959L, 0x9316ff75dd87cbd8L, 0xb7dcbf5354e9beceL, + 0xe5d3ef282a242e81L, 0x8fa475791a569d10L, 0xb38d92d760ec4455L, + 0xe070f78d3927556aL, 0x8c469ab843b89562L, 0xaf58416654a6babbL, + 0xdb2e51bfe9d0696aL, 0x88fcf317f22241e2L, 0xab3c2fddeeaad25aL, + 0xd60b3bd56a5586f1L, 0x85c7056562757456L, 0xa738c6bebb12d16cL, + 0xd106f86e69d785c7L, 0x82a45b450226b39cL, 0xa34d721642b06084L, + 0xcc20ce9bd35c78a5L, 0xff290242c83396ceL, 0x9f79a169bd203e41L, + 0xc75809c42c684dd1L, 0xf92e0c3537826145L, 0x9bbcc7a142b17ccbL, + 0xc2abf989935ddbfeL, 0xf356f7ebf83552feL, 0x98165af37b2153deL, + 0xbe1bf1b059e9a8d6L, 0xeda2ee1c7064130cL, 0x9485d4d1c63e8be7L, + 0xb9a74a0637ce2ee1L, 0xe8111c87c5c1ba99L, 0x910ab1d4db9914a0L, + 0xb54d5e4a127f59c8L, 0xe2a0b5dc971f303aL, 0x8da471a9de737e24L, + 0xb10d8e1456105dadL, 0xdd50f1996b947518L, 0x8a5296ffe33cc92fL, + 0xace73cbfdc0bfb7bL, 0xd8210befd30efa5aL, 0x8714a775e3e95c78L, + 0xa8d9d1535ce3b396L, 0xd31045a8341ca07cL, 0x83ea2b892091e44dL, + 0xa4e4b66b68b65d60L, 0xce1de40642e3f4b9L, 0x80d2ae83e9ce78f3L, + 0xa1075a24e4421730L, 0xc94930ae1d529cfcL, 0xfb9b7cd9a4a7443cL, + 0x9d412e0806e88aa5L, 0xc491798a08a2ad4eL, 0xf5b5d7ec8acb58a2L, + 0x9991a6f3d6bf1765L, 0xbff610b0cc6edd3fL, 0xeff394dcff8a948eL, + 0x95f83d0a1fb69cd9L, 0xbb764c4ca7a4440fL, 0xea53df5fd18d5513L, + 0x92746b9be2f8552cL, 0xb7118682dbb66a77L, 0xe4d5e82392a40515L, + 0x8f05b1163ba6832dL, 0xb2c71d5bca9023f8L, 0xdf78e4b2bd342cf6L, + 0x8bab8eefb6409c1aL, 0xae9672aba3d0c320L, 0xda3c0f568cc4f3e8L, + 0x8865899617fb1871L, 0xaa7eebfb9df9de8dL, 0xd51ea6fa85785631L, + 0x8533285c936b35deL, 0xa67ff273b8460356L, 0xd01fef10a657842cL, + 0x8213f56a67f6b29bL, 0xa298f2c501f45f42L, 0xcb3f2f7642717713L, + 0xfe0efb53d30dd4d7L, 0x9ec95d1463e8a506L, 0xc67bb4597ce2ce48L, + 0xf81aa16fdc1b81daL, 0x9b10a4e5e9913128L, 0xc1d4ce1f63f57d72L, + 0xf24a01a73cf2dccfL, 0x976e41088617ca01L, 0xbd49d14aa79dbc82L, + 0xec9c459d51852ba2L, 0x93e1ab8252f33b45L, 0xb8da1662e7b00a17L, + 0xe7109bfba19c0c9dL, 0x906a617d450187e2L, 0xb484f9dc9641e9daL, + 0xe1a63853bbd26451L, 0x8d07e33455637eb2L, 0xb049dc016abc5e5fL, + 0xdc5c5301c56b75f7L, 0x89b9b3e11b6329baL, 0xac2820d9623bf429L, + 0xd732290fbacaf133L, 0x867f59a9d4bed6c0L, 0xa81f301449ee8c70L, + 0xd226fc195c6a2f8cL, 0x83585d8fd9c25db7L, 0xa42e74f3d032f525L, + 0xcd3a1230c43fb26fL, 0x80444b5e7aa7cf85L, 0xa0555e361951c366L, + 0xc86ab5c39fa63440L, 0xfa856334878fc150L, 0x9c935e00d4b9d8d2L, + 0xc3b8358109e84f07L, 0xf4a642e14c6262c8L, 0x98e7e9cccfbd7dbdL, + 0xbf21e44003acdd2cL, 0xeeea5d5004981478L, 0x95527a5202df0ccbL, + 0xbaa718e68396cffdL, 0xe950df20247c83fdL, 0x91d28b7416cdd27eL, + 0xb6472e511c81471dL, 0xe3d8f9e563a198e5L, 0x8e679c2f5e44ff8fL + }; +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java index aa66788184..9481a32093 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java @@ -54,11 +54,11 @@ * text view. Concrete readers own input storage, string decoding, field-name probes, and direct * primitive numeric fast paths for their representation. * - *

Primitive {@code int}, {@code long}, {@code float}, and {@code double} parsing does not - * materialize a String or arbitrary-precision number. Precision-sensitive floating input uses the - * reusable boundary workspace. The internal length and scale limits apply only when constructing - * {@link BigInteger} or {@link BigDecimal}; raw number text, primitive scans, and skipped values do - * not inherit that resource policy. + *

Primitive numeric common paths parse directly from the input. Floating-point conversion uses + * integer bounds on decimal powers and delegates ambiguous rounding or long mantissas to the JDK. + * The internal length and scale limits apply only when constructing {@link BigInteger} or {@link + * BigDecimal}; raw number text, primitive scans, and skipped values do not inherit that resource + * policy. * *

Declared boolean and numeric scalars accept their native JSON token or the same token text in * quotes without a configuration gate. Quoted common paths consume only the quotes around the @@ -102,8 +102,6 @@ public abstract class JsonReader { private static final int DOUBLE_FRACTION_BITS = 52; private static final long DOUBLE_SIGN_BIT = 0x8000_0000_0000_0000L; private static final long DOUBLE_FRACTION_MASK = (1L << DOUBLE_FRACTION_BITS) - 1; - private static final long DOUBLE_INFINITY_BITS = 0x7ff0_0000_0000_0000L; - private static final long DOUBLE_MAX_FINITE_BITS = 0x7fef_ffff_ffff_ffffL; private static final long[] COMPACT_DOUBLE_MANTISSAS = { 0x8000_0000_0000_0000L, 0xcccc_cccc_cccc_ccccL, @@ -125,7 +123,6 @@ public abstract class JsonReader { 0xb877_aa32_36a4_b449L, 0x9392_ee8e_921d_5d07L }; - private static final int DECIMAL_BOUNDARY_DIGITS = 768; private static final double[] DOUBLE_POWERS_OF_TEN = { 1.0d, 10.0d, @@ -150,8 +147,6 @@ public abstract class JsonReader { private static final int FLOAT_SIGN_BIT = 0x8000_0000; private static final int FLOAT_FRACTION_MASK = (1 << FLOAT_FRACTION_BITS) - 1; private static final int FLOAT_EXPONENT_MASK = 0x7f80_0000; - private static final int FLOAT_INFINITY_BITS = 0x7f80_0000; - private static final int FLOAT_MAX_FINITE_BITS = 0x7f7f_ffff; // Past twice the maximum token digit count, an exponent cannot be canceled back into the // finite float range by integer, fractional, or truncated digits. private static final long TOKEN_EXPONENT_LIMIT = 2L * Integer.MAX_VALUE + 1_000L; @@ -1732,6 +1727,10 @@ protected static float compactFloatValue(boolean negative, long unscaled, int sc if (unscaled == 0) { return negative ? -0.0f : 0.0f; } + long converted = decimalToBinary(unscaled, scale, FLOAT_FRACTION_BITS, -126, 127); + if (converted >= 0) { + return Float.intBitsToFloat((int) converted | (negative ? FLOAT_SIGN_BIT : 0)); + } long divisor = LONG_POWERS_OF_TEN[scale]; float estimate = (float) unscaled / (float) divisor; int bits = correctCompactFloat(unscaled, divisor, Float.floatToRawIntBits(estimate)); @@ -1832,281 +1831,120 @@ private static int doubleBinaryExponent(long bits) { return exponent == 0 ? -1074 : exponent - 1075; } + // Validate JSON grammar before JDK conversion: its accepted syntax also includes non-JSON + // numbers. Materializing the uncommon token avoids the much greater cost of constructing + // decimal midpoint boundaries, while preserving subnormals and exact halfway rounding. protected final double readDoubleFallbackValue(int start) { position = start; - return readDoubleNumberFallback(start); + scanNumberToken(); + return Double.parseDouble(floatingToken(start, position)); } protected final double readDoubleExponentValue( boolean negative, long unscaled, int scale, int start, int exponentOffset) { long adjustedScale = readExponentScale(exponentOffset, scale); - return doubleFromDecimal(negative, unscaled, adjustedScale, false, start, position); + return readScannedDoubleValue(negative, unscaled, adjustedScale, start, position); } protected final double readScannedDoubleValue( boolean negative, long unscaled, long scale, int start, int end) { - return doubleFromDecimal(negative, unscaled, scale, false, start, end); - } - - // Mantissa overflow is the only concrete-reader fallback that rescans a valid token. It keeps - // the first 18 significant digits for a close estimate and compares the original token with - // exact IEEE midpoints, so primitive double parsing never materializes a String or big number. - private double readDoubleNumberFallback(int start) { - int offset = start; - int inputLength = length(); - boolean negative = false; - if (offset < inputLength && charAt(offset) == '-') { - negative = true; - offset++; + if (unscaled == 0) { + return negative ? -0.0d : 0.0d; } - if (offset >= inputLength) { - throw numberError(offset, "Expected double"); + if (scale >= 0 && scale <= COMPACT_DECIMAL_MAX_SCALE) { + return compactDoubleValue(negative, unscaled, (int) scale); } - - int ch = charAt(offset); - long significand = 0; - int storedDigits = 0; - int truncatedDigits = 0; - int fractionDigits = 0; - boolean nonZeroSeen = false; - boolean sticky = false; - if (ch == '0') { - offset++; - if (offset < inputLength) { - ch = charAt(offset); - if (ch >= '0' && ch <= '9') { - throw numberError(offset, "Leading zero in number"); - } + if (scale < 0 && scale >= -COMPACT_DECIMAL_MAX_SCALE) { + long multiplied = multiplyByPowerOfTen(unscaled, (int) -scale); + if (multiplied >= 0) { + return compactDoubleValue(negative, multiplied, 0); } - } else if (ch >= '1' && ch <= '9') { - do { - int digit = ch - '0'; - if (digit != 0 || nonZeroSeen) { - nonZeroSeen = true; - if (storedDigits < 18) { - significand = significand * 10 + digit; - storedDigits++; - } else { - truncatedDigits++; - sticky |= digit != 0; - } - } - offset++; - ch = offset < inputLength ? charAt(offset) : -1; - } while (ch >= '0' && ch <= '9'); - } else { - throw numberError(offset, "Expected double"); } - - if (offset < inputLength && charAt(offset) == '.') { - offset++; - int fractionStart = offset; - while (offset < inputLength) { - ch = charAt(offset); - if (ch < '0' || ch > '9') { - break; - } - int digit = ch - '0'; - if (digit != 0 || nonZeroSeen) { - nonZeroSeen = true; - if (storedDigits < 18) { - significand = significand * 10 + digit; - storedDigits++; - } else { - truncatedDigits++; - sticky |= digit != 0; - } - } - fractionDigits++; - offset++; - } - if (offset == fractionStart) { - throw numberError(offset, "Expected digit"); - } - } - - long exponent = 0; - if (offset < inputLength) { - ch = charAt(offset); - if (ch == 'e' || ch == 'E') { - offset++; - boolean negativeExponent = false; - if (offset < inputLength) { - ch = charAt(offset); - if (ch == '-' || ch == '+') { - negativeExponent = ch == '-'; - offset++; - } - } - int exponentStart = offset; - while (offset < inputLength) { - ch = charAt(offset); - if (ch < '0' || ch > '9') { - break; - } - if (exponent < TOKEN_EXPONENT_LIMIT) { - exponent = exponent * 10 + ch - '0'; - if (exponent > TOKEN_EXPONENT_LIMIT) { - exponent = TOKEN_EXPONENT_LIMIT; - } - } - offset++; - } - if (offset == exponentStart) { - throw numberError(offset, "Expected exponent digit"); - } - if (negativeExponent) { - exponent = -exponent; - } - } + long bits = decimalToBinary(unscaled, scale, DOUBLE_FRACTION_BITS, -1022, 1023); + if (bits < 0) { + return Double.parseDouble(floatingToken(start, end)); } - - position = offset; - if (!nonZeroSeen) { - return negative ? -0.0d : 0.0d; - } - long scale = (long) fractionDigits - exponent - truncatedDigits; - return doubleFromDecimal(negative, significand, scale, sticky, start, offset); + return Double.longBitsToDouble(bits | (negative ? DOUBLE_SIGN_BIT : 0)); } - private double doubleFromDecimal( - boolean negative, long significand, long scale, boolean sticky, int start, int end) { - if (significand == 0) { - return negative ? -0.0d : 0.0d; - } - if (!sticky) { - if (scale >= 0 && scale <= COMPACT_DECIMAL_MAX_SCALE) { - return compactDoubleValue(negative, significand, (int) scale); - } - if (scale < 0 && scale >= -COMPACT_DECIMAL_MAX_SCALE) { - long multiplied = multiplyByPowerOfTen(significand, (int) -scale); - if (multiplied >= 0) { - return compactDoubleValue(negative, multiplied, 0); - } - } - } - double estimate = approximateDouble(significand, scale); - return correctDoubleToken(negative, estimate, start, end); + protected final float readFloatExponentValue( + boolean negative, long unscaled, int scale, int start, int exponentOffset) { + long adjustedScale = readExponentScale(exponentOffset, scale); + return readScannedFloatValue(negative, unscaled, adjustedScale, start, position); } - private static double approximateDouble(long significand, long scale) { - long decimalExponent = -scale; - if (decimalExponent > 308) { - return Double.POSITIVE_INFINITY; + protected final float readScannedFloatValue( + boolean negative, long unscaled, long scale, int start, int end) { + if (unscaled == 0) { + return negative ? -0.0f : 0.0f; } - if (decimalExponent < -342) { - return 0.0d; + if (scale >= 0 && scale <= COMPACT_DECIMAL_MAX_SCALE) { + return compactFloatValue(negative, unscaled, (int) scale); } - double value = (double) significand; - if (decimalExponent > 0) { - return value * Math.pow(10.0d, decimalExponent); + if (scale < 0 && scale >= -COMPACT_DECIMAL_MAX_SCALE) { + long multiplied = multiplyByPowerOfTen(unscaled, (int) -scale); + if (multiplied >= 0) { + return compactFloatValue(negative, multiplied, 0); + } } - if (decimalExponent >= -308) { - return value / Math.pow(10.0d, -decimalExponent); + long bits = decimalToBinary(unscaled, scale, FLOAT_FRACTION_BITS, -126, 127); + if (bits < 0) { + // Parsing as double first would round some halfway values twice. + return Float.parseFloat(floatingToken(start, end)); } - value /= 1.0e308d; - return value / Math.pow(10.0d, -decimalExponent - 308); + return Float.intBitsToFloat((int) bits | (negative ? FLOAT_SIGN_BIT : 0)); } - private double correctDoubleToken(boolean negative, double estimate, int start, int end) { - long bits = Double.doubleToRawLongBits(estimate) & ~DOUBLE_SIGN_BIT; - byte[] boundary = numericWorkspace.decimalBoundaryDigits; - // Eighteen retained digits, one correctly rounded multiply/divide, and Math.pow's one-ULP - // contract keep the estimate within this local window. The exact search is a correctness-only - // fallback and is not expected on valid JDK implementations. - for (int i = 0; i < 4; i++) { - if (bits == DOUBLE_INFINITY_BITS) { - int packed = buildDoubleBoundary(DOUBLE_MAX_FINITE_BITS, DOUBLE_INFINITY_BITS, boundary); - int cmp = compareTokenToBoundary(start, end, boundary, packed >>> 16, packed & 0xffff); - if (cmp < 0) { - bits = DOUBLE_MAX_FINITE_BITS; - continue; - } - return signedDouble(negative, bits); - } - if (bits == 0) { - int packed = buildDoubleBoundary(0, 1, boundary); - int cmp = compareTokenToBoundary(start, end, boundary, packed >>> 16, packed & 0xffff); - if (cmp > 0) { - bits = 1; - continue; - } - return signedDouble(negative, bits); - } - - int packed = buildDoubleBoundary(bits - 1, bits, boundary); - int cmp = compareTokenToBoundary(start, end, boundary, packed >>> 16, packed & 0xffff); - if (cmp < 0 || (cmp == 0 && (bits & 1) != 0)) { - bits--; - continue; - } - - packed = buildDoubleBoundary(bits, bits + 1, boundary); - cmp = compareTokenToBoundary(start, end, boundary, packed >>> 16, packed & 0xffff); - if (cmp > 0 || (cmp == 0 && (bits & 1) != 0)) { - bits++; - continue; - } - return signedDouble(negative, bits); + private static long decimalToBinary( + long unscaled, long scale, int fractionBits, int minExponent, int maxExponent) { + if (scale > 342) { + return 0; } - return signedDouble(negative, exactDoubleTokenBits(start, end, boundary)); - } - - private long exactDoubleTokenBits(int start, int end, byte[] boundary) { - long low = 0; - long high = DOUBLE_INFINITY_BITS; - while (low < high) { - long middle = (low + high) >>> 1; - int packed = buildDoubleBoundary(middle, middle + 1, boundary); - int cmp = compareTokenToBoundary(start, end, boundary, packed >>> 16, packed & 0xffff); - if (cmp < 0) { - high = middle; - } else if (cmp > 0) { - low = middle + 1; - } else { - return (middle & 1) == 0 ? middle : middle + 1; + if (scale < -308) { + return (long) (maxExponent - minExponent + 2) << fractionBits; + } + int decimalExponent = (int) -scale; + int shift = Long.numberOfLeadingZeros(unscaled); + long high = + DecimalMath.unsignedMultiplyHigh( + unscaled << shift, DecimalPowers.MANTISSAS[decimalExponent + 342]); + int binaryExponent = (int) ((decimalExponent * 217706L) >> 16) + 1 - shift; + int leadingZeros = Long.numberOfLeadingZeros(high); + int exponent = 63 - leadingZeros + binaryExponent; + int discardedBits = + Math.max(63 - leadingZeros - fractionBits, minExponent - fractionBits - binaryExponent); + if (discardedBits > 64) { + return 0; + } + // The table rounds its normalized power down by less than one, and the high product drops + // less than one more unit. The exact value is therefore in [high, high + 2) * 2^binaryExponent. + // Return a result only if that whole interval lies on the same side of the rounding midpoint. + // A normalized positive signed-long significand is at most unsigned -2, so high + 2 cannot + // wrap. + if (discardedBits == 64) { + if (Long.compareUnsigned(high, Long.MIN_VALUE) > 0) { + return 1; } + return Long.compareUnsigned(high + 2, Long.MIN_VALUE) < 0 ? 0 : -1; } - return low; - } - - private static double signedDouble(boolean negative, long bits) { - if (negative) { - bits |= DOUBLE_SIGN_BIT; + long remainder = high & ((1L << discardedBits) - 1); + long halfway = 1L << (discardedBits - 1); + if (remainder <= halfway && remainder + 2 >= halfway) { + return -1; } - return Double.longBitsToDouble(bits); - } - - private static int buildDoubleBoundary(long lowBits, long highBits, byte[] digits) { - long numerator; - int binaryExponent; - if (highBits == DOUBLE_INFINITY_BITS) { - numerator = (1L << (DOUBLE_FRACTION_BITS + 2)) - 1; - binaryExponent = 970; - } else { - long lowMantissa = doubleMantissa(lowBits); - int lowExponent = doubleBinaryExponent(lowBits); - long highMantissa = doubleMantissa(highBits); - int highExponent = doubleBinaryExponent(highBits); - int exponent = Math.min(lowExponent, highExponent); - numerator = - (lowMantissa << (lowExponent - exponent)) + (highMantissa << (highExponent - exponent)); - binaryExponent = exponent - 1; + long significand = (high >>> discardedBits) + (remainder > halfway ? 1 : 0); + if (exponent < minExponent) { + return significand; } - int length = writeBoundaryDigits(numerator, binaryExponent, digits); - int scale = binaryExponent < 0 ? -binaryExponent : 0; - return (length << 16) | scale; - } - - protected final float readFloatExponentValue( - boolean negative, long unscaled, int scale, int start, int exponentOffset) { - long adjustedScale = readExponentScale(exponentOffset, scale); - return floatFromDecimal(negative, unscaled, adjustedScale, false, start, position); - } - - protected final float readScannedFloatValue( - boolean negative, long unscaled, long scale, int start, int end) { - return floatFromDecimal(negative, unscaled, scale, false, start, end); + if (significand == (1L << (fractionBits + 1))) { + significand >>>= 1; + exponent++; + } + if (exponent > maxExponent) { + return (long) (maxExponent - minExponent + 2) << fractionBits; + } + return ((long) (exponent - minExponent + 1) << fractionBits) + | (significand & ((1L << fractionBits) - 1)); } private long readExponentScale(int offset, long scale) { @@ -2144,254 +1982,66 @@ private long readExponentScale(int offset, long scale) { protected final float readFloatFallbackValue(int start) { position = start; - return readFloatNumberFallback(start); + scanNumberToken(); + return Float.parseFloat(floatingToken(start, position)); } - // Float fallback remains reader-owned: it must not materialize a number String or construct - // arbitrary-precision numbers. Big number allocation is owned only by BigInteger/BigDecimal. - private float readFloatNumberFallback(int start) { - int offset = start; - int inputLength = length(); - boolean negative = false; - if (offset < inputLength && charAt(offset) == '-') { - negative = true; - offset++; - } - if (offset >= inputLength) { - throw numberError(offset, "Expected float"); + private String floatingToken(int start, int end) { + if (end - start <= 1024) { + return slice(start, end); } + return normalizeFloatingToken(start, end); + } - int ch = charAt(offset); - long significand = 0; - int storedDigits = 0; - int truncatedDigits = 0; + private String normalizeFloatingToken(int start, int end) { + // JDK 8 bounds the parsed exponent using significant digits, excluding leading fractional + // zeros. Move the decimal point after removing those zeros so cancelling exponents remain + // correct. Retain every significant digit: truncation here could change halfway rounding. + boolean negative = charAt(start) == '-'; + int offset = negative ? start + 1 : start; + int first = -1; + int last = -1; int fractionDigits = 0; - boolean nonZeroSeen = false; - boolean sticky = false; - - if (ch == '0') { - offset++; - if (offset < inputLength) { - ch = charAt(offset); - if (ch >= '0' && ch <= '9') { - throw numberError(offset, "Leading zero in number"); - } - } - } else if (ch >= '1' && ch <= '9') { - do { - int digit = ch - '0'; - if (digit != 0 || nonZeroSeen) { - nonZeroSeen = true; - if (storedDigits < 18) { - significand = significand * 10 + digit; - storedDigits++; - } else { - truncatedDigits++; - sticky |= digit != 0; - } - } - offset++; - ch = offset < inputLength ? charAt(offset) : -1; - } while (ch >= '0' && ch <= '9'); - } else { - throw numberError(offset, "Expected float"); - } - - if (offset < inputLength && charAt(offset) == '.') { - offset++; - int fractionStart = offset; - while (offset < inputLength) { - ch = charAt(offset); - if (ch < '0' || ch > '9') { - break; - } - int digit = ch - '0'; - if (digit != 0 || nonZeroSeen) { - nonZeroSeen = true; - if (storedDigits < 18) { - significand = significand * 10 + digit; - storedDigits++; - } else { - truncatedDigits++; - sticky |= digit != 0; - } - } - fractionDigits++; - offset++; - } - if (offset == fractionStart) { - throw numberError(offset, "Expected digit"); - } - } - - long exponent = 0; - if (offset < inputLength) { - ch = charAt(offset); + int trailingZeros = 0; + boolean fraction = false; + while (offset < end) { + char ch = charAt(offset); if (ch == 'e' || ch == 'E') { - offset++; - boolean negativeExponent = false; - if (offset < inputLength) { - ch = charAt(offset); - if (ch == '-' || ch == '+') { - negativeExponent = ch == '-'; - offset++; - } + break; + } + if (ch == '.') { + fraction = true; + } else { + if (fraction) { + fractionDigits++; } - int exponentStart = offset; - while (offset < inputLength) { - ch = charAt(offset); - if (ch < '0' || ch > '9') { - break; - } - if (exponent < TOKEN_EXPONENT_LIMIT) { - exponent = exponent * 10 + ch - '0'; - if (exponent > TOKEN_EXPONENT_LIMIT) { - exponent = TOKEN_EXPONENT_LIMIT; - } + if (ch != '0') { + if (first < 0) { + first = offset; } - offset++; - } - if (offset == exponentStart) { - throw numberError(offset, "Expected exponent digit"); - } - if (negativeExponent) { - exponent = -exponent; - } - } - } - - position = offset; - if (!nonZeroSeen) { - return negative ? -0.0f : 0.0f; - } - long scale = (long) fractionDigits - exponent - truncatedDigits; - return floatFromDecimal(negative, significand, scale, sticky, start, offset); - } - - private float floatFromDecimal( - boolean negative, long significand, long scale, boolean sticky, int start, int end) { - if (significand == 0) { - return negative ? -0.0f : 0.0f; - } - if (!sticky) { - if (scale >= 0 && scale <= COMPACT_DECIMAL_MAX_SCALE) { - return compactFloatValue(negative, significand, (int) scale); - } - if (scale < 0 && scale >= -COMPACT_DECIMAL_MAX_SCALE) { - long multiplied = multiplyByPowerOfTen(significand, (int) -scale); - if (multiplied >= 0) { - return compactFloatValue(negative, multiplied, 0); + last = offset; + trailingZeros = 0; + } else { + trailingZeros++; } } + offset++; } - float result = approximateFloat(negative, significand, scale); - return correctFloatToken(negative, result, start, end); - } - - private static float approximateFloat(boolean negative, long significand, long scale) { - double value = (double) significand; - long decimalExponent = -scale; - if (decimalExponent > 0) { - if (decimalExponent > 50) { - return negative ? Float.NEGATIVE_INFINITY : Float.POSITIVE_INFINITY; - } - value *= Math.pow(10.0d, decimalExponent); - } else if (decimalExponent < 0) { - if (decimalExponent < -350) { - return negative ? -0.0f : 0.0f; - } - value /= Math.pow(10.0d, -decimalExponent); + if (first < 0) { + return negative ? "-0" : "0"; } - float result = (float) value; - return negative ? -result : result; - } - - // Long float tokens can sit within one double ULP of a float midpoint. Correct the close - // estimate against exact adjacent-float boundaries so fallback never needs a number String. - private float correctFloatToken(boolean negative, float estimate, int start, int end) { - int bits = Float.floatToRawIntBits(estimate); - bits &= ~FLOAT_SIGN_BIT; - byte[] boundary = numericWorkspace.decimalBoundaryDigits; - for (int i = 0; i < 4; i++) { - if (bits == FLOAT_INFINITY_BITS) { - int packed = buildFloatBoundary(FLOAT_MAX_FINITE_BITS, FLOAT_INFINITY_BITS, boundary); - int cmp = compareTokenToBoundary(start, end, boundary, packed >>> 16, packed & 0xffff); - if (cmp < 0) { - bits = FLOAT_MAX_FINITE_BITS; - continue; - } - return signedFloat(negative, bits); - } - if (bits == 0) { - int packed = buildFloatBoundary(0, 1, boundary); - int cmp = compareTokenToBoundary(start, end, boundary, packed >>> 16, packed & 0xffff); - if (cmp > 0) { - bits = 1; - continue; - } - return signedFloat(negative, bits); - } - - int packed = buildFloatBoundary(bits - 1, bits, boundary); - int cmp = compareTokenToBoundary(start, end, boundary, packed >>> 16, packed & 0xffff); - if (cmp < 0 || (cmp == 0 && !isEvenFloat(bits))) { - bits--; - continue; - } - - packed = buildFloatBoundary(bits, bits + 1, boundary); - cmp = compareTokenToBoundary(start, end, boundary, packed >>> 16, packed & 0xffff); - if (cmp > 0 || (cmp == 0 && !isEvenFloat(bits))) { - bits++; - continue; - } - return signedFloat(negative, bits); + long scale = offset < end ? readExponentScale(offset, fractionDigits) : fractionDigits; + StringBuilder token = new StringBuilder(last - first + 32); + if (negative) { + token.append('-'); } - return signedFloat(negative, exactFloatTokenBits(start, end, boundary)); - } - - private int exactFloatTokenBits(int start, int end, byte[] boundary) { - int low = 0; - int high = FLOAT_INFINITY_BITS; - while (low < high) { - int middle = (low + high) >>> 1; - int packed = buildFloatBoundary(middle, middle + 1, boundary); - int cmp = compareTokenToBoundary(start, end, boundary, packed >>> 16, packed & 0xffff); - if (cmp < 0) { - high = middle; - } else if (cmp > 0) { - low = middle + 1; - } else { - return isEvenFloat(middle) ? middle : middle + 1; + for (int i = first; i <= last; i++) { + char ch = charAt(i); + if (ch != '.') { + token.append(ch); } } - return low; - } - - private static float signedFloat(boolean negative, int bits) { - float result = Float.intBitsToFloat(bits); - return negative ? -result : result; - } - - private static int buildFloatBoundary(int lowBits, int highBits, byte[] digits) { - int numerator; - int binaryExponent; - if (highBits == FLOAT_INFINITY_BITS) { - numerator = (1 << (FLOAT_FRACTION_BITS + 2)) - 1; - binaryExponent = 103; - } else { - int lowMantissa = floatMantissa(lowBits); - int lowExponent = floatBinaryExponent(lowBits); - int highMantissa = floatMantissa(highBits); - int highExponent = floatBinaryExponent(highBits); - int exponent = Math.min(lowExponent, highExponent); - numerator = - (lowMantissa << (lowExponent - exponent)) + (highMantissa << (highExponent - exponent)); - binaryExponent = exponent - 1; - } - int length = writeBoundaryDigits(numerator, binaryExponent, digits); - int scale = binaryExponent < 0 ? -binaryExponent : 0; - return (length << 16) | scale; + return token.append('e').append((long) trailingZeros - scale).toString(); } private static int floatMantissa(int bits) { @@ -2404,146 +2054,6 @@ private static int floatBinaryExponent(int bits) { return exponent == 0 ? -149 : exponent - 150; } - private static int writeBoundaryDigits(long numerator, int binaryExponent, byte[] digits) { - int length = 0; - long value = numerator; - do { - digits[length++] = (byte) (value % 10); - value /= 10; - } while (value != 0); - int factor = binaryExponent >= 0 ? 2 : 5; - int count = binaryExponent >= 0 ? binaryExponent : -binaryExponent; - for (int i = 0; i < count; i++) { - length = multiplyDecimalDigits(digits, length, factor); - } - for (int left = 0, right = length - 1; left < right; left++, right--) { - byte digit = digits[left]; - digits[left] = digits[right]; - digits[right] = digit; - } - return length; - } - - private static int multiplyDecimalDigits(byte[] digits, int length, int factor) { - int carry = 0; - for (int i = 0; i < length; i++) { - int product = digits[i] * factor + carry; - digits[i] = (byte) (product % 10); - carry = product / 10; - } - while (carry != 0) { - digits[length++] = (byte) (carry % 10); - carry /= 10; - } - return length; - } - - private int compareTokenToBoundary( - int start, int end, byte[] boundaryDigits, int boundaryLength, int boundaryScale) { - int offset = start; - if (offset < end && charAt(offset) == '-') { - offset++; - } - int scan = offset; - int digitCount = 0; - int fractionDigits = 0; - boolean fraction = false; - boolean significant = false; - while (scan < end) { - int ch = charAt(scan); - if (ch >= '0' && ch <= '9') { - if (ch != '0' || significant) { - significant = true; - digitCount++; - } - if (fraction) { - fractionDigits++; - } - scan++; - } else if (ch == '.') { - fraction = true; - scan++; - } else { - break; - } - } - if (!significant) { - return -1; - } - long exponent = 0; - if (scan < end) { - int ch = charAt(scan); - if (ch == 'e' || ch == 'E') { - scan++; - boolean negativeExponent = false; - if (scan < end) { - ch = charAt(scan); - if (ch == '-' || ch == '+') { - negativeExponent = ch == '-'; - scan++; - } - } - while (scan < end) { - ch = charAt(scan); - if (ch < '0' || ch > '9') { - break; - } - if (exponent < TOKEN_EXPONENT_LIMIT) { - exponent = exponent * 10 + ch - '0'; - if (exponent > TOKEN_EXPONENT_LIMIT) { - exponent = TOKEN_EXPONENT_LIMIT; - } - } - scan++; - } - if (negativeExponent) { - exponent = -exponent; - } - } - } - - long adjustedLength = (long) digitCount + exponent - fractionDigits; - long boundaryAdjustedLength = (long) boundaryLength - boundaryScale; - if (adjustedLength != boundaryAdjustedLength) { - return adjustedLength < boundaryAdjustedLength ? -1 : 1; - } - - scan = offset; - significant = false; - int emitted = 0; - int max = Math.max(digitCount, boundaryLength); - for (int i = 0; i < max; i++) { - int digit = 0; - if (emitted < digitCount) { - while (scan < end) { - int ch = charAt(scan++); - if (ch == 'e' || ch == 'E') { - break; - } - if (ch < '0' || ch > '9') { - continue; - } - if (ch == '0' && !significant) { - continue; - } - significant = true; - digit = ch - '0'; - emitted++; - break; - } - } - int boundaryDigit = i < boundaryLength ? boundaryDigits[i] : 0; - if (digit != boundaryDigit) { - return digit < boundaryDigit ? -1 : 1; - } - } - return 0; - } - - private static boolean isEvenFloat(int bits) { - return (bits & 1) == 0; - } - private static long multiplyByPowerOfTen(long value, int power) { long multiplier = LONG_POWERS_OF_TEN[power]; if (value != 0 && value > Long.MAX_VALUE / multiplier) { @@ -3306,7 +2816,6 @@ private int hexValue(char ch) { protected abstract String slice(int start, int end); private static final class NumericWorkspace { - private final byte[] decimalBoundaryDigits = new byte[DECIMAL_BOUNDARY_DIGITS]; private char[] bigDecimalBuffer = new char[INITIAL_BIG_DECIMAL_BUFFER_SIZE]; } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 3f68dbc2cf..2f8212f3e2 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -666,6 +666,19 @@ public void readLongArrays() { () -> json.fromJson("[1,null]".getBytes(StandardCharsets.UTF_8), long[].class)); } + @Test + public void rejectNullFloatingElements() { + ForyJson json = newJson(); + for (Class type : new Class[] {float[].class, double[].class}) { + for (String text : new String[] {"[null]", "[1.5, null]", "[1e40, null]"}) { + assertThrows(ForyJsonException.class, () -> json.fromJson(text, type)); + assertThrows( + ForyJsonException.class, + () -> json.fromJson(text.getBytes(StandardCharsets.UTF_8), type)); + } + } + } + @Test public void writeLongArrays() { ForyJson json = newJson(); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 77bf099165..21a9551818 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -2156,6 +2156,23 @@ public void rejectGeneratedFloatingGrammar(boolean codegen) { "{\"ignored\":\"\u0100\",\"doubleValue\":1e1e1}", GeneratedFloatingFields.class)); } + @Test + public void readFloatingDecimalPowers() { + Random random = new Random(58390412L); + for (int exponent = -343; exponent <= 309; exponent++) { + long[] significands = { + 1, Long.MAX_VALUE, 12345678901234567L, random.nextLong() & Long.MAX_VALUE + }; + for (long significand : significands) { + String token = significand + "e" + exponent; + assertDoubleBits(token); + assertDoubleBits("-" + token); + assertFloatBits(token); + assertFloatBits("-" + token); + } + } + } + @Test public void readDoubleFallbackTokens() { assertDoubleBits("1.25e2"); @@ -2168,6 +2185,10 @@ public void readDoubleFallbackTokens() { long one = Double.doubleToRawLongBits(1.0d); assertDoubleBits("0." + repeat('0', 100_001) + "1e100002", one); assertDoubleBits("1" + repeat('0', 100_001) + "e-100001", one); + assertDoubleBits( + "0." + repeat('0', 100_001) + "123456789012345678901e100002", + Double.doubleToRawLongBits(Double.parseDouble("1.23456789012345678901"))); + assertDoubleBits("0." + repeat('0', 100_001) + "1e99970", Double.doubleToRawLongBits(1e-32)); } @Test @@ -2228,6 +2249,10 @@ public void readFloatFallbackTokens() { int one = Float.floatToRawIntBits(1.0f); assertFloatBits("0." + repeat('0', 100_001) + "1e100002", one); assertFloatBits("1" + repeat('0', 100_001) + "e-100001", one); + assertFloatBits( + "0." + repeat('0', 100_001) + "123456789012345678901e100002", + Float.floatToRawIntBits(Float.parseFloat("1.23456789012345678901"))); + assertFloatBits("0." + repeat('0', 100_001) + "1e99970", Float.floatToRawIntBits(1e-32f)); assertTrue(Float.isNaN(newUtf8Reader("\"NaN\"".getBytes(StandardCharsets.UTF_8)).readFloat())); assertEquals(newLatin1Reader(latin1Bytes("\"Infinity\"")).readFloat(), Float.POSITIVE_INFINITY); assertEquals(utf16Reader("\"-Infinity\"").readFloat(), Float.NEGATIVE_INFINITY);