spacetimedb_table/
static_layout.rs

1//! This module implements a fast path for converting certain row types between BFLATN <-> BSATN.
2//!
3//! The key insight is that a majority of row types will have a known fixed length,
4//! with no variable-length members.
5//! BFLATN is designed with this in mind, storing fixed-length portions of rows inline,
6//! at the expense of an indirection to reach var-length columns like strings.
7//! A majority of these types will also have a fixed BSATN length,
8//! but note that BSATN stores sum values (enums) without padding,
9//! so row types which contain sums may not have a fixed BSATN length
10//! if the sum's variants have different "live" unpadded lengths.
11//!
12//! For row types with fixed BSATN lengths, we can reduce the BFLATN <-> BSATN conversions
13//! to a series of `memcpy`s, skipping over padding sequences.
14//! This is potentially much faster than the more general
15//! [`crate::bflatn_from::serialize_row_from_page`] and [`crate::bflatn_to::write_row_to_page`] ,
16//! which both traverse a [`RowTypeLayout`] and dispatch on the type of each column.
17//!
18//! For example, to serialize a row of type `(u64, u64, u32, u64)`,
19//! [`bflatn_from`] will do four dispatches, three calls to `serialize_u64` and one to `serialize_u32`.
20//! This module will make 2 `memcpy`s (or actually, `<[u8]>::copy_from_slice`s):
21//! one of 20 bytes to copy the leading `(u64, u64, u32)`, which contains no padding,
22//! and then one of 8 bytes to copy the trailing `u64`, skipping over 4 bytes of padding in between.
23
24use super::{
25    indexes::{Byte, Bytes},
26    layout::{
27        AlgebraicTypeLayout, HasLayout, PrimitiveType, ProductTypeElementLayout, ProductTypeLayout, RowTypeLayout,
28        SumTypeLayout, SumTypeVariantLayout,
29    },
30    util::range_move,
31    MemoryUsage,
32};
33use core::mem::MaybeUninit;
34use core::ptr;
35
36/// A precomputed layout for a type whose encoded BSATN and BFLATN lengths are both known constants,
37/// enabling fast BFLATN <-> BSATN conversions.
38#[derive(PartialEq, Eq, Debug, Clone)]
39pub struct StaticLayout {
40    /// The length of the encoded BSATN representation of a row of this type,
41    /// in bytes.
42    ///
43    /// Storing this allows us to pre-allocate correctly-sized buffers,
44    /// avoiding potentially-expensive `realloc`s.
45    pub(crate) bsatn_length: u16,
46
47    /// A series of `memcpy` invocations from a BFLATN src/dst <-> a BSATN src/dst
48    /// which are sufficient to convert BSATN to BFLATN and vice versa.
49    fields: Box<[MemcpyField]>,
50}
51
52impl MemoryUsage for StaticLayout {
53    fn heap_usage(&self) -> usize {
54        let Self { bsatn_length, fields } = self;
55        bsatn_length.heap_usage() + fields.heap_usage()
56    }
57}
58
59impl StaticLayout {
60    /// Serialize `row` from BFLATN to BSATN into `buf`.
61    ///
62    /// # Safety
63    ///
64    /// - `buf` must be at least `self.bsatn_length` long.
65    /// - `row` must store a valid, initialized instance of the BFLATN row type
66    ///   for which `self` was computed.
67    ///   As a consequence of this, for every `field` in `self.fields`,
68    ///   `row[field.bflatn_offset .. field.bflatn_offset + length]` will be initialized.
69    unsafe fn serialize_row_into(&self, buf: &mut [MaybeUninit<Byte>], row: &Bytes) {
70        debug_assert!(buf.len() >= self.bsatn_length as usize);
71        for field in &*self.fields {
72            // SAFETY: forward caller requirements.
73            unsafe { field.copy_bflatn_to_bsatn(row, buf) };
74        }
75    }
76
77    /// Serialize `row` from BFLATN to BSATN into a `Vec<u8>`.
78    ///
79    /// # Safety
80    ///
81    /// - `row` must store a valid, initialized instance of the BFLATN row type
82    ///   for which `self` was computed.
83    ///   As a consequence of this, for every `field` in `self.fields`,
84    ///   `row[field.bflatn_offset .. field.bflatn_offset + length]` will be initialized.
85    pub(crate) unsafe fn serialize_row_into_vec(&self, row: &Bytes) -> Vec<u8> {
86        // Create an uninitialized buffer `buf` of the correct length.
87        let bsatn_len = self.bsatn_length as usize;
88        let mut buf = Vec::with_capacity(bsatn_len);
89        let sink = buf.spare_capacity_mut();
90
91        // (1) Write the row into the slice using a series of `memcpy`s.
92        // SAFETY:
93        // - Caller promised that `row` is valid for `self`.
94        // - `sink` was constructed with exactly the correct length above.
95        unsafe {
96            self.serialize_row_into(sink, row);
97        }
98
99        // SAFETY: In (1), we initialized `0..len`
100        // as `row` was valid for `self` per caller requirements.
101        unsafe { buf.set_len(bsatn_len) }
102        buf
103    }
104
105    /// Serialize `row` from BFLATN to BSATN, appending the BSATN to `buf`.
106    ///
107    /// # Safety
108    ///
109    /// - `row` must store a valid, initialized instance of the BFLATN row type
110    ///   for which `self` was computed.
111    ///   As a consequence of this, for every `field` in `self.fields`,
112    ///   `row[field.bflatn_offset .. field.bflatn_offset + length]` will be initialized.
113    pub(crate) unsafe fn serialize_row_extend(&self, buf: &mut Vec<u8>, row: &Bytes) {
114        // Get an uninitialized slice within `buf` of the correct length.
115        let start = buf.len();
116        let len = self.bsatn_length as usize;
117        buf.reserve(len);
118        let sink = &mut buf.spare_capacity_mut()[..len];
119
120        // (1) Write the row into the slice using a series of `memcpy`s.
121        // SAFETY:
122        // - Caller promised that `row` is valid for `self`.
123        // - `sink` was constructed with exactly the correct length above.
124        unsafe {
125            self.serialize_row_into(sink, row);
126        }
127
128        // SAFETY: In (1), we initialized `start .. start + len`
129        // as `row` was valid for `self` per caller requirements
130        // and we had initialized up to `start` before,
131        // so now we have initialized up to `start + len`.
132        unsafe { buf.set_len(start + len) }
133    }
134
135    #[allow(unused)]
136    /// Deserializes the BSATN-encoded `row` into the BFLATN-encoded `buf`.
137    ///
138    /// - `row` must be at least `self.bsatn_length` long.
139    /// - `buf` must be ready to store an instance of the BFLATN row type
140    ///   for which `self` was computed.
141    ///   As a consequence of this, for every `field` in `self.fields`,
142    ///   `field.bflatn_offset .. field.bflatn_offset + length` must be in-bounds of `buf`.
143    pub(crate) unsafe fn deserialize_row_into(&self, buf: &mut Bytes, row: &[u8]) {
144        for field in &*self.fields {
145            // SAFETY: forward caller requirements.
146            unsafe { field.copy_bsatn_to_bflatn(row, buf) };
147        }
148    }
149
150    /// Compares `row_a` for equality against `row_b`.
151    ///
152    /// # Safety
153    ///
154    /// - `row` must store a valid, initialized instance of the BFLATN row type
155    ///   for which `self` was computed.
156    ///   As a consequence of this, for every `field` in `self.fields`,
157    ///   `row[field.bflatn_offset .. field.bflatn_offset + field.length]` will be initialized.
158    pub(crate) unsafe fn eq(&self, row_a: &Bytes, row_b: &Bytes) -> bool {
159        // No need to check the lengths.
160        // We assume they are of the same length.
161        self.fields.iter().all(|field| {
162            // SAFETY: The consequence of what the caller promised is that
163            // `row_(a/b).len() >= field.bflatn_offset + field.length >= field.bflatn_offset`.
164            unsafe { field.eq(row_a, row_b) }
165        })
166    }
167
168    /// Construct a `StaticLayout` for converting BFLATN rows of `row_type` <-> BSATN.
169    ///
170    /// Returns `None` if `row_type` contains a column which does not have a constant length in BSATN,
171    /// either a [`VarLenType`]
172    /// or a [`SumTypeLayout`] whose variants do not have the same "live" unpadded length.
173    pub fn for_row_type(row_type: &RowTypeLayout) -> Option<Self> {
174        if !row_type.layout().fixed {
175            // Don't bother computing the static layout if there are variable components.
176            return None;
177        }
178
179        let mut builder = LayoutBuilder::new_builder();
180        builder.visit_product(row_type.product())?;
181        Some(builder.build())
182    }
183}
184
185/// An identifier for a series of bytes within a BFLATN row
186/// which can be directly copied into an output BSATN buffer
187/// with a known length and offset or vice versa.
188///
189/// Within the row type's BFLATN layout, `row[bflatn_offset .. (bflatn_offset + length)]`
190/// must not contain any padding bytes,
191/// i.e. all of those bytes must be fully initialized if the row is initialized.
192#[derive(PartialEq, Eq, Debug, Copy, Clone)]
193struct MemcpyField {
194    /// Offset in the BFLATN row from which to begin `memcpy`ing, in bytes.
195    bflatn_offset: u16,
196
197    /// Offset in the BSATN buffer to which to begin `memcpy`ing, in bytes.
198    // TODO(perf): Could be a running counter, but this way we just have all the `memcpy` args in one place.
199    // Should bench; I (pgoldman 2024-03-25) suspect this allows more insn parallelism and is therefore better.
200    bsatn_offset: u16,
201
202    /// Length to `memcpy`, in bytes.
203    length: u16,
204}
205
206impl MemoryUsage for MemcpyField {}
207
208impl MemcpyField {
209    /// Copies the bytes at `src[self.bflatn_offset .. self.bflatn_offset + self.length]`
210    /// into `dst[self.bsatn_offset .. self.bsatn_offset + self.length]`.
211    ///
212    /// # Safety
213    ///
214    /// 1. `src.len() >= self.bflatn_offset + self.length`.
215    /// 2. `dst.len() >= self.bsatn_offset + self.length`
216    unsafe fn copy_bflatn_to_bsatn(&self, src: &Bytes, dst: &mut [MaybeUninit<Byte>]) {
217        let src_offset = self.bflatn_offset as usize;
218        let dst_offset = self.bsatn_offset as usize;
219
220        let len = self.length as usize;
221        let src = src.as_ptr();
222        let dst = dst.as_mut_ptr();
223        // SAFETY: per 1., it follows that `src_offset` is in bounds of `src`.
224        let src = unsafe { src.add(src_offset) };
225        // SAFETY: per 2., it follows that `dst_offset` is in bounds of `dst`.
226        let dst = unsafe { dst.add(dst_offset) };
227        let dst = dst.cast();
228
229        // SAFETY:
230        // 1. `src` is valid for reads for `len` bytes per caller requirement 1.
231        //    and because `src` was derived from a shared slice.
232        // 2. `dst` is valid for writes for `len` bytes per caller requirement 2.
233        //    and because `dst` was derived from an exclusive slice.
234        // 3. Alignment for `u8` is trivially satisfied for any pointer.
235        // 4. As `src` and `dst` were derived from shared and exclusive slices, they cannot overlap.
236        unsafe { ptr::copy_nonoverlapping(src, dst, len) }
237    }
238
239    /// Copies the bytes at `src[self.bsatn_offset .. self.bsatn_offset + self.length]`
240    /// into `dst[self.bflatn_offset .. self.bflatn_offset + self.length]`.
241    ///
242    /// # Safety
243    ///
244    /// 1. `src.len() >= self.bsatn_offset + self.length`.
245    /// 2. `dst.len() >= self.bflatn_offset + self.length`
246    unsafe fn copy_bsatn_to_bflatn(&self, src: &Bytes, dst: &mut Bytes) {
247        let src_offset = self.bsatn_offset as usize;
248        let dst_offset = self.bflatn_offset as usize;
249
250        let len = self.length as usize;
251        let src = src.as_ptr();
252        let dst = dst.as_mut_ptr();
253        // SAFETY: per 1., it follows that `src_offset` is in bounds of `src`.
254        let src = unsafe { src.add(src_offset) };
255        // SAFETY: per 2., it follows that `dst_offset` is in bounds of `dst`.
256        let dst = unsafe { dst.add(dst_offset) };
257
258        // SAFETY:
259        // 1. `src` is valid for reads for `len` bytes per caller requirement 1.
260        //    and because `src` was derived from a shared slice.
261        // 2. `dst` is valid for writes for `len` bytes per caller requirement 2.
262        //    and because `dst` was derived from an exclusive slice.
263        // 3. Alignment for `u8` is trivially satisfied for any pointer.
264        // 4. As `src` and `dst` were derived from shared and exclusive slices, they cannot overlap.
265        unsafe { ptr::copy_nonoverlapping(src, dst, len) }
266    }
267
268    /// Compares `row_a` and `row_b` for equality in this field.
269    ///
270    /// # Safety
271    ///
272    /// - `row_a.len() >= self.bflatn_offset + self.length`
273    /// - `row_b.len() >= self.bflatn_offset + self.length`
274    unsafe fn eq(&self, row_a: &Bytes, row_b: &Bytes) -> bool {
275        let range = range_move(0..self.length as usize, self.bflatn_offset as usize);
276        let range2 = range.clone();
277        // SAFETY: The `range` is in bounds as
278        // `row_a.len() >= self.bflatn_offset + self.length >= self.bflatn_offset`.
279        let row_a_field = unsafe { row_a.get_unchecked(range) };
280        // SAFETY: The `range` is in bounds as
281        // `row_b.len() >= self.bflatn_offset + self.length >= self.bflatn_offset`.
282        let row_b_field = unsafe { row_b.get_unchecked(range2) };
283        row_a_field == row_b_field
284    }
285
286    fn is_empty(&self) -> bool {
287        self.length == 0
288    }
289}
290
291/// A builder for a [`StaticLayout`].
292struct LayoutBuilder {
293    /// Always at least one element.
294    fields: Vec<MemcpyField>,
295}
296
297impl LayoutBuilder {
298    fn new_builder() -> Self {
299        Self {
300            fields: vec![MemcpyField {
301                bflatn_offset: 0,
302                bsatn_offset: 0,
303                length: 0,
304            }],
305        }
306    }
307
308    fn build(self) -> StaticLayout {
309        let LayoutBuilder { fields } = self;
310        let fields: Vec<_> = fields.into_iter().filter(|field| !field.is_empty()).collect();
311        let bsatn_length = fields.last().map(|last| last.bsatn_offset + last.length).unwrap_or(0);
312        let fields = fields.into_boxed_slice();
313        StaticLayout { bsatn_length, fields }
314    }
315
316    fn current_field(&self) -> &MemcpyField {
317        self.fields.last().unwrap()
318    }
319
320    fn current_field_mut(&mut self) -> &mut MemcpyField {
321        self.fields.last_mut().unwrap()
322    }
323
324    fn next_bflatn_offset(&self) -> u16 {
325        let last = self.current_field();
326        last.bflatn_offset + last.length
327    }
328
329    fn next_bsatn_offset(&self) -> u16 {
330        let last = self.current_field();
331        last.bsatn_offset + last.length
332    }
333
334    fn visit_product(&mut self, product: &ProductTypeLayout) -> Option<()> {
335        let base_bflatn_offset = self.next_bflatn_offset();
336        for elt in product.elements.iter() {
337            self.visit_product_element(elt, base_bflatn_offset)?;
338        }
339        Some(())
340    }
341
342    fn visit_product_element(&mut self, elt: &ProductTypeElementLayout, product_base_offset: u16) -> Option<()> {
343        let elt_offset = product_base_offset + elt.offset;
344        let next_bflatn_offset = self.next_bflatn_offset();
345        if next_bflatn_offset != elt_offset {
346            // Padding between previous element and this element,
347            // so start a new field.
348            //
349            // Note that this is the only place we have to reason about alignment and padding
350            // because the enclosing `ProductTypeLayout` has already computed valid aligned offsets
351            // for the elements.
352
353            let bsatn_offset = self.next_bsatn_offset();
354            self.fields.push(MemcpyField {
355                bsatn_offset,
356                bflatn_offset: elt_offset,
357                length: 0,
358            });
359        }
360        self.visit_value(&elt.ty)
361    }
362
363    fn visit_value(&mut self, val: &AlgebraicTypeLayout) -> Option<()> {
364        match val {
365            AlgebraicTypeLayout::Sum(sum) => self.visit_sum(sum),
366            AlgebraicTypeLayout::Product(prod) => self.visit_product(prod),
367            AlgebraicTypeLayout::Primitive(prim) => {
368                self.visit_primitive(prim);
369                Some(())
370            }
371
372            // Var-len types (obviously) don't have a known BSATN length,
373            // so fail.
374            AlgebraicTypeLayout::VarLen(_) => None,
375        }
376    }
377
378    fn visit_sum(&mut self, sum: &SumTypeLayout) -> Option<()> {
379        // If the sum has no variants, it's the never type, so there's no point in computing a layout.
380        let first_variant = sum.variants.first()?;
381
382        let variant_layout = |variant: &SumTypeVariantLayout| {
383            let mut builder = LayoutBuilder::new_builder();
384            builder.visit_value(&variant.ty)?;
385            Some(builder.build())
386        };
387
388        // Check that the variants all have the same `StaticLayout`.
389        // If they don't, bail.
390        let first_variant_layout = variant_layout(first_variant)?;
391        for later_variant in &sum.variants[1..] {
392            let later_variant_layout = variant_layout(later_variant)?;
393            if later_variant_layout != first_variant_layout {
394                return None;
395            }
396        }
397
398        if first_variant_layout.bsatn_length == 0 {
399            // For C-style enums (those without payloads),
400            // simply serialize the tag and move on.
401            self.current_field_mut().length += 1;
402            return Some(());
403        }
404
405        // Now that we've reached this point, we know that `first_variant_layout`
406        // applies to the values of all the variants.
407
408        let tag_bflatn_offset = self.next_bflatn_offset();
409        let payload_bflatn_offset = tag_bflatn_offset + sum.payload_offset;
410
411        let tag_bsatn_offset = self.next_bsatn_offset();
412        let payload_bsatn_offset = tag_bsatn_offset + 1;
413
414        // Serialize the tag, consolidating into the previous memcpy if possible.
415        self.visit_primitive(&PrimitiveType::U8);
416
417        if sum.payload_offset > 1 {
418            // Add an empty marker field to keep track of padding.
419            self.fields.push(MemcpyField {
420                bflatn_offset: payload_bflatn_offset,
421                bsatn_offset: payload_bsatn_offset,
422                length: 0,
423            });
424        } // Otherwise, nothing to do.
425
426        // Lay out the variants.
427        // Since all variants have the same layout, we just use the first one.
428        self.visit_value(&first_variant.ty)?;
429
430        Some(())
431    }
432
433    fn visit_primitive(&mut self, prim: &PrimitiveType) {
434        self.current_field_mut().length += prim.size() as u16
435    }
436}
437
438#[cfg(test)]
439mod test {
440    use super::*;
441    use crate::blob_store::HashMapBlobStore;
442    use proptest::prelude::*;
443    use spacetimedb_sats::{bsatn, proptest::generate_typed_row, AlgebraicType, ProductType};
444
445    fn assert_expected_layout(ty: ProductType, bsatn_length: u16, fields: &[(u16, u16, u16)]) {
446        let expected_layout = StaticLayout {
447            bsatn_length,
448            fields: fields
449                .iter()
450                .copied()
451                .map(|(bflatn_offset, bsatn_offset, length)| MemcpyField {
452                    bflatn_offset,
453                    bsatn_offset,
454                    length,
455                })
456                .collect(),
457        };
458        let row_type = RowTypeLayout::from(ty.clone());
459        let Some(computed_layout) = StaticLayout::for_row_type(&row_type) else {
460            panic!("assert_expected_layout: Computed `None` for row {row_type:#?}\nExpected:{expected_layout:#?}");
461        };
462        assert_eq!(
463            computed_layout, expected_layout,
464            "assert_expected_layout: Computed layout (left) doesn't match expected (right) for {ty:?}",
465        );
466    }
467
468    #[test]
469    fn known_types_expected_layout_plain() {
470        for prim in [
471            AlgebraicType::Bool,
472            AlgebraicType::U8,
473            AlgebraicType::I8,
474            AlgebraicType::U16,
475            AlgebraicType::I16,
476            AlgebraicType::U32,
477            AlgebraicType::I32,
478            AlgebraicType::U64,
479            AlgebraicType::I64,
480            AlgebraicType::U128,
481            AlgebraicType::I128,
482            AlgebraicType::U256,
483            AlgebraicType::I256,
484        ] {
485            let size = AlgebraicTypeLayout::from(prim.clone()).size() as u16;
486            assert_expected_layout(ProductType::from([prim]), size, &[(0, 0, size)]);
487        }
488    }
489
490    #[test]
491    fn known_types_expected_layout_complex() {
492        for (ty, bsatn_length, fields) in [
493            (ProductType::new([].into()), 0, &[][..]),
494            (
495                ProductType::from([AlgebraicType::sum([
496                    AlgebraicType::U8,
497                    AlgebraicType::I8,
498                    AlgebraicType::Bool,
499                ])]),
500                2,
501                // In BFLATN, sums have padding after the tag to the max alignment of any variant payload.
502                // In this case, 0 bytes of padding, because all payloads are aligned to 1.
503                // Since there's no padding, the memcpys can be consolidated.
504                &[(0, 0, 2)][..],
505            ),
506            (
507                ProductType::from([AlgebraicType::sum([
508                    AlgebraicType::product([
509                        AlgebraicType::U8,
510                        AlgebraicType::U8,
511                        AlgebraicType::U8,
512                        AlgebraicType::U8,
513                    ]),
514                    AlgebraicType::product([AlgebraicType::U16, AlgebraicType::U16]),
515                    AlgebraicType::U32,
516                ])]),
517                5,
518                // In BFLATN, sums have padding after the tag to the max alignment of any variant payload.
519                // In this case, 3 bytes of padding.
520                &[(0, 0, 1), (4, 1, 4)][..],
521            ),
522            (
523                ProductType::from([
524                    AlgebraicType::sum([AlgebraicType::U128, AlgebraicType::I128]),
525                    AlgebraicType::U32,
526                ]),
527                21,
528                // In BFLATN, sums have padding after the tag to the max alignment of any variant payload.
529                // In this case, 15 bytes of padding.
530                &[(0, 0, 1), (16, 1, 20)][..],
531            ),
532            (
533                ProductType::from([
534                    AlgebraicType::sum([AlgebraicType::U256, AlgebraicType::I256]),
535                    AlgebraicType::U32,
536                ]),
537                37,
538                // In BFLATN, sums have padding after the tag to the max alignment of any variant payload.
539                // In this case, 15 bytes of padding.
540                &[(0, 0, 1), (32, 1, 36)][..],
541            ),
542            (
543                ProductType::from([
544                    AlgebraicType::U256,
545                    AlgebraicType::U128,
546                    AlgebraicType::U64,
547                    AlgebraicType::U32,
548                    AlgebraicType::U16,
549                    AlgebraicType::U8,
550                ]),
551                63,
552                &[(0, 0, 63)][..],
553            ),
554            (
555                ProductType::from([
556                    AlgebraicType::U8,
557                    AlgebraicType::U16,
558                    AlgebraicType::U32,
559                    AlgebraicType::U64,
560                    AlgebraicType::U128,
561                ]),
562                31,
563                &[(0, 0, 1), (2, 1, 30)][..],
564            ),
565            // Make sure sums with no variant data are handled correctly.
566            (
567                ProductType::from([AlgebraicType::sum([AlgebraicType::product::<[AlgebraicType; 0]>([])])]),
568                1,
569                &[(0, 0, 1)][..],
570            ),
571            (
572                ProductType::from([AlgebraicType::sum([
573                    AlgebraicType::product::<[AlgebraicType; 0]>([]),
574                    AlgebraicType::product::<[AlgebraicType; 0]>([]),
575                ])]),
576                1,
577                &[(0, 0, 1)][..],
578            ),
579            // Various experiments with 1-byte-aligned payloads.
580            // These are particularly nice for memcpy consolidation as there's no padding.
581            (
582                ProductType::from([AlgebraicType::sum([
583                    AlgebraicType::product([AlgebraicType::U8, AlgebraicType::U8]),
584                    AlgebraicType::product([AlgebraicType::Bool, AlgebraicType::Bool]),
585                ])]),
586                3,
587                &[(0, 0, 3)][..],
588            ),
589            (
590                ProductType::from([
591                    AlgebraicType::sum([AlgebraicType::Bool, AlgebraicType::U8]),
592                    AlgebraicType::sum([AlgebraicType::U8, AlgebraicType::Bool]),
593                ]),
594                4,
595                &[(0, 0, 4)][..],
596            ),
597            (
598                ProductType::from([
599                    AlgebraicType::U16,
600                    AlgebraicType::sum([AlgebraicType::U8, AlgebraicType::Bool]),
601                    AlgebraicType::U16,
602                ]),
603                6,
604                &[(0, 0, 6)][..],
605            ),
606            (
607                ProductType::from([
608                    AlgebraicType::U32,
609                    AlgebraicType::sum([AlgebraicType::U16, AlgebraicType::I16]),
610                    AlgebraicType::U32,
611                ]),
612                11,
613                &[(0, 0, 5), (6, 5, 6)][..],
614            ),
615        ] {
616            assert_expected_layout(ty, bsatn_length, fields);
617        }
618    }
619
620    #[test]
621    fn known_types_not_applicable() {
622        for ty in [
623            AlgebraicType::String,
624            AlgebraicType::bytes(),
625            AlgebraicType::never(),
626            AlgebraicType::array(AlgebraicType::U16),
627            AlgebraicType::sum([AlgebraicType::U8, AlgebraicType::U16]),
628        ] {
629            let layout = RowTypeLayout::from(ProductType::from([ty]));
630            if let Some(computed) = StaticLayout::for_row_type(&layout) {
631                panic!("Expected row type not to have a constant BSATN layout!\nRow type: {layout:#?}\nBSATN layout: {computed:#?}");
632            }
633        }
634    }
635
636    proptest! {
637        // The tests `known_bsatn_same_as_bflatn_from`
638        // and `known_bflatn_same_as_pv_from` generate a lot of rejects,
639        // as a vast majority of the space of `ProductType` does not have a fixed BSATN length.
640        // Writing a proptest generator which produces only types that have a fixed BSATN length
641        // seems hard, because we'd have to generate sums with known matching layouts,
642        // so we just bump the `max_global_rejects` up as high as it'll go and move on with our lives.
643        //
644        // Note that I (pgoldman 2024-03-21) tried modifying `generate_typed_row`
645        // to not emit `String`, `Array` or `Map` types (the trivially var-len types),
646        // but did not see a meaningful decrease in the number of rejects.
647        // This is because a majority of the var-len BSATN types in the `generate_typed_row` space
648        // are due to sums with inconsistent payload layouts.
649        //
650        // We still include the test `known_bsatn_same_as_bsatn_from`
651        // because it tests row types not covered in `known_types_expected_layout`,
652        // especially larger types with unusual sequences of aligned fields.
653        #![proptest_config(ProptestConfig { max_global_rejects: 65536, ..Default::default()})]
654
655        #[test]
656        fn known_bsatn_same_as_bflatn_from((ty, val) in generate_typed_row()) {
657            let mut blob_store = HashMapBlobStore::default();
658            let mut table = crate::table::test::table(ty);
659            let Some(static_layout) = table.static_layout().cloned() else {
660                // `ty` has a var-len member or a sum with different payload lengths,
661                // so the fast path doesn't apply.
662                return Err(TestCaseError::reject("Var-length type"));
663            };
664
665            let (_, row_ref) = table.insert(&mut blob_store, &val).unwrap();
666            let bytes = row_ref.get_row_data();
667
668            let slow_path = bsatn::to_vec(&row_ref).unwrap();
669
670            let fast_path = unsafe {
671                static_layout.serialize_row_into_vec(bytes)
672            };
673
674            let mut fast_path2 = Vec::new();
675            unsafe {
676                static_layout.serialize_row_extend(&mut fast_path2, bytes)
677            };
678
679            assert_eq!(slow_path, fast_path);
680            assert_eq!(slow_path, fast_path2);
681        }
682
683        #[test]
684        fn known_bflatn_same_as_pv_from((ty, val) in generate_typed_row()) {
685            let mut blob_store = HashMapBlobStore::default();
686            let mut table = crate::table::test::table(ty);
687            let Some(static_layout) = table.static_layout().cloned() else {
688                // `ty` has a var-len member or a sum with different payload lengths,
689                // so the fast path doesn't apply.
690                return Err(TestCaseError::reject("Var-length type"));
691            };
692            let bsatn = bsatn::to_vec(&val).unwrap();
693
694            let (_, row_ref) = table.insert(&mut blob_store, &val).unwrap();
695            let slow_path = row_ref.get_row_data();
696
697            let mut fast_path = vec![0u8; slow_path.len()];
698            unsafe {
699                static_layout.deserialize_row_into(&mut fast_path, &bsatn);
700            };
701
702            assert_eq!(slow_path, fast_path);
703        }
704    }
705}