//! Schema layer: the `AlkTypeKind` enum, `Endian`/`VariableEncoding` //! annotations, and shared constants. //! //! Under the BAST pivot the v0.1.0 custom-keyword accessors //! (`get_alktype_kind*`, `normalize_refs`, `inline_union_variant_refs`, //! `parse_*`, `resolve_ref*`, `DiscriminatorKind`) were removed — the //! BAST parser ([`crate::bast`]) is the typed surface every engine //! module walks. This module retains only the foundational types that //! the layout/data-access/materialize/tunion/builder layers depend on: //! the kind enum (with its BAST string mapping), the two annotation //! enums, and the shared byte-layout constants. use crate::error::AlkTypeError; use std::fmt; pub(crate) const U32_SIZE: usize = 4; pub(crate) const DISCRIMINATOR_PATH: &str = "__discriminator"; /// Maximum declared array element count. Schemas are untrusted input /// (AGENTS.md §3): every layout walker expands `count` into per-element /// work (offset-map/layout entries, per-element reads), so an unbounded /// count is a memory/CPU denial-of-service. The cap applies at parse /// time, before any walker sees the array; 2^16 elements bounds the /// per-array compile-time entry cost at ~10 MB. pub(crate) const MAX_ARRAY_ELEMENTS: usize = 1 << 16; /// Maximum computed byte size of a single array (`count × element /// stride`). Bounds the arithmetic product independently of /// [`MAX_ARRAY_ELEMENTS`] so multi-megabyte strides cannot combine with /// a legal count into an unbounded layout request. pub(crate) const MAX_ARRAY_BYTES: usize = 1 << 26; /// Maximum declared `align` annotation (struct- or field-level). /// Schemas are untrusted input (AGENTS.md §3): `align_up` rounds the /// running offset by the full annotation, so an unbounded align lets a /// one-field schema declare an exabyte-scale layout (`total_size = 2^63` /// from `align: 2^62` — review #006 N2 probe). Honest layouts never /// need more than page granularity (4096). pub(crate) const MAX_ALIGN: usize = 4096; /// Maximum declared `maxLength` annotation. Schemas are untrusted input /// (AGENTS.md §3): a string/bytes reservation contributes its full /// `maxLength` to `total_size`, so an unbounded value lets a one-field /// schema declare a terabyte-scale layout (`total_size = 2^40` from /// `maxLength: 1e12` — review #007 F2 probe). The cap matches /// [`MAX_ARRAY_BYTES`]'s rationale (a single fixed reservation no /// larger than the largest legal array). pub(crate) const MAX_LENGTH: usize = 1 << 26; /// The 18 BAST kinds recognized by the engine. /// /// Each variant corresponds to a lowercase BAST kind string /// ([`AlkTypeKind::to_bast_str`], e.g. `"uint32"` ↔ `Uint32`). The enum /// provides compile-time exhaustiveness checking and integer /// discriminant dispatch (jump table) instead of string comparison. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AlkTypeKind { Int8, Int16, Int32, Int64, Uint8, Uint16, Uint32, Uint64, Float32, Float64, Boolean, Enum, String, Bytes, Struct, Union, Array, Record, } impl AlkTypeKind { /// Fixed byte size, or `None` for variable-size / composite kinds. pub fn type_size(self) -> Option { match self { AlkTypeKind::Float32 | AlkTypeKind::Int32 | AlkTypeKind::Uint32 | AlkTypeKind::Enum => { Some(4) } AlkTypeKind::Float64 | AlkTypeKind::Int64 | AlkTypeKind::Uint64 => Some(8), AlkTypeKind::Int8 | AlkTypeKind::Uint8 | AlkTypeKind::Boolean => Some(1), AlkTypeKind::Int16 | AlkTypeKind::Uint16 => Some(2), AlkTypeKind::String | AlkTypeKind::Bytes | AlkTypeKind::Struct | AlkTypeKind::Union | AlkTypeKind::Array | AlkTypeKind::Record => None, } } /// Natural alignment: 1 for u8/i8/bool, 2 for u16/i16, 4 for u32/i32/f32/enum, /// 8 for u64/i64/f64, 4 for variable-length (u32 length prefix), 1 for /// composites. pub fn natural_alignment(self) -> usize { match self { AlkTypeKind::Int8 | AlkTypeKind::Uint8 | AlkTypeKind::Boolean => 1, AlkTypeKind::Int16 | AlkTypeKind::Uint16 => 2, AlkTypeKind::Int32 | AlkTypeKind::Uint32 | AlkTypeKind::Float32 | AlkTypeKind::Enum => 4, AlkTypeKind::Float64 | AlkTypeKind::Int64 | AlkTypeKind::Uint64 => 8, AlkTypeKind::String | AlkTypeKind::Bytes | AlkTypeKind::Record => 4, AlkTypeKind::Struct | AlkTypeKind::Union | AlkTypeKind::Array => 1, } } /// Returns `true` for fixed-size primitive kinds. pub fn is_fixed_size(self) -> bool { matches!( self, AlkTypeKind::Float32 | AlkTypeKind::Float64 | AlkTypeKind::Int8 | AlkTypeKind::Int16 | AlkTypeKind::Int32 | AlkTypeKind::Int64 | AlkTypeKind::Uint8 | AlkTypeKind::Uint16 | AlkTypeKind::Uint32 | AlkTypeKind::Uint64 | AlkTypeKind::Boolean | AlkTypeKind::Enum ) } /// Returns `true` for kinds whose read/write functions need an `Endian` parameter. pub fn needs_endian(self) -> bool { matches!( self, AlkTypeKind::Int16 | AlkTypeKind::Int32 | AlkTypeKind::Int64 | AlkTypeKind::Uint16 | AlkTypeKind::Uint32 | AlkTypeKind::Uint64 | AlkTypeKind::Float32 | AlkTypeKind::Float64 | AlkTypeKind::Enum | AlkTypeKind::String | AlkTypeKind::Bytes ) } /// Returns `true` for composite kinds (Struct, Union, Array, Record). pub fn is_composite(self) -> bool { matches!( self, AlkTypeKind::Struct | AlkTypeKind::Union | AlkTypeKind::Array | AlkTypeKind::Record ) } /// Returns `true` for variable-length kinds (String, Bytes, Record). pub fn is_variable_length(self) -> bool { matches!( self, AlkTypeKind::String | AlkTypeKind::Bytes | AlkTypeKind::Record ) } /// The lowercase BAST kind string, e.g. `"uint32"` (D-BAST-002). /// /// This is the string used in BAST documents (`{ "kind": "uint32" }`) /// and the value the parser and validator dispatch on. pub fn to_bast_str(self) -> &'static str { match self { AlkTypeKind::Int8 => "int8", AlkTypeKind::Int16 => "int16", AlkTypeKind::Int32 => "int32", AlkTypeKind::Int64 => "int64", AlkTypeKind::Uint8 => "uint8", AlkTypeKind::Uint16 => "uint16", AlkTypeKind::Uint32 => "uint32", AlkTypeKind::Uint64 => "uint64", AlkTypeKind::Float32 => "float32", AlkTypeKind::Float64 => "float64", AlkTypeKind::Boolean => "bool", AlkTypeKind::String => "string", AlkTypeKind::Bytes => "bytes", AlkTypeKind::Enum => "enum", AlkTypeKind::Struct => "struct", AlkTypeKind::Union => "union", AlkTypeKind::Array => "array", AlkTypeKind::Record => "record", } } /// Parse a lowercase BAST kind string into an `AlkTypeKind` /// (D-BAST-002). Returns `AlkTypeError::Schema` for unknown strings. /// /// This is the inverse of [`to_bast_str`](Self::to_bast_str). pub fn from_bast_str(s: &str) -> Result { match s { "int8" => Ok(AlkTypeKind::Int8), "int16" => Ok(AlkTypeKind::Int16), "int32" => Ok(AlkTypeKind::Int32), "int64" => Ok(AlkTypeKind::Int64), "uint8" => Ok(AlkTypeKind::Uint8), "uint16" => Ok(AlkTypeKind::Uint16), "uint32" => Ok(AlkTypeKind::Uint32), "uint64" => Ok(AlkTypeKind::Uint64), "float32" => Ok(AlkTypeKind::Float32), "float64" => Ok(AlkTypeKind::Float64), "bool" => Ok(AlkTypeKind::Boolean), "string" => Ok(AlkTypeKind::String), "bytes" => Ok(AlkTypeKind::Bytes), "enum" => Ok(AlkTypeKind::Enum), "struct" => Ok(AlkTypeKind::Struct), "union" => Ok(AlkTypeKind::Union), "array" => Ok(AlkTypeKind::Array), "record" => Ok(AlkTypeKind::Record), other => Err(AlkTypeError::Schema(format!( "unknown BAST kind: {other}" ))), } } } impl fmt::Display for AlkTypeKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.to_bast_str()) } } /// Byte endianness for multi-byte integer and float fields. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Endian { Little, Big, } /// The encoding strategy for a variable-length type. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum VariableEncoding { /// `[length: u32][data]` — the default. Length prefix at a known offset, /// variable data follows immediately. LengthPrefixed, /// `{offset: u32, length: u32}` pointing into a separate data region. /// The metatensor blob tensor pattern. OffsetIndirect, } #[cfg(test)] mod tests { use super::*; #[test] fn type_size_fixed_kinds() { assert_eq!(AlkTypeKind::Float32.type_size(), Some(4)); assert_eq!(AlkTypeKind::Float64.type_size(), Some(8)); assert_eq!(AlkTypeKind::Int8.type_size(), Some(1)); assert_eq!(AlkTypeKind::Int16.type_size(), Some(2)); assert_eq!(AlkTypeKind::Int32.type_size(), Some(4)); assert_eq!(AlkTypeKind::Int64.type_size(), Some(8)); assert_eq!(AlkTypeKind::Uint8.type_size(), Some(1)); assert_eq!(AlkTypeKind::Uint16.type_size(), Some(2)); assert_eq!(AlkTypeKind::Uint32.type_size(), Some(4)); assert_eq!(AlkTypeKind::Uint64.type_size(), Some(8)); assert_eq!(AlkTypeKind::Boolean.type_size(), Some(1)); assert_eq!(AlkTypeKind::Enum.type_size(), Some(4)); } #[test] fn type_size_variable_and_composite_kinds() { for kind in [ AlkTypeKind::String, AlkTypeKind::Bytes, AlkTypeKind::Struct, AlkTypeKind::Union, AlkTypeKind::Array, AlkTypeKind::Record, ] { assert_eq!(kind.type_size(), None, "failed for {kind:?}"); } } #[test] fn natural_alignment_matches_spec() { assert_eq!(AlkTypeKind::Int8.natural_alignment(), 1); assert_eq!(AlkTypeKind::Uint8.natural_alignment(), 1); assert_eq!(AlkTypeKind::Boolean.natural_alignment(), 1); assert_eq!(AlkTypeKind::Int16.natural_alignment(), 2); assert_eq!(AlkTypeKind::Uint16.natural_alignment(), 2); assert_eq!(AlkTypeKind::Int32.natural_alignment(), 4); assert_eq!(AlkTypeKind::Uint32.natural_alignment(), 4); assert_eq!(AlkTypeKind::Float32.natural_alignment(), 4); assert_eq!(AlkTypeKind::Enum.natural_alignment(), 4); assert_eq!(AlkTypeKind::Float64.natural_alignment(), 8); assert_eq!(AlkTypeKind::Int64.natural_alignment(), 8); assert_eq!(AlkTypeKind::Uint64.natural_alignment(), 8); assert_eq!(AlkTypeKind::String.natural_alignment(), 4); assert_eq!(AlkTypeKind::Bytes.natural_alignment(), 4); assert_eq!(AlkTypeKind::Record.natural_alignment(), 4); assert_eq!(AlkTypeKind::Struct.natural_alignment(), 1); assert_eq!(AlkTypeKind::Union.natural_alignment(), 1); assert_eq!(AlkTypeKind::Array.natural_alignment(), 1); } #[test] fn is_fixed_size_classifies_correctly() { for kind in [ AlkTypeKind::Float32, AlkTypeKind::Float64, AlkTypeKind::Int8, AlkTypeKind::Int16, AlkTypeKind::Int32, AlkTypeKind::Int64, AlkTypeKind::Uint8, AlkTypeKind::Uint16, AlkTypeKind::Uint32, AlkTypeKind::Uint64, AlkTypeKind::Boolean, AlkTypeKind::Enum, ] { assert!(kind.is_fixed_size(), "expected fixed: {kind:?}"); } for kind in [ AlkTypeKind::String, AlkTypeKind::Bytes, AlkTypeKind::Struct, AlkTypeKind::Union, AlkTypeKind::Array, AlkTypeKind::Record, ] { assert!(!kind.is_fixed_size(), "expected variable: {kind:?}"); } } #[test] fn needs_endian_classifies_correctly() { for kind in [ AlkTypeKind::Int16, AlkTypeKind::Int32, AlkTypeKind::Int64, AlkTypeKind::Uint16, AlkTypeKind::Uint32, AlkTypeKind::Uint64, AlkTypeKind::Float32, AlkTypeKind::Float64, AlkTypeKind::Enum, AlkTypeKind::String, AlkTypeKind::Bytes, ] { assert!(kind.needs_endian(), "expected needs_endian: {kind:?}"); } for kind in [ AlkTypeKind::Int8, AlkTypeKind::Uint8, AlkTypeKind::Boolean, AlkTypeKind::Struct, AlkTypeKind::Union, AlkTypeKind::Array, AlkTypeKind::Record, ] { assert!(!kind.needs_endian(), "expected not needs_endian: {kind:?}"); } } #[test] fn is_composite_classifies_correctly() { for kind in [ AlkTypeKind::Struct, AlkTypeKind::Union, AlkTypeKind::Array, AlkTypeKind::Record, ] { assert!(kind.is_composite(), "expected composite: {kind:?}"); } for kind in [ AlkTypeKind::Int8, AlkTypeKind::Int16, AlkTypeKind::Int32, AlkTypeKind::Int64, AlkTypeKind::Uint8, AlkTypeKind::Uint16, AlkTypeKind::Uint32, AlkTypeKind::Uint64, AlkTypeKind::Float32, AlkTypeKind::Float64, AlkTypeKind::Boolean, AlkTypeKind::Enum, AlkTypeKind::String, AlkTypeKind::Bytes, ] { assert!(!kind.is_composite(), "expected not composite: {kind:?}"); } } #[test] fn to_bast_str_covers_all_18_variants() { let cases: &[(AlkTypeKind, &str)] = &[ (AlkTypeKind::Int8, "int8"), (AlkTypeKind::Int16, "int16"), (AlkTypeKind::Int32, "int32"), (AlkTypeKind::Int64, "int64"), (AlkTypeKind::Uint8, "uint8"), (AlkTypeKind::Uint16, "uint16"), (AlkTypeKind::Uint32, "uint32"), (AlkTypeKind::Uint64, "uint64"), (AlkTypeKind::Float32, "float32"), (AlkTypeKind::Float64, "float64"), (AlkTypeKind::Boolean, "bool"), (AlkTypeKind::String, "string"), (AlkTypeKind::Bytes, "bytes"), (AlkTypeKind::Enum, "enum"), (AlkTypeKind::Struct, "struct"), (AlkTypeKind::Union, "union"), (AlkTypeKind::Array, "array"), (AlkTypeKind::Record, "record"), ]; assert_eq!(cases.len(), 18); for (kind, expected) in cases { assert_eq!(kind.to_bast_str(), *expected); } } #[test] fn from_bast_str_round_trips_all_variants() { for kind in [ AlkTypeKind::Int8, AlkTypeKind::Int16, AlkTypeKind::Int32, AlkTypeKind::Int64, AlkTypeKind::Uint8, AlkTypeKind::Uint16, AlkTypeKind::Uint32, AlkTypeKind::Uint64, AlkTypeKind::Float32, AlkTypeKind::Float64, AlkTypeKind::Boolean, AlkTypeKind::String, AlkTypeKind::Bytes, AlkTypeKind::Enum, AlkTypeKind::Struct, AlkTypeKind::Union, AlkTypeKind::Array, AlkTypeKind::Record, ] { let s = kind.to_bast_str(); let parsed = AlkTypeKind::from_bast_str(s).expect("round-trip {s}"); assert_eq!(parsed, kind, "round-trip {s}"); } } #[test] fn from_bast_str_parses_lowercase_strings() { assert!(matches!( AlkTypeKind::from_bast_str("uint32"), Ok(AlkTypeKind::Uint32) )); assert!(matches!( AlkTypeKind::from_bast_str("bool"), Ok(AlkTypeKind::Boolean) )); assert!(matches!( AlkTypeKind::from_bast_str("struct"), Ok(AlkTypeKind::Struct) )); } #[test] fn from_bast_str_rejects_unknown_strings() { assert!(AlkTypeKind::from_bast_str("uint128").is_err()); assert!(AlkTypeKind::from_bast_str("Uint32").is_err()); assert!(AlkTypeKind::from_bast_str("AlkType:Uint32").is_err()); assert!(AlkTypeKind::from_bast_str("timestamp").is_err()); assert!(AlkTypeKind::from_bast_str("").is_err()); } }