You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
3D/4D vector Length() with int/short/byte storage is one too low on perfect squares: Position3D<int>.FromMeter(6, 41, 42).Length() returns 58, not 59 #537
For storage types that IsRoundedThroughDouble sends through double (byte, sbyte, short, ushort, int, uint), the 3- and 4-component StorageMath.Hypot overloads nest two double.Hypot calls and then truncate the result:
Each Hypot call rounds, so an exact integer length can come back 1 ulp low, for example 58.99999999999999. CreateChecked then truncates it to 58. The method's documented contract is "the floor for an integer type". The true floor of √3481 is 59.
All generated Length(), Magnitude(), Distance() and DistanceTo() members on 3D and 4D vector forms go through this code (for example Velocity3D.g.cs:115-120).
Reproduction
Verified by running against the built assemblies at ff736ff:
new Acceleration4D<int>{ X = 3, Y = 3, Z = 3, W = 3 }.Length()
5
6
A brute force over components 0..60 in 3D and 0..20 in 4D found 3 wrong results in 3D and 20 in 4D. Other examples: (14,39,42) gives 58, (27,28,36) gives 52, (2,3,4,14) gives 14 instead of 15, and (1,4,8,12) gives 14 instead of 15. The 2-component Hypot<int> was correct on every Pythagorean triple below 46000.
Why it matters
This is a regression from 5f727f0, the #327 overflow fix. The previous code summed the squares and took a single Math.Sqrt, which is exact for perfect squares in range. The existing tests only use vectors with one non-zero component, such as (65536, 0, 0), so no test catches it. Callers comparing integer distances, for example distance <= radius on a grid, get off-by-one answers exactly on the boundary.
Suggested fix
Send the narrow integer primitives through the exact integer path rather than the double round trip. For example, widen them to long/Int128, use HypotOfWideInteger or an integer sum of squares plus an integer square root, then T.CreateChecked back.
Keep the double.Hypot route only for float, double and Half. For double itself the same 1-ulp error appears (Position3D<double>(6, 41, 42).Length() returns 58.99999999999999), which is less serious but worth the same review.
Add tests on perfect-square triples and quadruples: (6,41,42) → 59 and (3,3,3,3) → 6, for both int and short.
Related, lower priority: generated Distance() subtracts the components in T before calling Hypot (QuantitiesGenerator.cs:1935), so the difference can wrap without any error. Position3D<int>.FromMeter(int.MaxValue, 0, 0).Distance(FromMeter(-2, 0, 0)) returns 2147483647. That matches the type's unchecked - operator, but it is not the OverflowException the #520Hypot contract describes, so it is worth deciding on while this code is open.
Priority: High. Length(), Magnitude(), Distance() and DistanceTo() on every 3D and 4D vector with int/short/byte-family storage silently return a value one too low on exact perfect squares. That is exactly the boundary case for distance <= radius checks. There is no exception, and current tests can't catch it because they only use single-axis vectors.
Route the narrow integer primitives through the exact integer path: widen, use HypotOfWideInteger or an integer sum of squares with an integer square root, then CreateChecked back.
Add the perfect-square tests from the issue, (6,41,42) → 59 and (3,3,3,3) → 6, for int and short.
Decide separately on the wrap-around in generated Distance() (QuantitiesGenerator.cs:1935). It could be its own issue if it isn't handled here.
Activity
matt-edmondson commentedon Nov 4, 2026
Triage
Length()/Magnitude()/Distance()square the components before rooting:Position3D<int>.FromMeter(65536, 0, 0).Length()is 0, and adecimalvector past ~2.8e14 throws #327 overflow fix)Length(),Magnitude(),Distance()andDistanceTo()on every 3D and 4D vector withint/short/byte-family storage silently return a value one too low on exact perfect squares. That is exactly the boundary case fordistance <= radiuschecks. There is no exception, and current tests can't catch it because they only use single-axis vectors.Semantics.Quantities/StorageMath.cs, the 3- and 4-componentHypotoverloads (:322-328,:364-371). Whoever owns the VectorLength()/Magnitude()/Distance()square the components before rooting:Position3D<int>.FromMeter(65536, 0, 0).Length()is 0, and adecimalvector past ~2.8e14 throws #327/StorageMath.Sqrt/Hypot return the wrong floor for long, ulong, Int128 and UInt128 past 2^53, and Hypot throws OverflowException near MaxValue #520Hypotcontract.StorageMath(the only open PR is Bump ktsu.SourceGeneratorToolkit and ktsu.SourceGeneratorToolkit.Testing #501, a dependency bump).HypotOfWideIntegeror an integer sum of squares with an integer square root, thenCreateCheckedback.intandshort.Distance()(QuantitiesGenerator.cs:1935). It could be its own issue if it isn't handled here.Generated by Claude Code