Skip to main content

vyre_spec/
extension.rs

1//! Extension contracts for open IR.
2//!
3//! Downstream crates ship new `Expr`, `Node`, `DataType`, `BinOp`, `UnOp`,
4//! `AtomicOp`, `TernaryOp`, and `RuleCondition` variants by implementing the
5//! traits in this module and registering an id with the foundation extension
6//! registry.
7//!
8//! `vyre-spec` is intentionally data-only and carries no dependency on
9//! `inventory`. The trait signatures below describe the stable contract;
10//! actual registration and resolution lives in `vyre_foundation::extension`.
11//!
12//! Every extension id occupies the range `0x8000_0000..=0xFFFF_FFFF`  -  the
13//! high bit of the wire tag distinguishes extension ids from the frozen
14//! core tag space `0x00..=0x7F`. The `ExtensionDataTypeId::from_name`
15//! constructor folds a stable crate-name hash into the reserved range so
16//! two independently-authored extensions collide only on deliberate
17//! name-clashes.
18
19use core::fmt::Debug;
20
21macro_rules! impl_extension_id {
22    ($id:ident) => {
23        impl $id {
24            /// Reserved range: every extension id has its high bit set.
25            ///
26            /// Core IR discriminants occupy `0x00..=0x7F`; extensions occupy
27            /// `0x80..=0xFFFF_FFFF`. Wire decoders test the high byte to route
28            /// decoding between the two.
29            pub const EXTENSION_RANGE_MASK: u32 = 0x8000_0000;
30
31            /// Construct an id from a stable extension name.
32            ///
33            /// The id is derived deterministically with FNV-1a and folded into
34            /// the extension range by setting the high bit. Callers that pass
35            /// the same `name` always get the same id.
36            #[must_use]
37            pub const fn from_name(name: &str) -> Self {
38                Self(fnv1a_with_high_bit(name))
39            }
40
41            /// Return the raw id.
42            #[must_use]
43            pub const fn as_u32(self) -> u32 {
44                self.0
45            }
46
47            /// Is this a reserved extension id (high bit set)?
48            #[must_use]
49            pub const fn is_extension(self) -> bool {
50                (self.0 & Self::EXTENSION_RANGE_MASK) != 0
51            }
52        }
53    };
54}
55
56/// Stable u32 id for an extension variant.
57///
58/// Extension ids are generated deterministically from a stable name via
59/// [`ExtensionDataTypeId::from_name`]. A crate that never changes its
60/// extension name keeps the same id across versions, which is the
61/// wire-format contract: a `Program` encoded by v1.0 of an extension
62/// decodes identically in v1.1 so long as the name is stable.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
64pub struct ExtensionDataTypeId(pub u32);
65
66impl_extension_id!(ExtensionDataTypeId);
67
68/// The contract for an extension-declared `DataType`.
69///
70/// An implementer describes the runtime shape of a non-core data type:
71/// how many bytes it occupies, whether it participates in the float
72/// conformance family, and how it should be displayed.
73///
74/// The foundation extension registry walks a link-time inventory of
75/// `ExtensionDataTypeRegistration` entries. The resolver caches
76/// `&'static dyn ExtensionDataType` so downstream
77/// consumers never re-consult the registry on the hot path.
78pub trait ExtensionDataType: Send + Sync + Debug + 'static {
79    /// Stable id for this data type.
80    fn id(&self) -> ExtensionDataTypeId;
81    /// Human-readable name for display / debug.
82    fn display_name(&self) -> &'static str;
83    /// Minimum byte count to represent one value of this type.
84    fn min_bytes(&self) -> usize;
85    /// Maximum byte count for one value of this type; `None` when unbounded.
86    fn max_bytes(&self) -> Option<usize>;
87    /// Fixed element size in bytes, or `None` for variable-size types.
88    fn size_bytes(&self) -> Option<usize>;
89    /// Whether this type belongs to the IEEE-754 float conformance family.
90    fn is_float_family(&self) -> bool {
91        false
92    }
93    /// Whether values can be safely memcpy'd between host and device.
94    fn is_host_shareable(&self) -> bool {
95        true
96    }
97}
98
99/// Runtime contract for an extension-declared binary operator.
100///
101/// The foundation extension registry caches `&'static dyn ExtensionBinOp`
102/// pointers keyed by [`ExtensionBinOpId`]; downstream evaluators and lowerings
103/// call through this trait without re-consulting the registry on the hot path.
104pub trait ExtensionBinOp: Send + Sync + Debug + 'static {
105    /// Stable id of this binary operator.
106    fn id(&self) -> ExtensionBinOpId;
107    /// Human-readable name for display / debug.
108    fn display_name(&self) -> &'static str;
109    /// Evaluate on the reference (CPU) backend.
110    ///
111    /// Returning `None` means "this backend does not support the op"; the
112    /// caller surfaces a typed error. Extensions implementing backends
113    /// other than reference supply their own lowering via the backend
114    /// registry.
115    fn eval_u32(&self, _a: u32, _b: u32) -> Option<u32> {
116        None
117    }
118}
119
120/// Runtime contract for an extension-declared unary operator.
121pub trait ExtensionUnOp: Send + Sync + Debug + 'static {
122    /// Stable id of this unary operator.
123    fn id(&self) -> ExtensionUnOpId;
124    /// Human-readable name for display / debug.
125    fn display_name(&self) -> &'static str;
126    /// Evaluate on the reference (CPU) backend. `None` = unsupported.
127    fn eval_u32(&self, _a: u32) -> Option<u32> {
128        None
129    }
130}
131
132/// Runtime contract for an extension-declared atomic operator.
133pub trait ExtensionAtomicOp: Send + Sync + Debug + 'static {
134    /// Stable id of this atomic operator.
135    fn id(&self) -> ExtensionAtomicOpId;
136    /// Human-readable name for display / debug.
137    fn display_name(&self) -> &'static str;
138}
139
140/// Runtime contract for an extension-declared ternary operator.
141pub trait ExtensionTernaryOp: Send + Sync + Debug + 'static {
142    /// Stable id of this ternary operator.
143    fn id(&self) -> ExtensionTernaryOpId;
144    /// Human-readable name for display / debug.
145    fn display_name(&self) -> &'static str;
146}
147
148/// Stable u32 id for an extension binary operator.
149///
150/// Identical discipline to [`ExtensionDataTypeId`]: stable across process
151/// runs, high bit set, generated by FNV-1a of the extension name.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
153pub struct ExtensionBinOpId(pub u32);
154
155impl_extension_id!(ExtensionBinOpId);
156
157/// Stable u32 id for an extension unary operator.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
159pub struct ExtensionUnOpId(pub u32);
160
161impl_extension_id!(ExtensionUnOpId);
162
163/// Stable u32 id for an extension atomic operator.
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
165pub struct ExtensionAtomicOpId(pub u32);
166
167impl_extension_id!(ExtensionAtomicOpId);
168
169/// Stable u32 id for an extension ternary operator.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
171pub struct ExtensionTernaryOpId(pub u32);
172
173impl_extension_id!(ExtensionTernaryOpId);
174
175/// Stable u32 id for an extension rule condition.
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
177pub struct ExtensionRuleConditionId(pub u32);
178
179impl_extension_id!(ExtensionRuleConditionId);
180
181/// FNV-1a 32-bit hash folded into the extension range (high bit set).
182///
183/// Shared helper backing every `ExtensionXxxId::from_name`. Kept private
184/// so callers don't construct raw ids that bypass the high-bit invariant.
185#[must_use]
186const fn fnv1a_with_high_bit(name: &str) -> u32 {
187    let mut hash: u32 = 0x811c_9dc5;
188    let bytes = name.as_bytes();
189    let mut i = 0;
190    while i < bytes.len() {
191        hash ^= bytes[i] as u32;
192        hash = hash.wrapping_mul(0x0100_0193);
193        i += 1;
194    }
195    hash | 0x8000_0000
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn id_from_name_is_deterministic() {
204        assert_eq!(
205            ExtensionDataTypeId::from_name("tensor.gather"),
206            ExtensionDataTypeId::from_name("tensor.gather"),
207        );
208    }
209
210    #[test]
211    fn id_from_different_names_differ() {
212        let a = ExtensionDataTypeId::from_name("tensor.gather");
213        let b = ExtensionDataTypeId::from_name("tensor.scatter");
214        assert_ne!(a, b);
215    }
216
217    #[test]
218    fn every_id_is_in_extension_range() {
219        let id = ExtensionDataTypeId::from_name("anything");
220        assert!(id.is_extension(), "{:#010x} missing high bit", id.as_u32());
221        assert!(id.as_u32() & ExtensionDataTypeId::EXTENSION_RANGE_MASK != 0);
222    }
223}