fix: keep getColorFromString finite for long strings (#651) - #717
fix: keep getColorFromString finite for long strings (#651)#717DPS0340 wants to merge 2 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Re-verified the snapshot churn on this PR independently, since 103 changed files is a lot to take on trust and I'd rather a reviewer not have to derive this themselves. Automated check — strip every The four: All four are ordering or trailing-whitespace only. Sorting the elements of each file and comparing gives an identical multiset: The diff on the first is 6 lines, all of the form "this No coordinate, path, or geometry value changes anywhere in the 102 files. On why ordering shifts at all: Reproducible if you want to confirm: strip |
The hash accumulator grows without bound: acc * 31 + code leaves exact integer range at 11 characters and overflows to Infinity past ~200, at which point Infinity % 360 is NaN and the function returns an unparseable hsl(NaN, 100%, 50%, 1). Net ids are selector paths like 'group > capacitor.C101 > port.pin1 to U100.VOUT', so nested groups reach that length in practice — about 22 levels of nesting is enough. Keep the accumulator in signed 32-bit range, which stays finite and exact for any input. Hues change for inputs longer than 10 characters, so snapshots are regenerated.
e62db21 to
a376ebc
Compare
Fixes #651.
Problem
getColorFromStringaccumulatesacc * 31 + charCodeAt(0)on a plain JS number. That grows without bound:Number.isSafeIntegerfalse from there on)Infinityat 207 characters, andInfinity % 360isNaNso the function returns
hsl(NaN, 100%, 50%, 1), which is not a parseable colour.This is reachable, not theoretical. Net ids here are selector paths — the longest in the existing fixtures is
group > capacitor.C101 > port.pin1 to U100.VOUT(47 chars,example21.json). Nesting multiplies that:About 22 levels of group nesting is enough to produce an invalid colour in
LabelMergingSolver,TraceLabelOverlapAvoidanceSolverandExample28Solver's visualizations.Fix
Keep the accumulator in signed 32-bit range with
| 0, then normalise the hue into[0, 360). That's the standard string-hash formulation and is finite and exact for any input length.Verified over lengths 1–3000: no
NaN, every hue in range, output still deterministic,alphastill honoured.About the snapshot churn — please read before judging the diff size
I tried to avoid this and could not, honestly. My first attempt folded
% 360into each step, reasoning that(a * 31 + c) % 360 ≡ (a % 360 * 31 + c) % 360and so no existing hue would change. I tested that claim instead of trusting it, and it was wrong: 18,974 of 20,000 random inputs changed colour.The reason is the bug itself. That congruence holds for exact integers, but the old accumulator stops being an exact integer at 11 characters — it's already a lossy float. So the "old" hues for anything longer than 10 characters were derived from accumulated floating-point error, and there is no fix that preserves them. Any correct implementation changes those colours.
So the 102 regenerated snapshots are unavoidable. To make them reviewable, I verified programmatically what actually changed:
hsl(...)values — strip everyhsl(...)and the files are byte-identical.example40,bug-report-20260707T141025Z— same multiset of elements, just reordered) or trailing whitespace inside the embedded<script>(repro-example35-...,repro-rp2040-gamepad-...).Method, if you want to re-run it: strip
hsl\([^)]*\)and collapse runs of whitespace in both the old and new snapshot, then compare.Full suite: 144 pass / 0 fail / 4 skip. Biome clean.
If you'd rather not take the churn
Reasonable position, and I'd understand it. Two alternatives, happy to switch:
Number.isFinite(hash) ? hash % 360 : 0. Zero snapshot changes, but colours stay derived from float error and every string past ~207 chars collapses to the same hue.Diff is 1 source file plus mechanical snapshot regeneration.