Skip to main content

vyre_foundation/dispatch/
extern_registry.rs

1//! Discovery layer for community vyre-libs dialect packs.
2//!
3//! Foundation owns these inventory types so both the driver registry and
4//! downstream consumers can share one link-time collection point without
5//! introducing a package cycle.
6
7#![forbid(unsafe_code)]
8
9use rustc_hash::{FxHashMap, FxHashSet};
10
11/// Metadata describing a community-registered dialect pack.
12#[derive(Debug, Clone, PartialEq, Eq)]
13#[non_exhaustive]
14pub struct ExternDialect {
15    /// Dialect crate name on crates.io. Must start with `vyre-libs-`.
16    pub name: &'static str,
17    /// Crate version at link time. Informational.
18    pub version: &'static str,
19    /// Public repository URL (for diagnostics + trust).
20    pub crate_repo: &'static str,
21}
22
23impl ExternDialect {
24    /// Construct a dialect metadata entry.
25    #[must_use]
26    pub const fn new(name: &'static str, version: &'static str, crate_repo: &'static str) -> Self {
27        Self {
28            name,
29            version,
30            crate_repo,
31        }
32    }
33}
34
35inventory::collect!(ExternDialect);
36
37/// Individual Cat-A op contributed by a community dialect.
38#[derive(Debug, Clone, PartialEq, Eq)]
39#[non_exhaustive]
40pub struct ExternOp {
41    /// Owning dialect (matches [`ExternDialect::name`]).
42    pub dialect: &'static str,
43    /// Fully-qualified op id (e.g. `"vyre-libs-quant::int8::matmul"`).
44    pub op_id: &'static str,
45}
46
47impl ExternOp {
48    /// Construct an op registration.
49    #[must_use]
50    pub const fn new(dialect: &'static str, op_id: &'static str) -> Self {
51        Self { dialect, op_id }
52    }
53}
54
55inventory::collect!(ExternOp);
56
57/// Every dialect registered at link time.
58#[must_use]
59pub fn dialects() -> Vec<&'static ExternDialect> {
60    let iter = inventory::iter::<ExternDialect>();
61    let (lo, hi) = iter.size_hint();
62    let mut out = Vec::with_capacity(hi.unwrap_or(lo));
63    out.extend(iter);
64    out
65}
66
67/// Every registered op belonging to `dialect`.
68#[must_use]
69pub fn ops_in_dialect(dialect: &str) -> Vec<&'static ExternOp> {
70    let iter = inventory::iter::<ExternOp>().filter(|op| op.dialect == dialect);
71    let (lo, hi) = iter.size_hint();
72    let mut out = Vec::with_capacity(hi.unwrap_or(lo));
73    out.extend(iter);
74    out
75}
76
77/// Every registered op across every dialect.
78#[must_use]
79pub fn all_ops() -> Vec<&'static ExternOp> {
80    let iter = inventory::iter::<ExternOp>();
81    let (lo, hi) = iter.size_hint();
82    let mut out = Vec::with_capacity(hi.unwrap_or(lo));
83    out.extend(iter);
84    out
85}
86
87/// Structured validation error surfaced by [`verify`].
88#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
89#[non_exhaustive]
90pub enum ExternVerifyError {
91    /// Two or more `ExternDialect` entries share the same `name`.
92    #[error("duplicate dialect name `{name}`: {count} entries registered. Fix: pick a unique crates.io name for each community pack.")]
93    DuplicateDialect {
94        /// The offending dialect name.
95        name: &'static str,
96        /// Number of entries sharing this name.
97        count: usize,
98    },
99
100    /// Dialect `name` does not start with the reserved `vyre-libs-` prefix.
101    #[error("dialect name `{name}` does not start with `vyre-libs-`. Fix: rename the pack crate and its ExternDialect::name to begin with `vyre-libs-`.")]
102    MalformedDialectName {
103        /// The offending dialect name.
104        name: &'static str,
105    },
106
107    /// An `ExternOp` references a `dialect` name that no
108    /// `ExternDialect` entry claims.
109    #[error("orphan op `{op_id}` references dialect `{dialect}`, which is not registered. Fix: make sure the dialect's crate registers an `ExternDialect` entry with this name.")]
110    OrphanOp {
111        /// The orphan op's dialect reference.
112        dialect: &'static str,
113        /// The op id whose dialect is missing.
114        op_id: &'static str,
115    },
116
117    /// `ExternOp.op_id` is an empty string.
118    #[error("op registered with empty op_id under dialect `{dialect}`. Fix: every op must carry a fully-qualified id like `<dialect>::<op_name>`.")]
119    EmptyOpId {
120        /// The dialect claiming an empty-id op.
121        dialect: &'static str,
122    },
123}
124
125/// Run every consistency check across every registered extern dialect and op.
126///
127/// # Errors
128///
129/// Returns every discovered validation error.
130pub fn verify() -> Result<(), Vec<ExternVerifyError>> {
131    let mut errors = Vec::new();
132
133    // Single dialect sweep: duplicate counts, malformed-name checks, and the
134    // dialect-name set for orphan-op detection (VYRE_EXTERN_VERIFY HOT).
135    let mut counts: FxHashMap<&'static str, usize> = FxHashMap::default();
136    let mut known: FxHashSet<&'static str> = FxHashSet::default();
137    for dialect in inventory::iter::<ExternDialect>() {
138        *counts.entry(dialect.name).or_insert(0) += 1;
139        known.insert(dialect.name);
140        if !dialect.name.starts_with("vyre-libs-") {
141            errors.push(ExternVerifyError::MalformedDialectName { name: dialect.name });
142        }
143    }
144    for (name, count) in counts {
145        if count > 1 {
146            errors.push(ExternVerifyError::DuplicateDialect { name, count });
147        }
148    }
149
150    for op in inventory::iter::<ExternOp>() {
151        if op.op_id.is_empty() {
152            errors.push(ExternVerifyError::EmptyOpId {
153                dialect: op.dialect,
154            });
155        }
156        if !known.contains(op.dialect) {
157            errors.push(ExternVerifyError::OrphanOp {
158                dialect: op.dialect,
159                op_id: op.op_id,
160            });
161        }
162    }
163
164    if errors.is_empty() {
165        Ok(())
166    } else {
167        Err(errors)
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn extern_dialect_construction() {
177        let d = ExternDialect::new("vyre-libs-quant", "0.1.0", "https://github.com/example");
178        assert_eq!(d.name, "vyre-libs-quant");
179        assert_eq!(d.version, "0.1.0");
180        assert_eq!(d.crate_repo, "https://github.com/example");
181    }
182
183    #[test]
184    fn extern_op_construction() {
185        let op = ExternOp::new("vyre-libs-quant", "vyre-libs-quant::int8::matmul");
186        assert_eq!(op.dialect, "vyre-libs-quant");
187        assert_eq!(op.op_id, "vyre-libs-quant::int8::matmul");
188    }
189
190    #[test]
191    fn duplicate_dialect_error_display() {
192        let err = ExternVerifyError::DuplicateDialect {
193            name: "vyre-libs-x",
194            count: 3,
195        };
196        let msg = err.to_string();
197        assert!(msg.contains("vyre-libs-x"));
198        assert!(msg.contains("3"));
199    }
200
201    #[test]
202    fn malformed_name_error_display() {
203        let err = ExternVerifyError::MalformedDialectName { name: "bad-name" };
204        let msg = err.to_string();
205        assert!(msg.contains("bad-name"));
206        assert!(msg.contains("vyre-libs-"));
207    }
208
209    #[test]
210    fn orphan_op_error_display() {
211        let err = ExternVerifyError::OrphanOp {
212            dialect: "missing-dialect",
213            op_id: "missing::op",
214        };
215        assert!(err.to_string().contains("orphan"));
216    }
217
218    #[test]
219    fn empty_op_id_error_display() {
220        let err = ExternVerifyError::EmptyOpId {
221            dialect: "vyre-libs-x",
222        };
223        assert!(err.to_string().contains("empty"));
224    }
225
226    #[test]
227    fn verify_empty_registry_succeeds() {
228        // No ExternDialect or ExternOp are submitted in this test crate,
229        // so verify should pass (at minimum — other tests may submit entries).
230        let result = verify();
231        // Either Ok or the errors are all from other test crates.
232        if let Err(errors) = &result {
233            // All errors should be well-formed.
234            for e in errors {
235                let _ = e.to_string();
236            }
237        }
238    }
239}