1use core::convert::TryFrom;
13
14pub const SNAPSHOT_MAGIC: [u8; 8] = *b"OXMF2AST";
16
17pub const SNAPSHOT_MAJOR_VERSION: u16 = 0;
19
20pub const SNAPSHOT_MINOR_VERSION: u16 = 1;
23
24pub const SNAPSHOT_FEATURE_FLAGS: u32 = 0;
26
27pub const HEADER_SIZE: u32 = 32;
29
30pub const SECTION_RECORD_SIZE: u32 = 20;
32
33pub const ROOT_RECORD_SIZE: u32 = 16;
35
36pub const STRING_OFFSET_RECORD_SIZE: u32 = 8;
38
39pub const SOURCE_RECORD_SIZE: u32 = 32;
41
42pub const NODE_RECORD_SIZE: u32 = 24;
44
45pub const EDGE_RECORD_SIZE: u32 = 8;
47
48pub const TOKEN_RECORD_SIZE: u32 = 36;
50
51pub const TRIVIA_RECORD_SIZE: u32 = 16;
53
54pub const DIAGNOSTIC_RECORD_SIZE: u32 = 28;
56
57pub const DIAGNOSTIC_LABEL_RECORD_SIZE: u32 = 16;
59
60pub const EXTENDED_DATA_HEADER_SIZE: u32 = 8;
62
63pub const SECTION_ALIGNMENT: u32 = 8;
66
67pub const NONE_REF: u32 = u32::MAX;
71
72pub const SECTION_FLAG_REQUIRED: u16 = 1;
75
76pub const EDGE_KIND_NODE: u16 = 0;
78pub const EDGE_KIND_TOKEN: u16 = 1;
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87#[repr(u16)]
88pub enum SectionKind {
89 Roots = 1,
90 Sources = 2,
91 Nodes = 3,
92 Edges = 4,
93 Tokens = 5,
94 Trivia = 6,
95 Diagnostics = 7,
96 DiagnosticLabels = 8,
97 StringOffsets = 9,
98 StringData = 10,
99 SourceTextData = 11,
100 ExtendedData = 12,
101}
102
103impl SectionKind {
104 #[inline]
106 pub const fn as_u16(self) -> u16 {
107 self as u16
108 }
109
110 pub const fn from_u16(value: u16) -> Option<Self> {
113 Some(match value {
114 1 => Self::Roots,
115 2 => Self::Sources,
116 3 => Self::Nodes,
117 4 => Self::Edges,
118 5 => Self::Tokens,
119 6 => Self::Trivia,
120 7 => Self::Diagnostics,
121 8 => Self::DiagnosticLabels,
122 9 => Self::StringOffsets,
123 10 => Self::StringData,
124 11 => Self::SourceTextData,
125 12 => Self::ExtendedData,
126 _ => return None,
127 })
128 }
129
130 pub const fn is_core(self) -> bool {
133 matches!(
134 self,
135 Self::Roots
136 | Self::Sources
137 | Self::Nodes
138 | Self::Edges
139 | Self::Tokens
140 | Self::StringOffsets
141 | Self::StringData
142 )
143 }
144
145 pub const fn record_size(self) -> u16 {
148 match self {
149 Self::Roots => ROOT_RECORD_SIZE as u16,
150 Self::Sources => SOURCE_RECORD_SIZE as u16,
151 Self::Nodes => NODE_RECORD_SIZE as u16,
152 Self::Edges => EDGE_RECORD_SIZE as u16,
153 Self::Tokens => TOKEN_RECORD_SIZE as u16,
154 Self::Trivia => TRIVIA_RECORD_SIZE as u16,
155 Self::Diagnostics => DIAGNOSTIC_RECORD_SIZE as u16,
156 Self::DiagnosticLabels => DIAGNOSTIC_LABEL_RECORD_SIZE as u16,
157 Self::StringOffsets => STRING_OFFSET_RECORD_SIZE as u16,
158 Self::StringData | Self::SourceTextData | Self::ExtendedData => 0,
159 }
160 }
161
162 pub const ALL_IN_ORDER: [Self; 12] = [
167 Self::Roots,
168 Self::Sources,
169 Self::Nodes,
170 Self::Edges,
171 Self::Tokens,
172 Self::Trivia,
173 Self::Diagnostics,
174 Self::DiagnosticLabels,
175 Self::StringOffsets,
176 Self::StringData,
177 Self::SourceTextData,
178 Self::ExtendedData,
179 ];
180}
181
182#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
184#[repr(transparent)]
185pub struct RootId(pub u32);
186
187impl RootId {
188 #[inline]
190 pub const fn new(raw: u32) -> Self {
191 Self(raw)
192 }
193
194 #[inline]
196 pub const fn raw(self) -> u32 {
197 self.0
198 }
199
200 #[inline]
202 pub const fn index(self) -> usize {
203 self.0 as usize
204 }
205}
206
207impl From<u32> for RootId {
208 #[inline]
209 fn from(value: u32) -> Self {
210 Self(value)
211 }
212}
213
214impl From<RootId> for u32 {
215 #[inline]
216 fn from(value: RootId) -> Self {
217 value.0
218 }
219}
220
221#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
225#[repr(transparent)]
226pub struct StringId(pub u32);
227
228impl StringId {
229 pub const NONE: Self = Self(NONE_REF);
231
232 #[inline]
233 pub const fn new(raw: u32) -> Self {
234 Self(raw)
235 }
236
237 #[inline]
238 pub const fn raw(self) -> u32 {
239 self.0
240 }
241
242 #[inline]
243 pub const fn is_none(self) -> bool {
244 self.0 == NONE_REF
245 }
246}
247
248#[inline]
257pub(crate) fn write_u8(buf: &mut Vec<u8>, value: u8) {
258 buf.push(value);
259}
260
261#[inline]
262pub(crate) fn write_u16_le(buf: &mut Vec<u8>, value: u16) {
263 buf.extend_from_slice(&value.to_le_bytes());
264}
265
266#[inline]
267pub(crate) fn write_u32_le(buf: &mut Vec<u8>, value: u32) {
268 buf.extend_from_slice(&value.to_le_bytes());
269}
270
271#[inline]
272pub(crate) fn read_u16_le(bytes: &[u8], offset: usize) -> u16 {
273 let arr: [u8; 2] = bytes[offset..offset + 2].try_into().unwrap();
274 u16::from_le_bytes(arr)
275}
276
277#[inline]
278pub(crate) fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
279 let arr: [u8; 4] = bytes[offset..offset + 4].try_into().unwrap();
280 u32::from_le_bytes(arr)
281}
282
283#[inline]
287pub(crate) fn align_up(offset: u32) -> Option<u32> {
288 let mask = SECTION_ALIGNMENT - 1;
289 let extra = offset & mask;
290 if extra == 0 {
291 Some(offset)
292 } else {
293 let padding = SECTION_ALIGNMENT - extra;
294 offset.checked_add(padding)
295 }
296}
297
298#[inline]
302pub(crate) fn checked_u32(value: usize) -> Option<u32> {
303 u32::try_from(value).ok()
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn record_sizes_match_design_table() {
312 assert_eq!(HEADER_SIZE, 32);
313 assert_eq!(SECTION_RECORD_SIZE, 20);
314 assert_eq!(ROOT_RECORD_SIZE, 16);
315 assert_eq!(STRING_OFFSET_RECORD_SIZE, 8);
316 assert_eq!(SOURCE_RECORD_SIZE, 32);
317 assert_eq!(NODE_RECORD_SIZE, 24);
318 assert_eq!(EDGE_RECORD_SIZE, 8);
319 assert_eq!(TOKEN_RECORD_SIZE, 36);
320 assert_eq!(TRIVIA_RECORD_SIZE, 16);
321 assert_eq!(DIAGNOSTIC_RECORD_SIZE, 28);
322 assert_eq!(DIAGNOSTIC_LABEL_RECORD_SIZE, 16);
323 assert_eq!(EXTENDED_DATA_HEADER_SIZE, 8);
324 }
325
326 #[test]
327 fn section_kind_numeric_values_are_stable() {
328 assert_eq!(SectionKind::Roots.as_u16(), 1);
329 assert_eq!(SectionKind::Sources.as_u16(), 2);
330 assert_eq!(SectionKind::Nodes.as_u16(), 3);
331 assert_eq!(SectionKind::Edges.as_u16(), 4);
332 assert_eq!(SectionKind::Tokens.as_u16(), 5);
333 assert_eq!(SectionKind::Trivia.as_u16(), 6);
334 assert_eq!(SectionKind::Diagnostics.as_u16(), 7);
335 assert_eq!(SectionKind::DiagnosticLabels.as_u16(), 8);
336 assert_eq!(SectionKind::StringOffsets.as_u16(), 9);
337 assert_eq!(SectionKind::StringData.as_u16(), 10);
338 assert_eq!(SectionKind::SourceTextData.as_u16(), 11);
339 assert_eq!(SectionKind::ExtendedData.as_u16(), 12);
340 }
341
342 #[test]
343 fn section_kind_from_u16_rejects_zero_and_unknown() {
344 assert!(SectionKind::from_u16(0).is_none());
345 assert!(SectionKind::from_u16(13).is_none());
346 assert_eq!(SectionKind::from_u16(1), Some(SectionKind::Roots));
347 }
348
349 #[test]
350 fn section_kind_emission_order_is_stable() {
351 let order = SectionKind::ALL_IN_ORDER.map(SectionKind::as_u16);
352 assert_eq!(order, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
353 }
354
355 #[test]
356 fn core_section_predicate_matches_design() {
357 for kind in SectionKind::ALL_IN_ORDER {
358 let expected = matches!(
359 kind,
360 SectionKind::Roots
361 | SectionKind::Sources
362 | SectionKind::Nodes
363 | SectionKind::Edges
364 | SectionKind::Tokens
365 | SectionKind::StringOffsets
366 | SectionKind::StringData
367 );
368 assert_eq!(kind.is_core(), expected, "{kind:?}");
369 }
370 }
371
372 #[test]
373 fn align_up_rounds_to_eight_byte_boundary() {
374 assert_eq!(align_up(0), Some(0));
375 assert_eq!(align_up(1), Some(8));
376 assert_eq!(align_up(7), Some(8));
377 assert_eq!(align_up(8), Some(8));
378 assert_eq!(align_up(9), Some(16));
379 assert_eq!(align_up(u32::MAX - 6), None);
380 }
381
382 #[test]
383 fn checked_u32_surfaces_overflow_as_none() {
384 assert_eq!(checked_u32(0), Some(0));
385 assert_eq!(checked_u32(u32::MAX as usize), Some(u32::MAX));
386 if usize::BITS > 32 {
387 assert_eq!(checked_u32(u32::MAX as usize + 1), None);
388 }
389 }
390
391 #[test]
392 fn little_endian_helpers_round_trip() {
393 let mut buf = Vec::new();
394 write_u8(&mut buf, 0xAB);
395 write_u16_le(&mut buf, 0x1234);
396 write_u32_le(&mut buf, 0xDEAD_BEEF);
397 assert_eq!(buf.len(), 7);
398 assert_eq!(buf[0], 0xAB);
399 assert_eq!(read_u16_le(&buf, 1), 0x1234);
400 assert_eq!(read_u32_le(&buf, 3), 0xDEAD_BEEF);
401 }
402
403 #[test]
404 fn magic_is_oxmf2ast() {
405 assert_eq!(&SNAPSHOT_MAGIC, b"OXMF2AST");
406 }
407
408 #[test]
409 fn header_values_lock_initial_v01() {
410 assert_eq!(SNAPSHOT_MAJOR_VERSION, 0);
411 assert_eq!(SNAPSHOT_MINOR_VERSION, 1);
412 assert_eq!(SNAPSHOT_FEATURE_FLAGS, 0);
413 assert_eq!(SECTION_ALIGNMENT, 8);
414 }
415}