@@ -80,27 +80,51 @@ impl V3ValueType {
8080 pub fn as_u64 ( self ) -> u64 {
8181 self as u64
8282 }
83+ }
8384
84- /// Determines the best value type for a given f64 value.
85- ///
86- /// Prefers smaller representations when lossless:
87- /// - Zero for 0.0
88- /// - Sint64 for integers that fit in 49 bits
89- /// - Float32 for values representable as f32
90- /// - Float64 otherwise
91- pub fn for_value ( v : f64 ) -> Self {
85+ /// Intermediate point classification for value type compaction.
86+ ///
87+ /// This provides finer-grained classification than [`V3ValueType`] to avoid
88+ /// precision loss when combining different value types. In particular, it
89+ /// distinguishes small integers (that fit losslessly in f32) from large integers
90+ /// (that don't), so that mixing a large integer with a Float32 value correctly
91+ /// escalates to Float64 rather than silently truncating the integer.
92+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , PartialOrd , Ord ) ]
93+ #[ repr( u8 ) ]
94+ enum PointKind {
95+ /// Value is zero.
96+ Zero = 0 ,
97+ /// Integer with |v| <= 2^24, fits losslessly in both sint64 and f32.
98+ Int24 = 1 ,
99+ /// Integer with |v| > 2^24, fits in sint64 varint but NOT losslessly in f32.
100+ Int48 = 2 ,
101+ /// Fractional value exactly representable as f32.
102+ Float32 = 3 ,
103+ /// Everything else — requires full f64 precision.
104+ Float64 = 4 ,
105+ }
106+
107+ /// Maximum integer magnitude that fits losslessly in f32 (2^24).
108+ const F32_INT_MAX : i64 = 1 << 24 ;
109+
110+ impl PointKind {
111+ /// Classifies a single f64 value.
112+ fn for_value ( v : f64 ) -> Self {
92113 if v == 0.0 {
93114 return Self :: Zero ;
94115 }
95116
96- // Varint range that fits in 7 bytes or less (49 bits)
117+ // Varint range that fits in 7 bytes or less (49 bits).
97118 const VARINT_WIDTH : i32 = 7 * 7 - 1 ;
98119 const MAX_INT : i64 = 1 << VARINT_WIDTH ;
99120 const MIN_INT : i64 = -MAX_INT ;
100121
101122 let i = v as i64 ;
102123 if ( MIN_INT ..MAX_INT ) . contains ( & i) && ( i as f64 ) == v {
103- return Self :: Sint64 ;
124+ if ( -F32_INT_MAX ..=F32_INT_MAX ) . contains ( & i) {
125+ return Self :: Int24 ;
126+ }
127+ return Self :: Int48 ;
104128 }
105129
106130 if ( v as f32 as f64 ) == v {
@@ -110,46 +134,114 @@ impl V3ValueType {
110134 Self :: Float64
111135 }
112136
113- /// Returns the maximum (largest encoding) of two value types.
114- pub fn max ( self , other : Self ) -> Self {
115- if ( other as u8 ) > ( self as u8 ) {
116- other
117- } else {
118- self
137+ /// Combines two point kinds into the smallest kind that can represent both.
138+ ///
139+ /// This is `max(self, other)` in all cases **except**:
140+ /// - `Int48 + Float32 = Float64` (and vice versa), because large integers
141+ /// lose precision in f32, and fractional values can't be stored as sint64.
142+ fn union ( self , other : Self ) -> Self {
143+ match ( self , other) {
144+ ( Self :: Int48 , Self :: Float32 ) | ( Self :: Float32 , Self :: Int48 ) => Self :: Float64 ,
145+ _ => self . max ( other) ,
119146 }
120147 }
148+
149+ /// Converts to the wire-format value type.
150+ fn to_value_type ( self ) -> V3ValueType {
151+ match self {
152+ Self :: Zero => V3ValueType :: Zero ,
153+ Self :: Int24 | Self :: Int48 => V3ValueType :: Sint64 ,
154+ Self :: Float32 => V3ValueType :: Float32 ,
155+ Self :: Float64 => V3ValueType :: Float64 ,
156+ }
157+ }
158+ }
159+
160+ /// Determines the best [`V3ValueType`] for a set of f64 values.
161+ ///
162+ /// Uses [`PointKind`] internally to avoid precision loss when mixing
163+ /// large integers with fractional float32 values.
164+ pub ( super ) fn value_type_for_values ( values : impl Iterator < Item = f64 > ) -> V3ValueType {
165+ let mut kind = PointKind :: Zero ;
166+ for v in values {
167+ kind = kind. union ( PointKind :: for_value ( v) ) ;
168+ }
169+ kind. to_value_type ( )
121170}
122171
123172#[ cfg( test) ]
124173mod tests {
125174 use super :: * ;
126175
127176 #[ test]
128- fn test_value_type_for_value ( ) {
129- assert_eq ! ( V3ValueType :: for_value( 0.0 ) , V3ValueType :: Zero ) ;
130- assert_eq ! ( V3ValueType :: for_value( 100.0 ) , V3ValueType :: Sint64 ) ;
131- assert_eq ! ( V3ValueType :: for_value( -100.0 ) , V3ValueType :: Sint64 ) ;
132- assert_eq ! ( V3ValueType :: for_value( 1.5 ) , V3ValueType :: Float32 ) ;
133- assert_eq ! ( V3ValueType :: for_value( 2.75 ) , V3ValueType :: Float32 ) ;
134-
135- // Large integers that don't fit in 49 bits AND can't be exactly represented in f32
136- // Powers of 2 like (1 << 50) can be exactly represented in f32, so we add 1
137- // to make it an odd number that requires more precision than f32 provides
177+ fn test_point_kind_classification ( ) {
178+ // Zero
179+ assert_eq ! ( PointKind :: for_value( 0.0 ) , PointKind :: Zero ) ;
180+
181+ // Small integers (fit in f32)
182+ assert_eq ! ( PointKind :: for_value( 100.0 ) , PointKind :: Int24 ) ;
183+ assert_eq ! ( PointKind :: for_value( -100.0 ) , PointKind :: Int24 ) ;
184+ assert_eq ! ( PointKind :: for_value( ( 1 << 24 ) as f64 ) , PointKind :: Int24 ) ;
185+ assert_eq ! ( PointKind :: for_value( -( ( 1 << 24 ) as f64 ) ) , PointKind :: Int24 ) ;
186+
187+ // Large integers (don't fit losslessly in f32)
188+ assert_eq ! ( PointKind :: for_value( ( ( 1 << 24 ) + 1 ) as f64 ) , PointKind :: Int48 ) ;
189+ assert_eq ! ( PointKind :: for_value( ( 1i64 << 30 ) as f64 ) , PointKind :: Int48 ) ;
190+
191+ // Float32
192+ assert_eq ! ( PointKind :: for_value( 1.5 ) , PointKind :: Float32 ) ;
193+ assert_eq ! ( PointKind :: for_value( 2.75 ) , PointKind :: Float32 ) ;
194+
195+ // Float64
196+ assert_eq ! ( PointKind :: for_value( std:: f64 :: consts:: PI ) , PointKind :: Float64 ) ;
138197 let large = ( ( 1i64 << 50 ) + 1 ) as f64 ;
139- assert_eq ! ( V3ValueType :: for_value( large) , V3ValueType :: Float64 ) ;
198+ assert_eq ! ( PointKind :: for_value( large) , PointKind :: Float64 ) ;
199+ }
140200
141- // Values that require f64 precision - use PI which has more precision than f32 can hold
142- // and isn't an integer, so it won't be stored as Sint64
143- let pi = std:: f64:: consts:: PI ;
144- // PI requires full f64 precision to store exactly
145- assert_eq ! ( V3ValueType :: for_value( pi) , V3ValueType :: Float64 ) ;
201+ #[ test]
202+ fn test_point_kind_union ( ) {
203+ // Standard widening (max)
204+ assert_eq ! ( PointKind :: Zero . union ( PointKind :: Int24 ) , PointKind :: Int24 ) ;
205+ assert_eq ! ( PointKind :: Int24 . union ( PointKind :: Int48 ) , PointKind :: Int48 ) ;
206+ assert_eq ! ( PointKind :: Int24 . union ( PointKind :: Float32 ) , PointKind :: Float32 ) ;
207+ assert_eq ! ( PointKind :: Float32 . union ( PointKind :: Float64 ) , PointKind :: Float64 ) ;
208+ assert_eq ! ( PointKind :: Float64 . union ( PointKind :: Zero ) , PointKind :: Float64 ) ;
209+
210+ // The critical case: large integer + float32 must escalate to float64
211+ assert_eq ! ( PointKind :: Int48 . union ( PointKind :: Float32 ) , PointKind :: Float64 ) ;
212+ assert_eq ! ( PointKind :: Float32 . union ( PointKind :: Int48 ) , PointKind :: Float64 ) ;
146213 }
147214
148215 #[ test]
149- fn test_value_type_max ( ) {
150- assert_eq ! ( V3ValueType :: Zero . max( V3ValueType :: Sint64 ) , V3ValueType :: Sint64 ) ;
151- assert_eq ! ( V3ValueType :: Sint64 . max( V3ValueType :: Float32 ) , V3ValueType :: Float32 ) ;
152- assert_eq ! ( V3ValueType :: Float32 . max( V3ValueType :: Float64 ) , V3ValueType :: Float64 ) ;
153- assert_eq ! ( V3ValueType :: Float64 . max( V3ValueType :: Zero ) , V3ValueType :: Float64 ) ;
216+ fn test_value_type_for_values ( ) {
217+ // All zeros
218+ assert_eq ! ( value_type_for_values( [ 0.0 , 0.0 ] . into_iter( ) ) , V3ValueType :: Zero ) ;
219+
220+ // Small integers
221+ assert_eq ! ( value_type_for_values( [ 100.0 , 200.0 ] . into_iter( ) ) , V3ValueType :: Sint64 ) ;
222+
223+ // Large integers
224+ assert_eq ! (
225+ value_type_for_values( [ ( 1i64 << 30 ) as f64 , 200.0 ] . into_iter( ) ) ,
226+ V3ValueType :: Sint64
227+ ) ;
228+
229+ // Small integer + float32 → Float32 (safe, small int fits in f32)
230+ assert_eq ! ( value_type_for_values( [ 100.0 , 1.5 ] . into_iter( ) ) , V3ValueType :: Float32 ) ;
231+
232+ // Large integer + float32 → Float64 (the bug fix!)
233+ assert_eq ! (
234+ value_type_for_values( [ ( 1i64 << 30 ) as f64 , 1.5 ] . into_iter( ) ) ,
235+ V3ValueType :: Float64
236+ ) ;
237+
238+ // Float64 value forces Float64
239+ assert_eq ! (
240+ value_type_for_values( [ 100.0 , std:: f64 :: consts:: PI ] . into_iter( ) ) ,
241+ V3ValueType :: Float64
242+ ) ;
243+
244+ // Empty iterator
245+ assert_eq ! ( value_type_for_values( std:: iter:: empty( ) ) , V3ValueType :: Zero ) ;
154246 }
155247}
0 commit comments