What's wrong
QuantitiesGenerator.WriteVectorMethods (Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs:1778-1788, and again for Distance at ~1812-1825) writes every vector norm as a raw sum of squares followed by StorageMath.Sqrt:
T sum = (X * X) + (Y * Y) + (Z * Z);
return StorageMath.Sqrt(sum);
The intermediate X * X leaves the storage type's range long before the length does:
- Integer storage (
int, long, …) runs unchecked, so the square wraps. A component of 65536 squares to 2^32, which is 0 in int, and the length comes out 0 with no error. A component of 50000 wraps negative, and Sqrt then throws OverflowException.
decimal has a checked multiply, so any component ≥ ~2.8e14 throws OverflowException from Length(), Magnitude(), Distance(), DistanceTo() and Normalize(), even though the length itself fits comfortably.
float components ≥ ~1.8e19 give Length() == Infinity, and Normalize() then returns (0, 0, 0).
The library already has the fix. StorageMath.Hypot (Semantics.Quantities/StorageMath.cs:245, added in #239/#247) scales by the larger leg for exactly this reason, which CLAUDE.md states as "a pair whose squares leave the type still has its hypotenuse". The generated vector code never calls it.
Reproduction (built from HEAD):
Position3D<int>.FromMeter(65536,0,0).Length() -> 0
Displacement2D<int>.FromMeter(65536,0).Length() -> 0
Position3D<long>.FromMeter(1L<<32,0,0).Length() -> 0
Position3D<int>.FromMeter(50000,0,0).Length() -> OverflowException
Position3D<decimal>.FromMeter(1e15m,0,0).Length() -> OverflowException (Value was either too large or too small for a Decimal)
Position3D<decimal>.FromMeter(1e15m,0,0).Magnitude() -> OverflowException
Position3D<float>.FromMeter(1e20f,0,0).Length() -> Infinity
Position3D<float>.FromMeter(1e20f,0,0).Normalize() -> (0, 0, 0)
StorageMath.Hypot(65536, 0) -> 65536
StorageMath.Hypot(1e15m, 0m) -> 1000000000000000
StorageMath.Hypot(1e20f, 0f) -> 1E+20
1e15 m is about 0.1 light-year, so a decimal position in metres (for example in the orbital showcase that drove #240/#274) stops working at interstellar distances. The int case is a silently wrong answer at 65.5 m when the unit is millimetres.
Suggested fix / acceptance criteria
- Emit the norm through a scaled form: for 2D,
StorageMath.Hypot(X, Y). For 3D and 4D, either add an N-ary StorageMath.Hypot overload that uses the same largest-leg scaling for fractional types, or scale by max(|c|) in the generated code. Integer types keep the floor semantics Hypot already documents for them.
- Apply the same change to
Distance(), which feeds DistanceTo(). Magnitude() and Normalize() follow automatically.
LengthSquared()/DistanceSquared() can stay as they are: a squared result that overflows is the caller's own request.
- Tests: the eight rows above give the correct length, or throw only where the length itself is unrepresentable. Regenerate the committed
Generated/ output, and keep verify-generated passing.
What's wrong
QuantitiesGenerator.WriteVectorMethods(Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs:1778-1788, and again forDistanceat ~1812-1825) writes every vector norm as a raw sum of squares followed byStorageMath.Sqrt:The intermediate
X * Xleaves the storage type's range long before the length does:int,long, …) runs unchecked, so the square wraps. A component of 65536 squares to 2^32, which is 0 inint, and the length comes out 0 with no error. A component of 50000 wraps negative, andSqrtthen throwsOverflowException.decimalhas a checked multiply, so any component ≥ ~2.8e14 throwsOverflowExceptionfromLength(),Magnitude(),Distance(),DistanceTo()andNormalize(), even though the length itself fits comfortably.floatcomponents ≥ ~1.8e19 giveLength() == Infinity, andNormalize()then returns(0, 0, 0).The library already has the fix.
StorageMath.Hypot(Semantics.Quantities/StorageMath.cs:245, added in #239/#247) scales by the larger leg for exactly this reason, which CLAUDE.md states as "a pair whose squares leave the type still has its hypotenuse". The generated vector code never calls it.Reproduction (built from HEAD):
1e15 m is about 0.1 light-year, so a
decimalposition in metres (for example in the orbital showcase that drove #240/#274) stops working at interstellar distances. Theintcase is a silently wrong answer at 65.5 m when the unit is millimetres.Suggested fix / acceptance criteria
StorageMath.Hypot(X, Y). For 3D and 4D, either add an N-aryStorageMath.Hypotoverload that uses the same largest-leg scaling for fractional types, or scale bymax(|c|)in the generated code. Integer types keep the floor semanticsHypotalready documents for them.Distance(), which feedsDistanceTo().Magnitude()andNormalize()follow automatically.LengthSquared()/DistanceSquared()can stay as they are: a squared result that overflows is the caller's own request.Generated/output, and keepverify-generatedpassing.