pinapod 0.4.3

Zero-copy pod types with derive macros. Alignment-1 representations for zero-overhead data access.
Documentation
#![cfg(feature = "floats")]
#![allow(
	missing_docs,
	reason = "these are test fixtures rather than a published surface, so their schemas stay \
	          undocumented"
)]

use core::mem::align_of;
use core::mem::size_of;
use std::vec::Vec as StdVec;

use pinapod::PinaPod;
use pinapod::ZcElem;
use pinapod::ZcField;
use pinapod::ZcValidate;
use pinapod::pod::PodF32;
use pinapod::pod::PodF64;

fn assert_mapping<T, Pod>()
where
	T: ZcField<Pod = Pod>,
	Pod: ZcElem,
{
	assert_eq!(size_of::<T::Pod>(), size_of::<Pod>());
	assert_eq!(align_of::<Pod>(), 1);
}

#[test]
fn float_primitives_map_to_alignment_one_pods() {
	assert_mapping::<f32, PodF32>();
	assert_mapping::<f64, PodF64>();

	// The native spelling is accepted anywhere `ZcField` is required, which is
	// what lets a schema declare `f32`/`f64` directly.
	fn assert_zc_elem<T: ZcElem>() {}
	assert_zc_elem::<PodF32>();
	assert_zc_elem::<PodF64>();
}

#[test]
fn float_pods_roundtrip_bit_patterns_exactly() {
	for value in [
		0.0_f32,
		-0.0,
		-1.5,
		core::f32::consts::PI,
		f32::MIN_POSITIVE,
		f32::MAX,
		f32::INFINITY,
		f32::NEG_INFINITY,
		f32::from_bits(0x7f80_0001), // signaling NaN payload
		f32::from_bits(0xffc0_0000), // negative quiet NaN
	] {
		let pod = PodF32::from(value);
		assert_eq!(pod.to_bits(), value.to_bits(), "bits changed for {value}");
		assert_eq!(pod.get().to_bits(), value.to_bits());
		assert_eq!(f32::from(pod).to_bits(), value.to_bits());
	}

	for value in [0.0_f64, -2.25, f64::MAX, f64::NEG_INFINITY] {
		let pod = PodF64::from(value);
		assert_eq!(pod.to_bits(), value.to_bits());
		assert_eq!(pod.get(), value);
	}
}

#[test]
fn float_storage_is_little_endian() {
	let mut pod = PodF32::ZERO;
	pod.set(1.5);
	assert_eq!(pod.as_ref(), &1.5_f32.to_bits().to_le_bytes());

	pod.set_bits(0x4049_0fdb);
	assert_eq!(pod.get().to_bits(), 0x4049_0fdb);
	assert_eq!(pod.to_bits(), 0x4049_0fdb);

	let mut wide = PodF64::ZERO;
	wide.set(3.125);
	assert_eq!(wide.as_ref(), &3.125_f64.to_bits().to_le_bytes());
	assert_eq!(
		PodF64::new_from_array(3.125_f64.to_bits().to_le_bytes()),
		wide
	);
}

#[test]
fn equality_is_bitwise_not_float_valued() {
	assert_eq!(PodF32::from(1.5), PodF32::from(1.5));
	// `+0.0 == -0.0` under float semantics, but stored bits differ.
	assert_ne!(PodF32::from(0.0), PodF32::from(-0.0));

	let quiet_nan = PodF32::from(f32::NAN);
	assert_eq!(quiet_nan, quiet_nan, "NaN payloads compare bitwise");

	assert!(PodF32::ZERO.is_zero());
	assert!(!PodF32::from(-0.0).is_zero());

	// Ordering is intentionally absent: bitwise `Eq` and float ordering
	// cannot both hold (NaN payloads, `+0.0` vs `-0.0`). Decode with `get`
	// and compare the natives instead.
	assert_eq!(
		PodF32::from(1.0)
			.get()
			.partial_cmp(&PodF32::from(2.0).get()),
		Some(core::cmp::Ordering::Less)
	);
}

