Skip to content

fix: preserve negative zero in Float32Array and Float64Array#356

Open
greymoth-jp wants to merge 1 commit into
flightcontrolhq:mainfrom
greymoth-jp:fix-typed-array-negative-zero
Open

fix: preserve negative zero in Float32Array and Float64Array#356
greymoth-jp wants to merge 1 commit into
flightcontrolhq:mainfrom
greymoth-jp:fix-typed-array-negative-zero

Conversation

@greymoth-jp

@greymoth-jp greymoth-jp commented Jun 29, 2026

Copy link
Copy Markdown

stringify/parse drops the sign of negative zero when it is stored in a Float32Array or Float64Array:

const sj = new SuperJSON();
const back = sj.parse(sj.stringify(new Float64Array([-0])));
Object.is(back[0], -0); // false, but it should be true

The typed-array transformer already encodes the special float values that a JSON round-trip cannot represent: NaN, Infinity and -Infinity are written out as strings (#292, #349). -0 is the one that was left out. JSON.stringify(-0) returns "0", so the sign is gone as soon as the serialized payload passes through JSON.

superjson preserves -0 everywhere else (the dedicated number rule from regression #83), and the object-only serialize/deserialize path kept it too because it passes the raw value through untouched. Only the string path lost it, and only for the two float-backed typed arrays, since integer and clamped arrays cannot hold -0.

This stores -0 as "-0" and reads it back, the same way NaN/Infinity are handled in that function. I added a regression test next to the existing #292 case.

Greptile Summary

This PR fixes a bug where -0 stored in Float32Array or Float64Array was silently dropped during JSON serialization, coming back as +0 after a stringify/parse round-trip. It extends the existing special-float-value encoding in typedArrayRule to treat -0 the same way NaN, Infinity, and -Infinity are already handled.

  • src/transformer.ts: Adds a -0 detection check (n === 0 && 1 / n === -Infinity) in the serializer and a corresponding '-0'-0 conversion in the deserializer, following the exact pattern already used for the other IEEE 754 special values.
  • src/index.test.ts: Adds a regression test that verifies round-trip fidelity for -0 entries in both Float64Array and Float32Array, and also confirms positive 0 is not incorrectly converted.

Confidence Score: 5/5

Safe to merge — the change is minimal, isolated to the typed-array transformer, and follows the existing pattern exactly.

Both the serializer and deserializer changes are a single-line addition each, directly mirroring the already-proven NaN/Infinity handling. The negative-zero detection idiom (n === 0 && 1 / n === -Infinity) is correct and widely used in JS. The new test covers mixed arrays with both -0 and +0 to prevent false positives, and integer typed arrays are unaffected since they can never hold -0 in the first place.

No files require special attention.

Important Files Changed

Filename Overview
src/transformer.ts Adds negative-zero serialization ('-0' string) and deserialization in the typed-array transformer rule, consistent with the existing NaN/Infinity handling pattern. Logic is correct and the reciprocal-infinity idiom for detecting -0 is a well-established JavaScript technique.
src/index.test.ts New regression test covers Float64Array and Float32Array with mixed negative-zero, positive-zero, and normal values; uses Object.is for correct -0 identity checks and verifies positive zero is not falsely promoted to -0.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant App
    participant SuperJSON
    participant typedArrayRule
    participant JSON

    App->>SuperJSON: stringify(new Float64Array([-0, 0, 1]))
    SuperJSON->>typedArrayRule: serialize(Float64Array)
    typedArrayRule->>typedArrayRule: map each element
    Note over typedArrayRule: -0 → '-0' (new)<br/>0 → 0<br/>1 → 1
    typedArrayRule-->>SuperJSON: ['-0', 0, 1] + annotation
    SuperJSON->>JSON: JSON.stringify(['-0', 0, 1])
    JSON-->>App: "'{"json":{"a":["-0",0,1]},...}'"

    App->>SuperJSON: "parse('{"json":{"a":["-0",0,1]},...}')"
    SuperJSON->>JSON: JSON.parse(...)
    JSON-->>SuperJSON: "{a: ['-0', 0, 1]}"
    SuperJSON->>typedArrayRule: deserialize(['-0', 0, 1], 'Float64Array')
    typedArrayRule->>typedArrayRule: map each element
    Note over typedArrayRule: '-0' → -0 (new)<br/>0 → 0<br/>1 → 1
    typedArrayRule-->>SuperJSON: new Float64Array([-0, 0, 1])
    SuperJSON-->>App: Float64Array [-0, 0, 1]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant App
    participant SuperJSON
    participant typedArrayRule
    participant JSON

    App->>SuperJSON: stringify(new Float64Array([-0, 0, 1]))
    SuperJSON->>typedArrayRule: serialize(Float64Array)
    typedArrayRule->>typedArrayRule: map each element
    Note over typedArrayRule: -0 → '-0' (new)<br/>0 → 0<br/>1 → 1
    typedArrayRule-->>SuperJSON: ['-0', 0, 1] + annotation
    SuperJSON->>JSON: JSON.stringify(['-0', 0, 1])
    JSON-->>App: "'{"json":{"a":["-0",0,1]},...}'"

    App->>SuperJSON: "parse('{"json":{"a":["-0",0,1]},...}')"
    SuperJSON->>JSON: JSON.parse(...)
    JSON-->>SuperJSON: "{a: ['-0', 0, 1]}"
    SuperJSON->>typedArrayRule: deserialize(['-0', 0, 1], 'Float64Array')
    typedArrayRule->>typedArrayRule: map each element
    Note over typedArrayRule: '-0' → -0 (new)<br/>0 → 0<br/>1 → 1
    typedArrayRule-->>SuperJSON: new Float64Array([-0, 0, 1])
    SuperJSON-->>App: Float64Array [-0, 0, 1]
Loading

Reviews (1): Last reviewed commit: "fix: preserve negative zero in Float32Ar..." | Re-trigger Greptile

@greymoth-jp greymoth-jp requested a review from Skn0tt as a code owner June 29, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant