use core::ops::Deref;
use std::sync::Arc;
use crate::metal::GpuBuffer;
use crate::pool::ScratchPool;
pub const TAG_SHIFT: u32 = 56;
pub const PAYLOAD_MASK: u64 = (1 << TAG_SHIFT) - 1;
pub const CONTAINER_INDEX_MASK: u64 = 0xFFFF_FFFF;
pub const CONTAINER_COUNT_SHIFT: u32 = 32;
pub const CONTAINER_COUNT_MAX: u32 = 0xFF_FFFF;
pub const STRING_OFFSET_MASK: u64 = PAYLOAD_MASK;
pub const STRING_RECORD_HEADER_BYTES: usize = 4;
pub const STRING_RECORD_TRAILER_BYTES: usize = 1;
pub const TAG_ROOT: u8 = b'r';
pub const TAG_START_OBJECT: u8 = b'{';
pub const TAG_END_OBJECT: u8 = b'}';
pub const TAG_START_ARRAY: u8 = b'[';
pub const TAG_END_ARRAY: u8 = b']';
pub const TAG_STRING: u8 = b'"';
pub const TAG_INT64: u8 = b'l';
pub const TAG_UINT64: u8 = b'u';
pub const TAG_DOUBLE: u8 = b'd';
pub const TAG_TRUE: u8 = b't';
pub const TAG_FALSE: u8 = b'f';
pub const TAG_NULL: u8 = b'n';
#[inline]
#[must_use]
pub const fn make_entry(tag: u8, payload: u64) -> u64 {
debug_assert!(payload <= PAYLOAD_MASK, "tape payload exceeds 56 bits");
((tag as u64) << TAG_SHIFT) | (payload & PAYLOAD_MASK)
}
#[inline]
#[must_use]
pub const fn make_root(final_root_index: u64) -> u64 {
make_entry(TAG_ROOT, final_root_index)
}
#[inline]
#[must_use]
pub const fn make_final_root() -> u64 {
make_entry(TAG_ROOT, 0)
}
#[inline]
#[must_use]
pub const fn make_open(tag: u8, end_index: u32, count: u32) -> u64 {
debug_assert!(
tag == TAG_START_OBJECT || tag == TAG_START_ARRAY,
"make_open requires '{{' or '['"
);
let saturated = if count > CONTAINER_COUNT_MAX {
CONTAINER_COUNT_MAX
} else {
count
};
make_entry(
tag,
((saturated as u64) << CONTAINER_COUNT_SHIFT) | (end_index as u64),
)
}
#[inline]
#[must_use]
pub const fn make_close(tag: u8, open_index: u32) -> u64 {
debug_assert!(
tag == TAG_END_OBJECT || tag == TAG_END_ARRAY,
"make_close requires '}}' or ']'"
);
make_entry(tag, open_index as u64)
}
#[inline]
#[must_use]
pub const fn make_string(offset: u64) -> u64 {
debug_assert!(
offset <= STRING_OFFSET_MASK,
"string offset exceeds 56 bits"
);
make_entry(TAG_STRING, offset)
}
#[inline]
#[must_use]
pub const fn make_int64_marker() -> u64 {
make_entry(TAG_INT64, 0)
}
#[inline]
#[must_use]
pub const fn make_uint64_marker() -> u64 {
make_entry(TAG_UINT64, 0)
}
#[inline]
#[must_use]
pub const fn make_double_marker() -> u64 {
make_entry(TAG_DOUBLE, 0)
}
#[inline]
#[must_use]
pub const fn make_true() -> u64 {
make_entry(TAG_TRUE, 0)
}
#[inline]
#[must_use]
pub const fn make_false() -> u64 {
make_entry(TAG_FALSE, 0)
}
#[inline]
#[must_use]
pub const fn make_null() -> u64 {
make_entry(TAG_NULL, 0)
}
#[inline]
#[must_use]
pub const fn int64_bits(value: i64) -> u64 {
value as u64
}
#[inline]
#[must_use]
pub const fn double_bits(value: f64) -> u64 {
value.to_bits()
}
#[inline]
#[must_use]
pub const fn tag(word: u64) -> u8 {
(word >> TAG_SHIFT) as u8
}
#[inline]
#[must_use]
pub const fn payload(word: u64) -> u64 {
word & PAYLOAD_MASK
}
#[inline]
#[must_use]
pub const fn root_final_index(word: u64) -> u64 {
payload(word)
}
#[inline]
#[must_use]
pub const fn container_end_index(word: u64) -> u32 {
(word & CONTAINER_INDEX_MASK) as u32
}
#[inline]
#[must_use]
pub const fn container_count(word: u64) -> u32 {
((word >> CONTAINER_COUNT_SHIFT) & (CONTAINER_COUNT_MAX as u64)) as u32
}
#[inline]
#[must_use]
pub const fn container_open_index(word: u64) -> u32 {
(word & CONTAINER_INDEX_MASK) as u32
}
#[inline]
#[must_use]
pub const fn string_offset(word: u64) -> u64 {
payload(word)
}
#[inline]
#[must_use]
pub const fn int64_from_bits(word: u64) -> i64 {
word as i64
}
#[inline]
#[must_use]
pub const fn double_from_bits(word: u64) -> f64 {
f64::from_bits(word)
}
#[derive(Debug)]
pub(crate) struct PooledGpuBytes {
buf: Option<GpuBuffer>,
pool: Arc<ScratchPool>,
}
impl PooledGpuBytes {
pub(crate) fn new(buf: GpuBuffer, pool: Arc<ScratchPool>) -> Self {
Self {
buf: Some(buf),
pool,
}
}
fn buffer(&self) -> &GpuBuffer {
self.buf.as_ref().expect("buffer present until drop")
}
}
impl Drop for PooledGpuBytes {
fn drop(&mut self) {
if let Some(buf) = self.buf.take() {
self.pool.put_back(buf);
}
}
}
enum TapeStorage {
Vec(Vec<u64>),
Gpu(PooledGpuBytes),
}
#[derive(Default)]
pub struct TapeBuffer {
storage: TapeStorage,
}
impl Default for TapeStorage {
fn default() -> Self {
Self::Vec(Vec::new())
}
}
impl TapeBuffer {
#[must_use]
pub const fn new() -> Self {
Self {
storage: TapeStorage::Vec(Vec::new()),
}
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
storage: TapeStorage::Vec(Vec::with_capacity(capacity)),
}
}
pub(crate) fn from_gpu(buf: GpuBuffer, pool: Arc<ScratchPool>) -> Self {
Self {
storage: TapeStorage::Gpu(PooledGpuBytes::new(buf, pool)),
}
}
fn vec_mut(&mut self, op: &str) -> &mut Vec<u64> {
match &mut self.storage {
TapeStorage::Vec(words) => words,
TapeStorage::Gpu(_) => {
panic!("TapeBuffer::{op}: GPU-backed tapes are finished artifacts (immutable)")
}
}
}
#[inline]
pub fn push(&mut self, word: u64) -> usize {
let words = self.vec_mut("push");
let index = words.len();
words.push(word);
index
}
#[inline]
pub fn set(&mut self, index: usize, word: u64) {
self.vec_mut("set")[index] = word;
}
#[inline]
#[must_use]
pub fn as_words(&self) -> &[u64] {
self
}
pub fn clear(&mut self) {
self.vec_mut("clear").clear();
}
}
impl Deref for TapeBuffer {
type Target = [u64];
#[inline]
fn deref(&self) -> &[u64] {
match &self.storage {
TapeStorage::Vec(words) => words,
TapeStorage::Gpu(gpu) => gpu.buffer().as_slice::<u64>(),
}
}
}
impl Clone for TapeBuffer {
fn clone(&self) -> Self {
Self {
storage: TapeStorage::Vec(self.as_words().to_vec()),
}
}
}
impl std::fmt::Debug for TapeBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TapeBuffer")
.field("len", &self.len())
.field(
"storage",
&match &self.storage {
TapeStorage::Vec(_) => "vec",
TapeStorage::Gpu(_) => "gpu",
},
)
.finish()
}
}
#[derive(Default)]
pub struct StringBuffer {
storage: StringStorage,
}
enum StringStorage {
Vec(Vec<u8>),
Gpu(PooledGpuBytes),
}
impl Default for StringStorage {
fn default() -> Self {
Self::Vec(Vec::new())
}
}
impl StringBuffer {
#[must_use]
pub const fn new() -> Self {
Self {
storage: StringStorage::Vec(Vec::new()),
}
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
storage: StringStorage::Vec(Vec::with_capacity(capacity)),
}
}
pub(crate) fn from_gpu(buf: GpuBuffer, pool: Arc<ScratchPool>) -> Self {
Self {
storage: StringStorage::Gpu(PooledGpuBytes::new(buf, pool)),
}
}
fn bytes(&self) -> &[u8] {
match &self.storage {
StringStorage::Vec(bytes) => bytes,
StringStorage::Gpu(gpu) => gpu.buffer().contents(),
}
}
fn vec_mut(&mut self, op: &str) -> &mut Vec<u8> {
match &mut self.storage {
StringStorage::Vec(bytes) => bytes,
StringStorage::Gpu(_) => panic!(
"StringBuffer::{op}: GPU-backed string buffers are finished artifacts (immutable)"
),
}
}
pub fn append_record(&mut self, content: &[u8]) -> u64 {
let len = u32::try_from(content.len()).expect("string longer than u32::MAX bytes");
let bytes = self.vec_mut("append_record");
let offset = bytes.len() as u64;
assert!(
offset <= STRING_OFFSET_MASK,
"string buffer offset exceeds 56 bits"
);
bytes.reserve(STRING_RECORD_HEADER_BYTES + content.len() + STRING_RECORD_TRAILER_BYTES);
bytes.extend_from_slice(&len.to_le_bytes());
bytes.extend_from_slice(content);
bytes.push(0);
offset
}
pub fn pad_to(&mut self, offset: u64) {
let target = usize::try_from(offset).expect("string offset exceeds usize");
let bytes = self.vec_mut("pad_to");
assert!(
target >= bytes.len(),
"pad_to({target}) would shrink the buffer (len {})",
bytes.len()
);
bytes.resize(target, 0);
}
pub fn append_record_at(&mut self, offset: u64, content: &[u8]) -> u64 {
self.pad_to(offset);
self.append_record(content)
}
#[must_use]
pub fn record_bytes(&self, offset: u64) -> &[u8] {
let bytes = self.bytes();
let start = usize::try_from(offset).expect("string offset exceeds usize");
let header: [u8; STRING_RECORD_HEADER_BYTES] = bytes
[start..start + STRING_RECORD_HEADER_BYTES]
.try_into()
.expect("string record header out of bounds");
let len = u32::from_le_bytes(header) as usize;
let content_start = start + STRING_RECORD_HEADER_BYTES;
&bytes[content_start..content_start + len]
}
#[must_use]
pub fn record_str(&self, offset: u64) -> &str {
core::str::from_utf8(self.record_bytes(offset))
.expect("string record content is not valid UTF-8")
}
#[must_use]
pub fn len(&self) -> usize {
self.bytes().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.bytes().is_empty()
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
self.bytes()
}
pub fn clear(&mut self) {
self.vec_mut("clear").clear();
}
}
impl Clone for StringBuffer {
fn clone(&self) -> Self {
Self {
storage: StringStorage::Vec(self.bytes().to_vec()),
}
}
}
impl std::fmt::Debug for StringBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StringBuffer")
.field("len", &self.len())
.field(
"storage",
&match &self.storage {
StringStorage::Vec(_) => "vec",
StringStorage::Gpu(_) => "gpu",
},
)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
const ALL_TAGS: [u8; 12] = [
TAG_ROOT,
TAG_START_OBJECT,
TAG_END_OBJECT,
TAG_START_ARRAY,
TAG_END_ARRAY,
TAG_STRING,
TAG_INT64,
TAG_UINT64,
TAG_DOUBLE,
TAG_TRUE,
TAG_FALSE,
TAG_NULL,
];
#[test]
fn tag_values_are_the_ascii_tape_characters() {
assert_eq!(TAG_ROOT, 0x72);
assert_eq!(TAG_START_OBJECT, 0x7B);
assert_eq!(TAG_END_OBJECT, 0x7D);
assert_eq!(TAG_START_ARRAY, 0x5B);
assert_eq!(TAG_END_ARRAY, 0x5D);
assert_eq!(TAG_STRING, 0x22);
assert_eq!(TAG_INT64, 0x6C);
assert_eq!(TAG_UINT64, 0x75);
assert_eq!(TAG_DOUBLE, 0x64);
assert_eq!(TAG_TRUE, 0x74);
assert_eq!(TAG_FALSE, 0x66);
assert_eq!(TAG_NULL, 0x6E);
}
#[test]
fn entry_round_trips_every_tag_and_payload_extremes() {
for t in ALL_TAGS {
for p in [0u64, 1, 0xDEAD_BEEF, CONTAINER_INDEX_MASK, PAYLOAD_MASK] {
let word = make_entry(t, p);
assert_eq!(tag(word), t, "tag round trip for {t:#x} payload {p:#x}");
assert_eq!(payload(word), p, "payload round trip for {t:#x}");
}
}
}
#[test]
fn max_payload_does_not_bleed_into_tag() {
let word = make_entry(TAG_STRING, PAYLOAD_MASK);
assert_eq!(tag(word), TAG_STRING);
assert_eq!(payload(word), PAYLOAD_MASK);
assert_eq!(word, 0x22FF_FFFF_FFFF_FFFF);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "tape payload exceeds 56 bits")]
fn oversized_payload_panics_in_debug() {
let _ = make_entry(TAG_STRING, PAYLOAD_MASK + 1);
}
#[test]
fn open_word_round_trips_index_and_count() {
let word = make_open(TAG_START_OBJECT, 12, 2);
assert_eq!(tag(word), TAG_START_OBJECT);
assert_eq!(container_end_index(word), 12);
assert_eq!(container_count(word), 2);
assert_eq!(word, 0x7B00_0002_0000_000C);
let word = make_open(TAG_START_ARRAY, u32::MAX, 0);
assert_eq!(tag(word), TAG_START_ARRAY);
assert_eq!(container_end_index(word), u32::MAX);
assert_eq!(container_count(word), 0);
}
#[test]
fn open_word_count_saturates_at_24_bits() {
let word = make_open(TAG_START_ARRAY, 7, CONTAINER_COUNT_MAX - 1);
assert_eq!(container_count(word), CONTAINER_COUNT_MAX - 1);
let word = make_open(TAG_START_ARRAY, 7, CONTAINER_COUNT_MAX);
assert_eq!(container_count(word), CONTAINER_COUNT_MAX);
for over in [CONTAINER_COUNT_MAX + 1, u32::MAX] {
let word = make_open(TAG_START_OBJECT, 7, over);
assert_eq!(
container_count(word),
CONTAINER_COUNT_MAX,
"count {over:#x}"
);
assert_eq!(container_end_index(word), 7);
assert_eq!(tag(word), TAG_START_OBJECT);
}
}
#[test]
fn close_word_round_trips_open_index() {
for index in [0u32, 1, 0x00FF_FFFF, u32::MAX] {
let word = make_close(TAG_END_OBJECT, index);
assert_eq!(tag(word), TAG_END_OBJECT);
assert_eq!(container_open_index(word), index);
}
assert_eq!(tag(make_close(TAG_END_ARRAY, 3)), TAG_END_ARRAY);
assert_eq!(make_close(TAG_END_ARRAY, 3), 0x5D00_0000_0000_0003);
}
#[test]
fn string_word_round_trips_56_bit_offsets() {
for offset in [0u64, 1, 0xABCD_EF01, STRING_OFFSET_MASK] {
let word = make_string(offset);
assert_eq!(tag(word), TAG_STRING);
assert_eq!(string_offset(word), offset, "offset {offset:#x}");
}
}
#[test]
fn root_words() {
let first = make_root(12);
assert_eq!(tag(first), TAG_ROOT);
assert_eq!(root_final_index(first), 12);
assert_eq!(first, 0x7200_0000_0000_000C);
let last = make_final_root();
assert_eq!(tag(last), TAG_ROOT);
assert_eq!(payload(last), 0);
assert_eq!(last, 0x7200_0000_0000_0000);
assert_eq!(root_final_index(make_root(PAYLOAD_MASK)), PAYLOAD_MASK);
}
#[test]
fn number_markers_have_zero_payload() {
assert_eq!(tag(make_int64_marker()), TAG_INT64);
assert_eq!(payload(make_int64_marker()), 0);
assert_eq!(tag(make_uint64_marker()), TAG_UINT64);
assert_eq!(payload(make_uint64_marker()), 0);
assert_eq!(tag(make_double_marker()), TAG_DOUBLE);
assert_eq!(payload(make_double_marker()), 0);
}
#[test]
fn int64_value_word_round_trips() {
for v in [0i64, 1, -1, 42, i64::MIN, i64::MAX] {
assert_eq!(int64_from_bits(int64_bits(v)), v, "i64 {v}");
}
assert_eq!(int64_bits(-1), u64::MAX);
}
#[test]
fn double_value_word_round_trips_bit_exactly() {
for v in [
0.0f64,
-0.0,
2.5,
-1.0e308,
f64::MIN_POSITIVE,
5e-324, f64::INFINITY,
f64::NEG_INFINITY,
] {
let word = double_bits(v);
assert_eq!(double_from_bits(word).to_bits(), v.to_bits(), "f64 {v:e}");
}
assert_ne!(double_bits(0.0), double_bits(-0.0));
let weird_nan = f64::from_bits(0x7FF8_0000_DEAD_BEEF);
assert_eq!(
double_from_bits(double_bits(weird_nan)).to_bits(),
0x7FF8_0000_DEAD_BEEF
);
assert_eq!(double_bits(2.5), 0x4004_0000_0000_0000);
}
#[test]
fn literal_words() {
assert_eq!(tag(make_true()), TAG_TRUE);
assert_eq!(payload(make_true()), 0);
assert_eq!(tag(make_false()), TAG_FALSE);
assert_eq!(payload(make_false()), 0);
assert_eq!(tag(make_null()), TAG_NULL);
assert_eq!(payload(make_null()), 0);
}
#[test]
fn tape_buffer_push_set_and_deref() {
let mut tape = TapeBuffer::new();
assert!(tape.is_empty());
assert_eq!(tape.push(make_root(0)), 0);
assert_eq!(tape.push(make_null()), 1);
assert_eq!(tape.push(make_final_root()), 2);
assert_eq!(tape.len(), 3);
tape.set(0, make_root(2));
assert_eq!(root_final_index(tape[0]), 2);
let via_deref: &[u64] = &tape;
assert_eq!(via_deref, tape.as_words());
assert_eq!(
tape.as_words(),
&[make_root(2), make_null(), make_final_root()]
);
tape.clear();
assert!(tape.is_empty());
}
#[test]
fn string_buffer_record_layout() {
let mut buf = StringBuffer::new();
assert!(buf.is_empty());
let off_empty = buf.append_record(b"");
assert_eq!(off_empty, 0);
assert_eq!(buf.as_bytes(), &[0, 0, 0, 0, 0]);
assert_eq!(buf.record_bytes(off_empty), b"");
let off_a = buf.append_record(b"a");
assert_eq!(off_a, 5);
assert_eq!(buf.record_str(off_a), "a");
assert_eq!(&buf.as_bytes()[5..11], &[1, 0, 0, 0, b'a', 0]);
let off_nul = buf.append_record(b"x\0y");
assert_eq!(off_nul, 11);
assert_eq!(buf.record_bytes(off_nul), b"x\0y");
assert_eq!(buf.record_str(off_nul), "x\0y");
assert_eq!(buf.len(), 19);
buf.clear();
assert!(buf.is_empty());
}
#[test]
fn append_record_at_zero_fills_gaps() {
let mut buf = StringBuffer::new();
assert_eq!(buf.append_record_at(0, b"ab"), 0);
assert_eq!(buf.len(), 7);
assert_eq!(buf.append_record_at(10, b"c"), 10);
assert_eq!(
buf.as_bytes(),
&[
2, 0, 0, 0, b'a', b'b', 0, 0, 0, 0, 1, 0, 0, 0, b'c', 0, ]
);
assert_eq!(buf.record_bytes(0), b"ab");
assert_eq!(buf.record_str(10), "c");
assert_eq!(buf.append_record_at(16, b""), 16);
assert_eq!(buf.len(), 21);
}
#[test]
fn pad_to_extends_with_zeros_and_is_idempotent() {
let mut buf = StringBuffer::new();
buf.append_record(b"x"); buf.pad_to(9); assert_eq!(buf.len(), 9);
assert_eq!(&buf.as_bytes()[6..], &[0, 0, 0]);
buf.pad_to(9);
assert_eq!(buf.len(), 9);
assert_eq!(buf.record_str(0), "x");
}
#[test]
#[should_panic(expected = "would shrink the buffer")]
fn pad_to_panics_on_backward_offsets() {
let mut buf = StringBuffer::new();
buf.append_record(b"hello");
buf.pad_to(3);
}
#[test]
fn string_buffer_length_prefix_is_little_endian_u32() {
let mut buf = StringBuffer::new();
let content = vec![b'z'; 0x0102]; let off = buf.append_record(&content);
assert_eq!(off, 0);
assert_eq!(&buf.as_bytes()[..4], &[0x02, 0x01, 0x00, 0x00]);
assert_eq!(buf.record_bytes(off), &content[..]);
assert_eq!(buf.as_bytes()[4 + 0x0102], 0);
assert_eq!(buf.len(), 4 + 0x0102 + 1);
}
#[test]
fn string_buffer_multibyte_utf8_round_trips() {
let mut buf = StringBuffer::new();
let s = "héllo \u{1F600} wörld";
let off = buf.append_record(s.as_bytes());
assert_eq!(buf.record_str(off), s);
}
#[test]
fn worked_example_matches_tape_format_doc() {
let mut tape = TapeBuffer::new();
let mut strings = StringBuffer::new();
let root = tape.push(0); let obj_open = tape.push(0); let off_a = strings.append_record_at(0, b"a"); tape.push(make_string(off_a)); let arr_open = tape.push(0); tape.push(make_int64_marker()); tape.push(int64_bits(1)); tape.push(make_double_marker()); tape.push(double_bits(2.5)); let arr_close = tape.push(make_close(TAG_END_ARRAY, arr_open as u32)); tape.set(
arr_open,
make_open(TAG_START_ARRAY, (arr_close + 1) as u32, 2),
);
let off_b = strings.append_record_at(6, b"b"); tape.push(make_string(off_b)); let off_xn = strings.append_record_at(12, b"x\n"); tape.push(make_string(off_xn)); strings.pad_to(20); let obj_close = tape.push(make_close(TAG_END_OBJECT, obj_open as u32)); tape.set(
obj_open,
make_open(TAG_START_OBJECT, (obj_close + 1) as u32, 2),
);
let final_root = tape.push(make_final_root()); tape.set(root, make_root(final_root as u64));
let expected: [u64; 13] = [
0x7200_0000_0000_000C, 0x7B00_0002_0000_000C, 0x2200_0000_0000_0000, 0x5B00_0002_0000_0009, 0x6C00_0000_0000_0000, 0x0000_0000_0000_0001, 0x6400_0000_0000_0000, 0x4004_0000_0000_0000, 0x5D00_0000_0000_0003, 0x2200_0000_0000_0006, 0x2200_0000_0000_000C, 0x7D00_0000_0000_0001, 0x7200_0000_0000_0000, ];
assert_eq!(tape.as_words(), &expected);
let expected_strings: [u8; 20] = [
0x01, 0x00, 0x00, 0x00, 0x61, 0x00, 0x01, 0x00, 0x00, 0x00, 0x62, 0x00, 0x02, 0x00, 0x00, 0x00, 0x78, 0x0A, 0x00, 0x00, ];
assert_eq!(strings.as_bytes(), &expected_strings);
assert_eq!(root_final_index(tape[0]), 12);
assert_eq!(container_end_index(tape[1]), 12);
assert_eq!(container_count(tape[1]), 2);
assert_eq!(strings.record_str(string_offset(tape[2])), "a");
assert_eq!(container_end_index(tape[3]), 9);
assert_eq!(container_count(tape[3]), 2);
assert_eq!(int64_from_bits(tape[5]), 1);
assert_eq!(double_from_bits(tape[7]), 2.5);
assert_eq!(container_open_index(tape[8]), 3);
assert_eq!(strings.record_str(string_offset(tape[10])), "x\n");
assert_eq!(container_open_index(tape[11]), 1);
}
fn parse_msl_constants(header: &str) -> Vec<(String, u64)> {
let mut out = Vec::new();
for raw in header.lines() {
let line = raw.split("//").next().unwrap_or("").trim();
let Some(rest) = line.strip_prefix("constant constexpr ") else {
continue;
};
let decl = rest
.split(';')
.next()
.unwrap_or_else(|| panic!("missing ';' in tape_types.h line: {raw}"));
let (lhs, rhs) = decl
.split_once('=')
.unwrap_or_else(|| panic!("missing '=' in tape_types.h line: {raw}"));
let name = lhs
.split_whitespace()
.last()
.unwrap_or_else(|| panic!("missing name in tape_types.h line: {raw}"))
.to_owned();
let text = rhs.trim().trim_end_matches(['u', 'U', 'l', 'L']).to_owned();
let value =
if let Some(hex) = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
u64::from_str_radix(hex, 16)
} else {
text.parse::<u64>()
}
.unwrap_or_else(|e| panic!("cannot parse value of {name} ({text:?}): {e}"));
out.push((name, value));
}
out
}
#[test]
fn msl_header_layout_lock() {
let header = include_str!("../shaders/tape_types.h");
let parsed = parse_msl_constants(header);
assert!(
!parsed.is_empty(),
"parsed no constants from shaders/tape_types.h — \
header format changed without updating the test parser?"
);
let expected: &[(&str, u64)] = &[
("MJ_TAPE_TAG_SHIFT", TAG_SHIFT as u64),
("MJ_TAPE_PAYLOAD_MASK", PAYLOAD_MASK),
("MJ_TAG_ROOT", TAG_ROOT as u64),
("MJ_TAG_START_OBJECT", TAG_START_OBJECT as u64),
("MJ_TAG_END_OBJECT", TAG_END_OBJECT as u64),
("MJ_TAG_START_ARRAY", TAG_START_ARRAY as u64),
("MJ_TAG_END_ARRAY", TAG_END_ARRAY as u64),
("MJ_TAG_STRING", TAG_STRING as u64),
("MJ_TAG_INT64", TAG_INT64 as u64),
("MJ_TAG_UINT64", TAG_UINT64 as u64),
("MJ_TAG_DOUBLE", TAG_DOUBLE as u64),
("MJ_TAG_TRUE", TAG_TRUE as u64),
("MJ_TAG_FALSE", TAG_FALSE as u64),
("MJ_TAG_NULL", TAG_NULL as u64),
("MJ_CONTAINER_INDEX_MASK", CONTAINER_INDEX_MASK),
("MJ_CONTAINER_COUNT_SHIFT", CONTAINER_COUNT_SHIFT as u64),
("MJ_CONTAINER_COUNT_MAX", CONTAINER_COUNT_MAX as u64),
("MJ_STRING_OFFSET_MASK", STRING_OFFSET_MASK),
(
"MJ_STRING_RECORD_HEADER_BYTES",
STRING_RECORD_HEADER_BYTES as u64,
),
(
"MJ_STRING_RECORD_TRAILER_BYTES",
STRING_RECORD_TRAILER_BYTES as u64,
),
];
for (name, want) in expected {
let got = parsed
.iter()
.find(|(n, _)| n == name)
.unwrap_or_else(|| panic!("shaders/tape_types.h is missing constant {name}"));
assert_eq!(
got.1, *want,
"shaders/tape_types.h {name} = {:#x}, but src/tape.rs says {want:#x}",
got.1
);
}
for (name, value) in &parsed {
assert!(
expected.iter().any(|(n, _)| n == name),
"shaders/tape_types.h defines {name} = {value:#x}, \
which src/tape.rs's layout-lock test does not know about — \
add it to both sides"
);
}
for (i, (name, _)) in parsed.iter().enumerate() {
assert!(
!parsed[..i].iter().any(|(n, _)| n == name),
"shaders/tape_types.h defines {name} twice"
);
}
}
}