#[test]
fn every_bit_pattern_is_a_valid_stored_value() {
	// Validation cannot fail for any bit pattern, including NaN payloads and
	// the negative zero the float semantics would treat as equal to zero.
	for bits in [0u32, 1, 0x7f80_0001, 0xffc0_0000, u32::MAX] {
		let mut pod = PodF32::ZERO;
		pod.set_bits(bits);
		assert!(PodF32::validate_ref(&pod).is_ok());
		assert_eq!(pod.to_bits(), bits);
	}

	let mut wide = PodF64::ZERO;
	wide.set(f64::NAN);
	assert!(PodF64::validate_ref(&wide).is_ok());
}

#[allow(dead_code)]
#[derive(PinaPod)]
struct FloatAccount {
	pub temperature: f32,
	pub depth: f64,
	pub previous_depth: Option<f64>,
	pub bias: PodF32,
}

#[test]
fn fixed_schema_roundtrips_native_float_fields() {
	let mut bytes = [0u8; FloatAccount::SIZE];
	assert_eq!(size_of::<PodF32>(), 4);
	assert_eq!(size_of::<PodF64>(), 8);

	{
		let account = FloatAccount::read_exact_mut(&mut bytes).unwrap();
		account.temperature.set(-12.5);
		account.depth.set(3.125);
		account.previous_depth.set(Some(PodF64::from(2.0)));
		account.bias.set(0.25);
	}

	assert_eq!(&bytes[..4], &(-12.5_f32).to_bits().to_le_bytes());
	assert_eq!(&bytes[4..12], &3.125_f64.to_bits().to_le_bytes());

	let account = FloatAccount::read_exact(&bytes).unwrap();
	// The generated accessor decodes back to the native float.
	assert_eq!(account.temperature(), -12.5);
	assert_eq!(account.depth(), 3.125);
	assert_eq!(
		account.previous_depth.get().map(|value| value.get()),
		Some(2.0)
	);
	assert_eq!(account.bias.get(), 0.25);
}

#[allow(dead_code)]
#[derive(PinaPod)]
#[pinapod(compact)]
struct FloatBook {
	pub mark_price: f32,
	pub venues: pinapod::Vec<f32, 4>,
	pub name: pinapod::String<8>,
}

#[test]
fn compact_schema_roundtrips_float_header_and_tail() {
	let mut bytes = [0u8; FloatBook::MAX_SIZE];
	let prices = [1.0_f32, 2.5];
	let mapped = prices.map(PodF32::from);

	let patch = FloatBookPatch::new()
		.mark_price(1.75)
		.replace_venues(&mapped)
		.name("float");
	let encoded_size = FloatBook::initialize(&mut bytes, &patch).unwrap();

	assert_eq!(
		encoded_size,
		FloatBook::HEADER_SIZE + mapped.len() * size_of::<PodF32>() + "float".len(),
	);

	let book = FloatBook::read_prefix(&bytes[..encoded_size]).unwrap();
	assert_eq!(book.mark_price.get(), 1.75);
	assert_eq!(
		book.venues().iter().map(PodF32::get).collect::<StdVec<_>>(),
		prices,
	);
	assert_eq!(book.name(), "float");
}

#[test]
fn nan_payloads_survive_a_schema_round_trip() {
	let mut bytes = [0u8; FloatAccount::SIZE];
	{
		let account = FloatAccount::read_exact_mut(&mut bytes).unwrap();
		account.temperature.set_bits(0x7fc0_0001);
		account.depth.set(f64::NAN);
	}

	let account = FloatAccount::read_exact(&bytes).unwrap();
	assert_eq!(account.temperature.get().to_bits(), 0x7fc0_0001);
	assert!(account.depth.get().is_nan());
}

#[allow(dead_code)]
#[derive(PinaPod)]
struct FloatCollections {
	pub samples: Option<f32>,
	pub history: pinapod::Vec<f64, 3>,
}

