Skip to content

Commit 75543a7

Browse files
committed
test(java): give the float primitive assertion a value that discriminates
Thirteenth full-range review: APPROVE, 0 blocking. The one important finding is a test with no teeth rather than a code defect, which is a first for this range. floatShapesAllUseTheGoFormatter claimed to cover all three float carriers but pinned only two: FloatHolder.getPrimitive() returned 0.1f, and Jackson's default writeNumber(float) also emits "0.1", so the assertion could not tell the registered path from the default one. Deleting the Float.TYPE registration left all 336 tests green. The reviewer established the registration is load-bearing before blaming the test — with 1e20f the output is 1.0E20 without it — which is the right order of inference. Now 1e-7f, where Go gives "1e-7" and Jackson gives "1.0E-7", and the case goes red when the registration is removed. Two counts corrected, both mine: - "11 low subnormals" is 9. Re-counted exhaustively over float32 subnormals, bits 1..0x7FFFFF, by output bytes changing — the same convention the double-side figure uses: patterns 1, 2, 3, 4, 6, 7, 21, 29, 71. The javadoc now names the range and the convention, which the double figure already did and this one did not. - The main-code comment still said "over 50 divergences in a 200k random sweep" after the test comment and llmdoc had both been corrected to 11-13 for uniform sampling. Three places quoting one measurement, two updated, one missed. Also recorded a forward-looking warning the reviewer raised beyond its findings: pine-cpp has no float32 path today (Variant::value_t holds only double), so no divergence exists — but adding one by widening to double and calling go_format_json_number would reproduce exactly the trap the Java side just handled, including that the threshold must be compared against the shortened decimal rather than the widened value. 336 Java tests, 247 C++ cases, lint, codegen-check, cross-validate 55/55, differential-fuzz 1000/1000. The reviewer additionally ran real Go and Java servers against fixture 06 and got cmp-identical bodies, and swept 8,388,607 float32 subnormals plus 2,000,000 double subnormals with zero divergences.
1 parent 03a98de commit 75543a7

3 files changed

Lines changed: 36 additions & 4 deletions

File tree

llmdoc/reference/number-formatting-parity.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,25 @@ C++ 那支的成因单独说一下,因为不看代码想不到:`go_format_lo
153153
**后续要收这条时的注意点**:不要只改 Java 的位数——C++ 的 `buf[64]` 必须一起扩,
154154
否则「统一」之后 C++ 仍在输出 `5e-324`。这一点是第九轮审查提出的,它在自己的
155155
finding 之外额外查了 C++ 分支,而我之前的文档只写了 Java vs Go。
156+
157+
## 如果将来给 pine-cpp 加 float32 路径
158+
159+
现在不存在这条路径——`Variant::value_t` 只有 `double`,所以 C++ 侧没有 float32 分歧,
160+
这一节是**给未来的警告**,不是待修项。
161+
162+
真要加的时候,**不要把 float32 加宽成 double 再调 `go_format_json_number`**。Go 用
163+
`strconv.AppendFloat(..., 32)`,数字是「对 float32 最短往返」,加宽会把窄类型原本藏住的
164+
二进制噪声抖出来:
165+
166+
```
167+
float32 0.1 Go: 0.1 加宽后: 0.10000000149011612
168+
float32 1e20 Go: 100000000000000000000 加宽后: 100000002004087730000
169+
```
170+
171+
pine-java 侧的 `formatJsonNumber(float)` 就是踩过这个坑之后的写法:单独一条
172+
`shortestRoundTrip(float)` 按 float 精度缩短,**且阈值要拿缩短后的十进制去比、不是拿加宽的
173+
double 去比**`bits=897988541` 加宽是 `9.999999974752427e-07` 低于 1e-6,缩短后是 `1e-06`
174+
不低于,Go 输出 `0.000001`)。C++ 侧要加的话,同样两点都得照做。
175+
176+
另外注意 `Float.toString` / `Double.toString`**不是**次正规的最短表示(float32 有 9 个
177+
bit pattern、double 有 8 个),这是两条 `shortestRoundTrip` 存在的唯一理由。

pine-java/src/main/java/page/liam/pine/GoFormat.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,10 @@ public static String formatJsonNumber(double d) {
132132
* <p>Float.toString supplies the digits, but is not shortest for subnormals
133133
* — the same defect the double path has with Double.toString. It renders
134134
* Float.MIN_VALUE as "1.4E-45" when "1E-45" round-trips, and Go emits the
135-
* latter; 11 low subnormals diverged that way. So the digits go through
135+
* latter. Exhaustively over the float32 subnormals (bits 1..0x7FFFFF), nine
136+
* bit patterns render with more digits than needed — 1, 2, 3, 4, 6, 7, 21,
137+
* 29 and 71 — counted by output bytes changing, the same convention the
138+
* double figure above uses. So the digits go through
136139
* shortestRoundTrip(float) first, which shortens against float precision.
137140
* Placement and thresholds are then identical to the double case, which is
138141
* why this delegates rather than duplicating them.
@@ -197,7 +200,10 @@ private static String shortestRoundTrip(double d) {
197200
// different last digit for some values, because MathContext rounds
198201
// HALF_UP on the true expansion while Go's shortest algorithm reports
199202
// the digit nearest the double: 2209012388886329.2 in Go against
200-
// ...329.3 that way, over 50 such divergences in a 200k random sweep.
203+
// ...329.3 that way. The count depends entirely on how you sample:
204+
// 11-13 over 200k uniform random bit patterns, and far more when drawing
205+
// by magnitude. The mechanism does not depend on the draw; see
206+
// llmdoc/reference/number-formatting-parity.md.
201207
// Double.toString's digits are already the correct ones; the only thing
202208
// wrong with them is that there can be too many.
203209
java.math.BigDecimal exact = new java.math.BigDecimal(repr);

pine-java/src/test/java/page/liam/pine/GoJsonNumberParityTest.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,11 @@ void floatShapesAllUseTheGoFormatter() throws Exception {
272272
String json = MAPPER.writeValueAsString(new FloatHolder());
273273
org.junit.jupiter.api.Assertions.assertTrue(
274274
json.contains("\"boxed\":100000000000000000000"), json);
275-
org.junit.jupiter.api.Assertions.assertTrue(json.contains("\"primitive\":0.1"), json);
275+
// 1e-7f, not 0.1f: Jackson's default writeNumber(float) also emits "0.1",
276+
// so that value cannot tell the registered path from the default one and
277+
// the assertion had no teeth. Go renders float32 1e-7 as "1e-7" while
278+
// Jackson gives "1.0E-7".
279+
org.junit.jupiter.api.Assertions.assertTrue(json.contains("\"primitive\":1e-7"), json);
276280
org.junit.jupiter.api.Assertions.assertTrue(
277281
json.contains("\"array\":[100000000000000000000,1e-7]"), json);
278282
}
@@ -284,7 +288,7 @@ public Float getBoxed() {
284288
}
285289

286290
public float getPrimitive() {
287-
return 0.1f;
291+
return 1e-7f;
288292
}
289293

290294
public float[] getArray() {

0 commit comments

Comments
 (0)