Skip to main content

vyre_foundation/
operation.rs

1//! Canonical semantic operation registration and derived catalog views.
2
3use std::borrow::Cow;
4use std::collections::BTreeMap;
5use std::sync::LazyLock;
6
7use crate::dialect_lookup::Signature;
8use crate::ir::{BufferAccess, Program};
9use crate::program_caps::{scan as scan_capabilities, RequiredCapabilities};
10
11/// Deterministic fixture input cases. One case contains declaration-ordered buffers.
12pub type OperationFixtures = fn() -> Vec<Vec<Vec<u8>>>;
13/// One immutable semantic record used by validation, inlining, conformance,
14/// documentation, and target-facet joins.
15#[derive(Clone, Copy, Debug)]
16pub struct SemanticOperation {
17    /// Stable operation identifier.
18    pub id: &'static str,
19    /// Semantic schema version.
20    pub semantic_version: u32,
21    /// Explicit callable signature when the operation is used through `Expr::Call`.
22    pub signature: Option<&'static Signature>,
23    /// Semantic tier.
24    pub tier: OperationTier,
25    /// Derived dialect/category namespace.
26    pub category: Option<&'static str>,
27    /// Optional neutral program builder.
28    pub build: Option<fn() -> Program>,
29    /// Deterministic fixture inputs.
30    pub test_inputs: Option<OperationFixtures>,
31    /// Deterministic fixture outputs.
32    pub expected_output: Option<OperationFixtures>,
33    /// Algebraic or semantic law identifiers.
34    pub laws: &'static [&'static str],
35    /// Numerical comparison policy.
36    pub tolerance: TolerancePolicy,
37}
38
39impl SemanticOperation {
40    /// Build the canonical program and stamp its stable operation identity.
41    #[must_use]
42    pub fn program(self) -> Option<Program> {
43        self.build.map(|build| build().with_entry_op_id(self.id))
44    }
45
46    /// Derive target-neutral capability requirements from the canonical program.
47    #[must_use]
48    pub fn required_capabilities(self) -> Option<RequiredCapabilities> {
49        self.program().map(|program| scan_capabilities(&program))
50    }
51
52    /// Derive target-neutral effects from the canonical program.
53    #[must_use]
54    pub fn effects(self) -> Option<OperationEffects> {
55        self.program()
56            .map(|program| OperationEffects::from_program(&program))
57    }
58
59    /// Return the coarse category.
60    #[must_use]
61    pub const fn category(self) -> Option<&'static str> {
62        self.category
63    }
64
65    /// Return the permitted f32 drift in ULPs.
66    #[must_use]
67    pub const fn tolerance(self) -> u32 {
68        self.tolerance.f32_ulp
69    }
70}
71
72/// Coarse semantic tier used by catalog and conformance consumers.
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
74#[non_exhaustive]
75pub enum OperationTier {
76    /// Foundation IR or built-in operation.
77    Foundation,
78    /// Hardware-facing semantic intrinsic.
79    Intrinsic,
80    /// Reusable backend-neutral primitive.
81    Primitive,
82    /// Library composition over typed IR.
83    Library,
84    /// Runtime-owned semantic operation.
85    Runtime,
86    /// External extension operation.
87    External,
88    /// Identifier does not match an accepted semantic namespace.
89    Unknown,
90}
91
92impl OperationTier {
93    /// Stable operation-matrix spelling.
94    #[must_use]
95    pub const fn matrix_value(self) -> &'static str {
96        match self {
97            Self::Foundation => "foundation_ir",
98            Self::Intrinsic => "intrinsic",
99            Self::Primitive => "primitive",
100            Self::Library => "libs",
101            Self::Runtime => "runtime",
102            Self::External => "external",
103            Self::Unknown => "unknown",
104        }
105    }
106}
107
108/// Classify one operation identity by its canonical namespace.
109#[must_use]
110pub fn classify_operation_id(id: &str) -> OperationTier {
111    if id.starts_with("vyre-intrinsics::hardware::") {
112        OperationTier::Intrinsic
113    } else if id.starts_with("vyre-primitives::") {
114        OperationTier::Primitive
115    } else if id.starts_with("vyre-libs::") {
116        OperationTier::Library
117    } else if id.starts_with("core.") || id.starts_with("io.") || id.starts_with("mem.") {
118        OperationTier::Runtime
119    } else if id
120        .split_once("::")
121        .is_some_and(|(crate_name, _)| !crate_name.is_empty() && !crate_name.starts_with("vyre-"))
122    {
123        OperationTier::External
124    } else {
125        OperationTier::Unknown
126    }
127}
128
129/// Semantic memory and synchronization effects derived from an operation program.
130#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
131pub struct OperationEffects {
132    /// The operation reads caller-visible storage.
133    pub reads: bool,
134    /// The operation writes caller-visible storage.
135    pub writes: bool,
136    /// The operation contains atomic memory effects.
137    pub atomics: bool,
138    /// The operation requires intra- or inter-workgroup synchronization.
139    pub synchronizes: bool,
140}
141
142impl OperationEffects {
143    /// Derive neutral effects from the canonical program declaration and statistics.
144    #[must_use]
145    pub fn from_program(program: &Program) -> Self {
146        let mut effects = Self::default();
147        for buffer in program.buffers() {
148            match buffer.access() {
149                BufferAccess::ReadOnly => effects.reads = true,
150                BufferAccess::ReadWrite => {
151                    effects.reads = true;
152                    effects.writes = true;
153                }
154                BufferAccess::WriteOnly => effects.writes = true,
155                _ => {
156                    effects.reads = true;
157                    effects.writes = true;
158                }
159            }
160        }
161        let stats = program.stats();
162        effects.atomics = stats.atomic_op_count > 0;
163        effects.synchronizes = stats.has_node_barrier() || stats.distributed_collectives();
164        effects
165    }
166}
167
168/// Numerical comparison policy owned by the semantic operation.
169#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
170pub struct TolerancePolicy {
171    /// Maximum accepted f32 drift measured in ULPs.
172    pub f32_ulp: u32,
173}
174
175impl TolerancePolicy {
176    /// Exact byte identity.
177    pub const EXACT: Self = Self { f32_ulp: 0 };
178
179    /// Construct an f32 ULP tolerance.
180    #[must_use]
181    pub const fn f32_ulp(maximum: u32) -> Self {
182        Self { f32_ulp: maximum }
183    }
184}
185
186/// One semantic operation identity and all target-neutral catalog policy.
187pub struct OperationRegistration {
188    /// Stable operation identifier.
189    pub id: &'static str,
190    /// Semantic schema version.
191    pub semantic_version: u32,
192    /// Optional explicitly declared signature. When absent, [`Self::program`] is authoritative.
193    pub signature: Option<Signature>,
194    /// Semantic tier.
195    pub tier: OperationTier,
196    /// Coarse taxonomy category.
197    pub category: Option<&'static str>,
198    /// Optional neutral program builder.
199    pub build: Option<fn() -> Program>,
200    /// Deterministic fixture inputs.
201    pub test_inputs: Option<OperationFixtures>,
202    /// Optional deterministic fixture outputs or reference-oracle projection.
203    pub expected_output: Option<OperationFixtures>,
204    /// Algebraic or semantic law identifiers.
205    pub laws: &'static [&'static str],
206    /// Numerical comparison policy.
207    pub tolerance: TolerancePolicy,
208}
209
210impl OperationRegistration {
211    /// Construct a neutral operation registration with exact comparison policy.
212    #[must_use]
213    pub const fn new(
214        id: &'static str,
215        tier: OperationTier,
216        build: Option<fn() -> Program>,
217        test_inputs: Option<OperationFixtures>,
218        expected_output: Option<OperationFixtures>,
219    ) -> Self {
220        Self {
221            id,
222            semantic_version: 1,
223            signature: None,
224            tier,
225            category: None,
226            build,
227            test_inputs,
228            expected_output,
229            laws: &[],
230            tolerance: TolerancePolicy::EXACT,
231        }
232    }
233
234    /// Construct a library-composition registration.
235    #[must_use]
236    pub const fn library(
237        id: &'static str,
238        build: fn() -> Program,
239        test_inputs: Option<OperationFixtures>,
240        expected_output: Option<OperationFixtures>,
241    ) -> Self {
242        Self::new(
243            id,
244            OperationTier::Library,
245            Some(build),
246            test_inputs,
247            expected_output,
248        )
249    }
250
251    /// Construct a reusable primitive registration.
252    #[must_use]
253    pub const fn primitive(
254        id: &'static str,
255        build: fn() -> Program,
256        test_inputs: Option<OperationFixtures>,
257        expected_output: Option<OperationFixtures>,
258    ) -> Self {
259        Self::new(
260            id,
261            OperationTier::Primitive,
262            Some(build),
263            test_inputs,
264            expected_output,
265        )
266    }
267
268    /// Attach an explicit signature.
269    #[must_use]
270    pub const fn with_signature(mut self, signature: Signature) -> Self {
271        self.signature = Some(signature);
272        self
273    }
274
275    /// Attach a coarse category.
276    #[must_use]
277    pub const fn with_category(mut self, category: &'static str) -> Self {
278        self.category = Some(category);
279        self
280    }
281
282    /// Attach semantic law identifiers.
283    #[must_use]
284    pub const fn with_laws(mut self, laws: &'static [&'static str]) -> Self {
285        self.laws = laws;
286        self
287    }
288
289    /// Return the coarse category.
290    #[must_use]
291    pub const fn category(&self) -> Option<&'static str> {
292        self.category
293    }
294
295    /// Return the permitted f32 drift in ULPs.
296    #[must_use]
297    pub const fn tolerance(&self) -> u32 {
298        self.tolerance.f32_ulp
299    }
300
301    /// Attach the numerical tolerance policy.
302    #[must_use]
303    pub const fn with_tolerance(mut self, tolerance: TolerancePolicy) -> Self {
304        self.tolerance = tolerance;
305        self
306    }
307
308    /// Build the canonical program and stamp its stable operation identity.
309    #[must_use]
310    pub fn program(&self) -> Option<Program> {
311        self.build.map(|build| build().with_entry_op_id(self.id))
312    }
313
314    /// Derive target-neutral capability requirements from the canonical program.
315    #[must_use]
316    pub fn required_capabilities(&self) -> Option<RequiredCapabilities> {
317        self.program().map(|program| scan_capabilities(&program))
318    }
319
320    /// Derive target-neutral effects from the canonical program.
321    #[must_use]
322    pub fn effects(&self) -> Option<OperationEffects> {
323        self.program()
324            .map(|program| OperationEffects::from_program(&program))
325    }
326}
327impl From<&'static OperationRegistration> for SemanticOperation {
328    fn from(registration: &'static OperationRegistration) -> Self {
329        Self {
330            id: registration.id,
331            semantic_version: registration.semantic_version,
332            signature: registration.signature.as_ref(),
333            tier: registration.tier,
334            category: registration.category,
335            build: registration.build,
336            test_inputs: registration.test_inputs,
337            expected_output: registration.expected_output,
338            laws: registration.laws,
339            tolerance: registration.tolerance,
340        }
341    }
342}
343
344inventory::collect!(OperationRegistration);
345
346/// Catalog validation failure.
347#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
348pub enum OperationRegistryError {
349    /// Two linked registrations claimed one stable identity.
350    #[error("duplicate operation registration `{id}`; keep exactly one semantic owner")]
351    DuplicateId {
352        /// Duplicated stable operation id.
353        id: &'static str,
354    },
355    /// A registration used the reserved zero semantic version.
356    #[error("operation `{id}` uses semantic version zero; use a positive schema version")]
357    InvalidVersion {
358        /// Invalid operation id.
359        id: &'static str,
360    },
361    /// A registration supplied neither a neutral program nor an explicit signature.
362    #[error("operation `{id}` supplies neither a neutral program nor an explicit signature")]
363    MissingSemantics {
364        /// Incomplete operation id.
365        id: &'static str,
366    },
367    /// Registration tier does not match its canonical namespace.
368    #[error(
369        "operation `{id}` declares tier {declared:?}, but its canonical namespace classifies as {classified:?}"
370    )]
371    InvalidTier {
372        /// Invalid operation id.
373        id: &'static str,
374        /// Tier supplied by the registration.
375        declared: OperationTier,
376        /// Tier derived from the canonical namespace.
377        classified: OperationTier,
378    },
379}
380
381/// Immutable validated view over every linked semantic operation registration.
382pub struct OperationRegistry {
383    ordered: Vec<&'static OperationRegistration>,
384    by_id: BTreeMap<&'static str, &'static OperationRegistration>,
385}
386
387impl OperationRegistry {
388    fn build() -> Result<Self, OperationRegistryError> {
389        let mut ordered = inventory::iter::<OperationRegistration>
390            .into_iter()
391            .collect::<Vec<_>>();
392        ordered.sort_unstable_by_key(|entry| entry.id);
393        let mut by_id = BTreeMap::new();
394        for entry in &ordered {
395            if entry.semantic_version == 0 {
396                return Err(OperationRegistryError::InvalidVersion { id: entry.id });
397            }
398            if entry.build.is_none() && entry.signature.is_none() {
399                return Err(OperationRegistryError::MissingSemantics { id: entry.id });
400            }
401            let classified = classify_operation_id(entry.id);
402            if classified == OperationTier::Unknown || classified != entry.tier {
403                return Err(OperationRegistryError::InvalidTier {
404                    id: entry.id,
405                    declared: entry.tier,
406                    classified,
407                });
408            }
409            if by_id.insert(entry.id, *entry).is_some() {
410                return Err(OperationRegistryError::DuplicateId { id: entry.id });
411            }
412        }
413        Ok(Self { ordered, by_id })
414    }
415
416    /// Return the process-wide validated semantic operation registry.
417    #[must_use]
418    pub fn global() -> &'static Self {
419        static REGISTRY: LazyLock<OperationRegistry> = LazyLock::new(|| {
420            OperationRegistry::build()
421                .unwrap_or_else(|error| panic!("invalid semantic operation registry: {error}"))
422        });
423        &REGISTRY
424    }
425
426    /// Resolve one stable operation identity.
427    #[must_use]
428    pub fn get(&self, id: &str) -> Option<SemanticOperation> {
429        self.by_id.get(id).copied().map(SemanticOperation::from)
430    }
431
432    /// Iterate registrations in stable operation-id order.
433    pub fn iter(&self) -> impl ExactSizeIterator<Item = SemanticOperation> + '_ {
434        self.ordered.iter().copied().map(SemanticOperation::from)
435    }
436}
437
438/// Validated target identity carried by target-owned facet registrations.
439///
440/// Linked target owners construct borrowed identities at declaration time.
441/// Deserialized manifests retain owned identities without leaking storage.
442#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
443pub struct TargetId(Cow<'static, str>);
444
445impl TargetId {
446    /// Construct a borrowed target identity from an owner-defined stable spelling.
447    ///
448    /// # Errors
449    ///
450    /// Empty or whitespace-padded identities are rejected.
451    pub const fn new(id: &'static str) -> Result<Self, &'static str> {
452        if id.is_empty() || has_surrounding_ascii_whitespace(id.as_bytes()) {
453            return Err("target identity must be non-empty and contain no surrounding whitespace");
454        }
455        Ok(Self(Cow::Borrowed(id)))
456    }
457
458    /// Construct an owned target identity from persisted or caller-supplied data.
459    ///
460    /// # Errors
461    ///
462    /// Empty or whitespace-padded identities are rejected.
463    pub fn from_owned(id: String) -> Result<Self, &'static str> {
464        if id.is_empty() || has_surrounding_ascii_whitespace(id.as_bytes()) {
465            return Err("target identity must be non-empty and contain no surrounding whitespace");
466        }
467        Ok(Self(Cow::Owned(id)))
468    }
469
470    /// Return the stable owner-defined spelling.
471    #[must_use]
472    pub fn as_str(&self) -> &str {
473        self.0.as_ref()
474    }
475
476    /// Construct a validated borrowed target identity for a compile-time constant.
477    ///
478    /// # Panics
479    ///
480    /// Panics when the identity is empty or has surrounding whitespace.
481    #[must_use]
482    pub const fn expect_valid(id: &'static str) -> Self {
483        if id.is_empty() || has_surrounding_ascii_whitespace(id.as_bytes()) {
484            panic!("target identity must be non-empty and contain no surrounding whitespace");
485        }
486        Self(Cow::Borrowed(id))
487    }
488}
489
490impl serde::Serialize for TargetId {
491    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
492    where
493        S: serde::Serializer,
494    {
495        serializer.serialize_str(self.as_str())
496    }
497}
498
499impl<'de> serde::Deserialize<'de> for TargetId {
500    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
501    where
502        D: serde::Deserializer<'de>,
503    {
504        let id = <String as serde::Deserialize>::deserialize(deserializer)?;
505        Self::from_owned(id).map_err(serde::de::Error::custom)
506    }
507}
508
509impl std::fmt::Display for TargetId {
510    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
511        formatter.write_str(self.as_str())
512    }
513}
514
515impl PartialEq<&str> for TargetId {
516    fn eq(&self, other: &&str) -> bool {
517        self.as_str() == *other
518    }
519}
520
521const fn has_surrounding_ascii_whitespace(bytes: &[u8]) -> bool {
522    matches!(bytes.first(), Some(byte) if byte.is_ascii_whitespace())
523        || matches!(bytes.last(), Some(byte) if byte.is_ascii_whitespace())
524}
525
526/// Derived target-specific capability keyed by canonical semantic operation id.
527///
528/// Concrete drivers submit one backend registration containing their validated
529/// target identity, compiler, materializer, and supported-operation set. The
530/// shared driver joins that record with [`OperationRegistry`] to produce this
531/// read-only view without a second operation submission.
532#[derive(Clone, Debug, PartialEq, Eq)]
533pub struct TargetOperationFacet {
534    /// Canonical semantic operation id.
535    pub operation_id: &'static str,
536    /// Validated target identity from the concrete driver's registration.
537    pub target_id: TargetId,
538    /// Target facet schema version.
539    pub version: u32,
540}