#[test]
fn floats_compose_with_bounded_collections() {
	let mut bytes = [0u8; FloatCollections::SIZE];
	let history = [1.0_f64, -2.5];
	let mapped = history.map(PodF64::from);

	{
		let value = FloatCollections::read_exact_mut(&mut bytes).unwrap();
		value.samples.set(Some(PodF32::from(0.5)));
		value.history.try_set(mapped).unwrap();
	}

	let value = FloatCollections::read_exact(&bytes).unwrap();
	assert_eq!(value.samples.get().map(|sample| sample.get()), Some(0.5));
	assert_eq!(
		value.history.iter().map(PodF64::get).collect::<StdVec<_>>(),
		history,
	);
}

#[test]
fn float_pods_expose_formatting_hashing_and_constants() {
	let pod = PodF32::from(1.5);
	assert_eq!(std::format!("{pod}"), "1.5");
	assert_eq!(std::format!("{pod:?}"), "1.5");
	assert_eq!(std::format!("{pod:x}"), "3fc00000");
	assert_eq!(std::format!("{pod:X}"), "3FC00000");
	assert_eq!(std::format!("{pod:b}"), "111111110000000000000000000000");
	assert_eq!(std::format!("{}", PodF64::from(3.125)), "3.125");

	// Bitwise hashing agrees for equal patterns.
	let mut hasher = std::collections::hash_map::DefaultHasher::new();
	std::hash::Hash::hash(&pod, &mut hasher);
	let first = std::hash::Hasher::finish(&hasher);
	let mut hasher = std::collections::hash_map::DefaultHasher::new();
	std::hash::Hash::hash(&PodF32::from(1.5), &mut hasher);
	assert_eq!(first, std::hash::Hasher::finish(&hasher));

	assert_eq!(PodF32::default(), PodF32::ZERO);
	assert_eq!(PodF32::MIN_POSITIVE.to_bits(), f32::MIN_POSITIVE.to_bits());
	assert_eq!(PodF32::MAX.to_bits(), f32::MAX.to_bits());
	assert_eq!(PodF64::MAX.to_bits(), f64::MAX.to_bits());

	// No `Ord`/`PartialOrd`/`max` on the pods themselves.
	assert!(PodF32::from(1.0).get() < PodF32::from(2.0).get());
}

#[test]
fn float_pods_copy_and_convert_like_byte_containers() {
	let pod = PodF32::from(-0.75);
	let copied = pod; // Copy
	#[allow(clippy::clone_on_copy)]
	let cloned = pod.clone();
	assert_eq!(copied, cloned);
	assert_eq!(f32::from(copied), -0.75);
	assert_eq!(
		PodF64::from(f64::from(PodF64::from(1.25))),
		PodF64::from(1.25)
	);
	assert_eq!(<StdVec<_>>::from([pod]).len(), 1);
}

#[test]
fn float_arrays_accept_every_bit_pattern() {
	// Float pods override the per-element array walk with a no-op because NaN,
	// infinities, and both zero signs are all valid stored values.
	let f32s = [
		PodF32::from(f32::NAN),
		PodF32::from(f32::INFINITY),
		PodF32::from(-0.0),
		PodF32::ZERO,
	];
	ZcValidate::validate_ref(&f32s).unwrap();

	let f64s = [PodF64::from(f64::NEG_INFINITY), PodF64::from(f64::MAX)];
	ZcValidate::validate_ref(&f64s).unwrap();

	// Every raw bit pattern is valid too, including signaling NaN payloads,
	// which a native `f32` field could never hold.
	let signaling_nan = PodF32::new_from_array(0x7F80_0001_u32.to_le_bytes());
	let all_ones = PodF32::new_from_array(0xFFFF_FFFF_u32.to_le_bytes());
	let raw = [signaling_nan, all_ones];
	ZcValidate::validate_ref(&raw).unwrap();
}