Skip to content

Vector Length()/Magnitude()/Distance() square the components before rooting: Position3D<int>.FromMeter(65536, 0, 0).Length() is 0, and a decimal vector past ~2.8e14 throws #327

Description

@matt-edmondson

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions