HarperFast / HarperFast/structon

Typed-struct float32 optimization can lose ≤1 ULP on non-canonical doubles

Open
#3 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
13
Forks
1
Avg merge
7m
Merged PRs (30d)
1

Description

## Summary

The typed-struct (`randomAccessStructure`) number path stores certain float64 values as a 4-byte float32 field and reconstructs them on decode using the significant-digit rounding heuristic (the `mult10` table). For a double that is *not* the canonical shortest-decimal representative of its decimal value, decode returns the canonical neighbor instead — a loss of up to 1 ULP.

This is the **deliberate msgpackr float32 optimization** carried over byte-for-byte from msgpackr's `struct.js` (it is **not** a structon regression — structon's output is byte-identical to msgpackr v1.11's native `randomAccessStructure` here). Filing as a tracking issue per discussion; **no fix planned right now**.

## Scope

- Only the **typed-struct path** is affected. Plain msgpackr/cbor-x number encoding preserves the exact double.
- The same behavior exists in **msgpackr v1's `struct.js`** (the deployed 5.0.x code), so any future change should be considered jointly.

## Reproduction

```js
import { Packr } from 'msgpackr';
import { createStructon } from 'structon';
const S = createStructon(Packr);
const enc = new S({ randomAccessStructure: true, useRecords: false });

const v = 1000.0001000000001; // a double adjacent to the 1000.0001 literal
const out = enc.decode(enc.encode({ x: v })).x;
console.log(out); // 1000.0001
console.log(Object.is(out, v)); // false (decoded to the canonical neighbor)
console.log(1000.0001 === v); // false (they are distinct doubles)
```

`1000.0001` (the canonical literal) round-trips exactly; only the non-canonical neighbor canonicalizes.

## Why it happens

In the number case, a float32-range value is written as float32, then accepted as `num32` if `number * mult10[exp]` is an integer. That check passes for both a nice decimal and its ULP-adjacent neighbors, so the neighbor decodes to the nice-decimal double.

## Possible fix (deferred)

Tighten the float32-acceptance check to require an exact round-trip (`decode(float32-bytes) === number`) before choosing `num32`, falling through to float64 otherwise. This would **diverge from msgpackr v1's bytes** (breaks the byte-identical-output property the test suite asserts), so it's a deliberate-optimization change, not a drop-in fix.

## Impact

Low: ≤1 ULP (~1e-13 relative), only on the struct path, only for non-canonical doubles. Not a correctness concern for typical data (e.g. it did not contribute to the typed-struct memory blowup tracked separately).

---
*Filed by Claude (Opus 4.7) on behalf of @kris-zyp during the typed-struct memory investigation.*

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.