serializer/types.rs
1//! Core data types for DX format
2//!
3//! This module defines the core value types for the DX **machine format** (binary).
4//! The machine format is optimized for zero-copy deserialization and runtime performance.
5//!
6//! # Format Comparison
7//!
8//! DX provides two serialization formats:
9//!
10//! | Format | Type | Use Case | Performance |
11//! |--------|------|----------|-------------|
12//! | **Machine** | [`DxValue`] | Binary, zero-copy, runtime | Fastest parsing |
13//! | **LLM** | [`DxLlmValue`](crate::llm::DxLlmValue) | Text, token-efficient, LLM context | 73%+ token savings |
14//!
15//! # When to Use This Module
16//!
17//! Use [`DxValue`] and the machine format when:
18//! - You need maximum parsing and serialization performance
19//! - You're working with binary data or network protocols
20//! - You want zero-copy deserialization for large datasets
21//! - You're building runtime data structures
22//!
23//! Use [`DxLlmValue`](crate::llm::DxLlmValue) when:
24//! - You're preparing data for LLM context windows
25//! - You need human-readable output
26//! - Token efficiency is more important than raw speed
27//!
28//! # Thread Safety
29//!
30//! All types in this module implement `Send + Sync` and can be safely shared
31//! between threads. See the compile-time assertions at the bottom of this module.
32
33use rustc_hash::FxHashMap;
34use std::fmt;
35
36/// The core value type for the DX **machine format** (binary, zero-copy).
37///
38/// `DxValue` represents all possible values in the DX binary format, optimized
39/// for maximum parsing performance and zero-copy deserialization.
40///
41/// # Relationship to DxLlmValue
42///
43/// DX provides two value types for different use cases:
44///
45/// - **`DxValue`** (this type): For the binary machine format. Use when you need
46/// maximum performance, zero-copy parsing, or runtime data structures.
47///
48/// - **[`DxLlmValue`](crate::llm::DxLlmValue)**: For the text LLM format. Use when
49/// preparing data for LLM context windows or when token efficiency matters.
50///
51/// The two types have different variant sets because they serve different purposes:
52/// - `DxValue` has separate `Int` and `Float` variants for type precision
53/// - `DxLlmValue` has a single `Num` variant since LLMs don't distinguish
54/// - `DxValue` has `Object` and `Table` for structured data
55/// - `DxLlmValue` has `Ref` for reference pointers in the Dx Serializer format
56///
57/// # When to Use DxValue
58///
59/// Choose `DxValue` when:
60/// - **Performance is critical**: Binary format parses faster than text
61/// - **Zero-copy is needed**: Large datasets can be memory-mapped
62/// - **Type precision matters**: Separate `Int`/`Float` types preserve precision
63/// - **Building runtime structures**: Objects and Tables provide efficient access
64///
65/// # Examples
66///
67/// ## Creating Values
68///
69/// ```rust
70/// use serializer::DxValue;
71///
72/// // Primitive values
73/// let null = DxValue::Null;
74/// let boolean = DxValue::Bool(true);
75/// let integer = DxValue::Int(42);
76/// let float = DxValue::Float(3.14);
77/// let string = DxValue::String("hello".to_string());
78/// ```
79///
80/// ## Working with Arrays
81///
82/// ```rust
83/// use serializer::{DxValue, DxArray};
84///
85/// let mut arr = DxArray::new();
86/// arr.values.push(DxValue::Int(1));
87/// arr.values.push(DxValue::Int(2));
88/// arr.values.push(DxValue::Int(3));
89///
90/// let array_value = DxValue::Array(arr);
91/// ```
92///
93/// ## Working with Objects
94///
95/// ```rust
96/// use serializer::{DxValue, DxObject};
97///
98/// let mut obj = DxObject::new();
99/// obj.insert("name".to_string(), DxValue::String("Alice".to_string()));
100/// obj.insert("age".to_string(), DxValue::Int(30));
101///
102/// let object_value = DxValue::Object(obj);
103/// ```
104///
105/// ## Type Inspection
106///
107/// ```rust
108/// use serializer::DxValue;
109///
110/// let value = DxValue::Int(42);
111/// assert_eq!(value.type_name(), "int");
112/// assert_eq!(value.as_int(), Some(42));
113/// ```
114///
115/// # Thread Safety
116///
117/// `DxValue` implements `Send + Sync` and can be safely shared between threads.
118/// This is verified at compile time via static assertions.
119///
120/// # See Also
121///
122/// - [`DxLlmValue`](crate::llm::DxLlmValue) - Value type for the LLM text format
123/// - [`DxArray`] - Array container type
124/// - [`DxObject`] - Object/map container type
125/// - [`DxTable`] - Schema-defined table type
126#[derive(Debug, Clone, PartialEq)]
127pub enum DxValue {
128 /// Null value, represented as `~` in DX format.
129 Null,
130 /// Boolean value, represented as `+` (true) or `-` (false) in DX format.
131 Bool(bool),
132 /// 64-bit signed integer.
133 ///
134 /// Note: [`DxLlmValue`](crate::llm::DxLlmValue) uses a single `Num` variant
135 /// for all numbers since LLMs don't distinguish integer vs float.
136 Int(i64),
137 /// 64-bit floating-point number.
138 ///
139 /// Note: [`DxLlmValue`](crate::llm::DxLlmValue) uses a single `Num` variant
140 /// for all numbers since LLMs don't distinguish integer vs float.
141 Float(f64),
142 /// String value. In machine format, strings don't require quotes.
143 String(String),
144 /// Array/List of values. See [`DxArray`] for details.
145 Array(DxArray),
146 /// Object/Map with key-value pairs. See [`DxObject`] for details.
147 Object(DxObject),
148 /// Table with schema-defined columns. See [`DxTable`] for details.
149 ///
150 /// Tables are a DX-specific feature for efficient tabular data representation.
151 Table(DxTable),
152 /// Reference to an anchor by index (`@N` in DX format).
153 ///
154 /// Anchors allow deduplication of repeated values in the document.
155 Ref(usize),
156}
157
158/// A DX array (inline or vertical)
159#[derive(Debug, Clone, PartialEq)]
160pub struct DxArray {
161 /// Values stored in insertion order.
162 pub values: Vec<DxValue>,
163 /// Whether this was a stream (>)
164 pub is_stream: bool,
165}
166
167impl DxArray {
168 /// Create an empty array value.
169 pub fn new() -> Self {
170 Self {
171 values: Vec::new(),
172 is_stream: false,
173 }
174 }
175
176 /// Create an empty array value with preallocated capacity.
177 pub fn with_capacity(cap: usize) -> Self {
178 Self {
179 values: Vec::with_capacity(cap),
180 is_stream: false,
181 }
182 }
183
184 /// Create a stream-style array from existing values.
185 pub fn stream(values: Vec<DxValue>) -> Self {
186 Self {
187 values,
188 is_stream: true,
189 }
190 }
191}
192
193impl Default for DxArray {
194 fn default() -> Self {
195 Self::new()
196 }
197}
198
199/// A DX object (key-value pairs)
200#[derive(Debug, Clone, PartialEq)]
201pub struct DxObject {
202 /// Ordered key-value pairs
203 pub fields: Vec<(String, DxValue)>,
204 /// Fast lookup map (key index)
205 lookup: FxHashMap<String, usize>,
206}
207
208impl DxObject {
209 /// Create an empty ordered object.
210 pub fn new() -> Self {
211 Self {
212 fields: Vec::new(),
213 lookup: FxHashMap::default(),
214 }
215 }
216
217 /// Create an empty ordered object with preallocated field capacity.
218 pub fn with_capacity(cap: usize) -> Self {
219 Self {
220 fields: Vec::with_capacity(cap),
221 lookup: FxHashMap::with_capacity_and_hasher(cap, Default::default()),
222 }
223 }
224
225 /// Insert or replace a field while preserving insertion order.
226 pub fn insert(&mut self, key: String, value: DxValue) {
227 if let Some(&idx) = self.lookup.get(&key) {
228 self.fields[idx].1 = value;
229 } else {
230 let idx = self.fields.len();
231 self.fields.push((key.clone(), value));
232 self.lookup.insert(key, idx);
233 }
234 }
235
236 /// Get a field value by key.
237 pub fn get(&self, key: &str) -> Option<&DxValue> {
238 self.lookup.get(key).map(|&idx| &self.fields[idx].1)
239 }
240
241 /// Iterate fields in insertion order.
242 pub fn iter(&self) -> impl Iterator<Item = &(String, DxValue)> {
243 self.fields.iter()
244 }
245}
246
247impl Default for DxObject {
248 fn default() -> Self {
249 Self::new()
250 }
251}
252
253impl fmt::Display for DxObject {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 write!(f, "{{")?;
256 for (i, (k, v)) in self.fields.iter().enumerate() {
257 if i > 0 {
258 write!(f, ", ")?;
259 }
260 write!(f, "{}: {:?}", k, v)?;
261 }
262 write!(f, "}}")
263 }
264}
265
266/// A table with schema-defined columns
267#[derive(Debug, Clone, PartialEq)]
268pub struct DxTable {
269 /// Schema that defines column names and type hints.
270 pub schema: crate::schema::Schema,
271 /// Table rows, each matching the schema column count.
272 pub rows: Vec<Vec<DxValue>>,
273}
274
275impl DxTable {
276 /// Create an empty table for the provided schema.
277 pub fn new(schema: crate::schema::Schema) -> Self {
278 Self {
279 schema,
280 rows: Vec::new(),
281 }
282 }
283
284 /// Add a row after validating its width against the schema.
285 pub fn add_row(&mut self, row: Vec<DxValue>) -> Result<(), String> {
286 if row.len() != self.schema.columns.len() {
287 return Err(format!(
288 "Row length {} doesn't match schema length {}",
289 row.len(),
290 self.schema.columns.len()
291 ));
292 }
293 self.rows.push(row);
294 Ok(())
295 }
296
297 /// Return the number of rows currently stored in the table.
298 pub fn row_count(&self) -> usize {
299 self.rows.len()
300 }
301
302 /// Return the number of columns declared by the table schema.
303 pub fn column_count(&self) -> usize {
304 self.schema.columns.len()
305 }
306}
307
308impl DxValue {
309 /// Check if this value is "empty" for ditto logic
310 pub fn is_empty(&self) -> bool {
311 matches!(self, DxValue::Null)
312 }
313
314 /// Get type name for error messages
315 pub fn type_name(&self) -> &'static str {
316 match self {
317 DxValue::Null => "null",
318 DxValue::Bool(_) => "bool",
319 DxValue::Int(_) => "int",
320 DxValue::Float(_) => "float",
321 DxValue::String(_) => "string",
322 DxValue::Array(_) => "array",
323 DxValue::Object(_) => "object",
324 DxValue::Table(_) => "table",
325 DxValue::Ref(_) => "ref",
326 }
327 }
328
329 /// Convert to boolean if possible
330 pub fn as_bool(&self) -> Option<bool> {
331 match self {
332 DxValue::Bool(b) => Some(*b),
333 _ => None,
334 }
335 }
336
337 /// Convert to integer if possible
338 pub fn as_int(&self) -> Option<i64> {
339 match self {
340 DxValue::Int(i) => Some(*i),
341 DxValue::Float(f) => Some(*f as i64),
342 _ => None,
343 }
344 }
345
346 /// Convert to float if possible
347 pub fn as_float(&self) -> Option<f64> {
348 match self {
349 DxValue::Float(f) => Some(*f),
350 DxValue::Int(i) => Some(*i as f64),
351 _ => None,
352 }
353 }
354
355 /// Convert to string if possible
356 pub fn as_str(&self) -> Option<&str> {
357 match self {
358 DxValue::String(s) => Some(s),
359 _ => None,
360 }
361 }
362}
363
364// =============================================================================
365// Thread Safety Compile-Time Assertions
366// =============================================================================
367
368// These static assertions verify at compile time that our types are thread-safe.
369// If any of these types stop implementing Send or Sync, compilation will fail.
370
371/// Compile-time assertion that a type implements Send
372const fn _assert_send<T: Send>() {}
373
374/// Compile-time assertion that a type implements Sync
375const fn _assert_sync<T: Sync>() {}
376
377// Verify DxValue is Send + Sync
378const _: () = _assert_send::<DxValue>();
379const _: () = _assert_sync::<DxValue>();
380
381// Verify DxArray is Send + Sync
382const _: () = _assert_send::<DxArray>();
383const _: () = _assert_sync::<DxArray>();
384
385// Verify DxObject is Send + Sync
386const _: () = _assert_send::<DxObject>();
387const _: () = _assert_sync::<DxObject>();
388
389// Verify DxTable is Send + Sync
390const _: () = _assert_send::<DxTable>();
391const _: () = _assert_sync::<DxTable>();