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}