Skip to main content

vyre_foundation/dispatch/
dialect_lookup.rs

1//! Dialect lookup contract shared by foundation-side consumers.
2//!
3//! This module is the dependency-inversion boundary between the reference
4//! interpreter and the driver registry. Reference code may ask for op ids and
5//! frozen op definitions through `DialectLookup`, but it must not depend on
6//! `vyre-driver` or the `vyre` meta crate.
7//!
8//! The trait is deliberately sealed by a hidden `__sealed` method on
9//! `DialectLookup`. Downstream crates can consume a lookup, but the only sanctioned
10//! implementations are installed by vyre driver crates so this surface can grow
11//! through additive default methods without breaking external implementors.
12
13use crate::ir_inner::model::program::Program;
14use lasso::ThreadedRodeo;
15use std::sync::{Arc, OnceLock};
16use vyre_spec::{AlgebraicLaw, CpuFn};
17
18/// Interned operation identifier used by every dialect lookup.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct InternedOpId(pub u32);
21
22fn get_interner() -> &'static ThreadedRodeo {
23    static INTERNER: OnceLock<ThreadedRodeo> = OnceLock::new();
24    INTERNER.get_or_init(ThreadedRodeo::new)
25}
26
27/// Intern a stable operation-id string into a compact process-local id.
28#[must_use]
29pub fn intern_string(s: &str) -> InternedOpId {
30    let interner = get_interner();
31    let key = interner.get_or_intern(s);
32    InternedOpId(key.into_inner().get())
33}
34
35/// Function pointer used by reference-backend lowerings.
36pub type ReferenceKind = CpuFn;
37
38/// Backend lowering context retained for source compatibility.
39#[derive(Default, Debug, Clone)]
40pub struct LoweringCtx<'a> {
41    /// Marker tying context references to the call lifetime.
42    pub unused: std::marker::PhantomData<&'a ()>,
43}
44
45/// Backend text module descriptor used by native lowering builders.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct TextModule {
48    /// Backend assembly text.
49    pub asm: String,
50    /// Backend format version encoded by the builder.
51    pub version: u32,
52}
53
54/// native-module module descriptor used by native lowering builders.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct NativeModule {
57    /// Backend-owned serialized AST payload.
58    pub ast: Vec<u8>,
59    /// Entry-point name.
60    pub entry: String,
61}
62
63/// Reserved builder type for the primary text lowering slot.
64pub type PrimaryTextBuilder = fn(&LoweringCtx<'_>) -> Result<(), String>;
65/// Reserved builder type for the primary binary lowering slot.
66pub type PrimaryBinaryBuilder = fn(&LoweringCtx<'_>) -> Vec<u32>;
67/// Builder type for the secondary text lowering slot.
68pub type SecondaryTextBuilder = fn(&LoweringCtx<'_>) -> TextModule;
69/// Builder type for native-module lowering.
70pub type NativeModuleBuilder = fn(&LoweringCtx<'_>) -> NativeModule;
71/// Builder-type erased payload for any out-of-tree backend.
72///
73/// Extension lowerings register a function that reads the shared
74/// [`LoweringCtx`] and writes backend-specific bytes into an opaque
75/// output buffer. The caller backend owns the payload format; the
76/// core dialect registry does not interpret the bytes  -  it only
77/// dispatches to the right builder by `BackendId`.
78///
79/// This is the extensibility lever: a concrete backend appends a new
80/// lowering *without*
81/// editing vyre-foundation, vyre-driver, or vyre-spec. The core
82/// surface remains frozen.
83pub type ExtensionLoweringFn =
84    fn(&LoweringCtx<'_>) -> Result<std::vec::Vec<u8>, std::string::String>;
85
86/// Lowering function table attached to an operation definition.
87///
88/// The named fields are terminal 0.6 in-tree slots. `extensions` is
89/// the open-ended slot: any
90/// out-of-tree backend registers its builder under its stable
91/// backend-id string. Look up by id via
92/// [`LoweringTable::extension`].
93///
94/// Not `#[non_exhaustive]` so static registrations can use functional
95/// record update (`..LoweringTable::empty()`) from `inventory::submit!`
96/// closures. Additive fields must carry defaults so the spread form
97/// keeps working without a breaking change.
98#[derive(Clone)]
99pub struct LoweringTable {
100    /// Portable CPU reference implementation.
101    pub cpu_ref: ReferenceKind,
102    /// Primary text builder. `None` in v0.4.1 pure-IR ops.
103    pub primary_text: Option<PrimaryTextBuilder>,
104    /// Primary binary builder. `None` in v0.4.1 pure-IR ops.
105    pub primary_binary: Option<PrimaryBinaryBuilder>,
106    /// Secondary text builder. `None` unless a concrete backend owns it.
107    pub secondary_text: Option<SecondaryTextBuilder>,
108    /// Native native-module builder. `None` until native-module support lands.
109    pub native_module: Option<NativeModuleBuilder>,
110    /// Open extension map for out-of-tree backends. Keyed by backend
111    /// id (matches the string a `VyreBackend::id` returns). Builders
112    /// are by-value function pointers so lookup is allocation-free
113    /// and the map stays `Clone + Send + Sync` without interior
114    /// locking.
115    pub extensions: rustc_hash::FxHashMap<&'static str, ExtensionLoweringFn>,
116}
117
118impl Default for LoweringTable {
119    fn default() -> Self {
120        Self::empty()
121    }
122}
123
124impl LoweringTable {
125    /// Build a lowering table with only the explicit CPU reference oracle
126    /// populated. Production execution still requires a concrete backend
127    /// lowering (`primary_*`, `secondary_text`, `native_module`, or an
128    /// extension); this constructor is for parity/conformance surfaces and
129    /// incremental backend registration.
130    #[must_use]
131    pub fn new(cpu_ref: ReferenceKind) -> Self {
132        Self {
133            cpu_ref,
134            primary_text: None,
135            primary_binary: None,
136            secondary_text: None,
137            native_module: None,
138            extensions: rustc_hash::FxHashMap::default(),
139        }
140    }
141
142    /// Empty table whose reference-oracle slot is the structured-intrinsic
143    /// sentinel. Invoking that slot panics after clearing output so missing
144    /// reference adapters cannot masquerade as empty CPU results. This is not
145    /// a production fallback path.
146    #[must_use]
147    pub fn empty() -> Self {
148        // Read through the static, never by naming the function: see
149        // `cpu_op::SENTINEL_CPU_REF` for why the address of the function
150        // itself is not a reliable identity.
151        let cpu_ref = crate::cpu_op::SENTINEL_CPU_REF;
152        Self {
153            cpu_ref,
154            primary_text: None,
155            primary_binary: None,
156            secondary_text: None,
157            native_module: None,
158            extensions: rustc_hash::FxHashMap::default(),
159        }
160    }
161
162    /// Register an out-of-tree backend's lowering. Stable backend id
163    /// is the key `DialectRegistry::get_lowering` uses for lookup; pick it
164    /// carefully, it is a wire-like identifier.
165    #[must_use]
166    pub fn with_extension(
167        mut self,
168        backend_id: &'static str,
169        builder: ExtensionLoweringFn,
170    ) -> Self {
171        self.extensions.insert(backend_id, builder);
172        self
173    }
174
175    /// Look up an extension builder by backend id.
176    #[must_use]
177    pub fn extension(&self, backend_id: &str) -> Option<ExtensionLoweringFn> {
178        self.extensions.get(backend_id).copied()
179    }
180}
181
182impl std::fmt::Debug for LoweringTable {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        f.debug_struct("LoweringTable")
185            .field("cpu_ref", &"<fn>")
186            .field("primary_text", &self.primary_text.map(|_| "<fn>"))
187            .field("primary_binary", &self.primary_binary.map(|_| "<fn>"))
188            .field("secondary_text", &self.secondary_text.map(|_| "<fn>"))
189            .field("native_module", &self.native_module.map(|_| "<fn>"))
190            .field(
191                "extensions",
192                &self
193                    .extensions
194                    .keys()
195                    .copied()
196                    .collect::<std::vec::Vec<_>>(),
197            )
198            .finish()
199    }
200}
201
202/// Attribute value type declared by an operation schema.
203#[derive(Debug, Clone, PartialEq, Eq)]
204#[non_exhaustive]
205pub enum AttrType {
206    /// Unsigned 32-bit integer.
207    U32,
208    /// Signed 32-bit integer.
209    I32,
210    /// IEEE-754 binary32.
211    F32,
212    /// Boolean.
213    Bool,
214    /// Opaque byte string.
215    Bytes,
216    /// UTF-8 string.
217    String,
218    /// Enumerated string value.
219    Enum(&'static [&'static str]),
220    /// Unknown extension attribute.
221    Unknown,
222}
223
224/// Attribute schema entry.
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct AttrSchema {
227    /// Attribute name.
228    pub name: &'static str,
229    /// Attribute value type.
230    pub ty: AttrType,
231    /// Optional default value.
232    pub default: Option<&'static str>,
233}
234
235/// Typed input or output parameter.
236#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct TypedParam {
238    /// Parameter name.
239    pub name: &'static str,
240    /// Stable type spelling.
241    pub ty: &'static str,
242}
243
244/// Operation signature contract.
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct Signature {
247    /// Input parameters.
248    pub inputs: &'static [TypedParam],
249    /// Output parameters.
250    pub outputs: &'static [TypedParam],
251    /// Attribute parameters.
252    pub attrs: &'static [AttrSchema],
253    /// True when this op may read `DataType::Bytes` buffers.
254    pub bytes_extraction: bool,
255}
256
257impl Signature {
258    /// Construct a signature for an op that performs bytes extraction.
259    #[must_use]
260    pub const fn bytes_extractor(
261        inputs: &'static [TypedParam],
262        outputs: &'static [TypedParam],
263        attrs: &'static [AttrSchema],
264    ) -> Self {
265        Self {
266            inputs,
267            outputs,
268            attrs,
269            bytes_extraction: true,
270        }
271    }
272}
273
274/// Operation category.
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276pub enum Category {
277    /// Composition over IR.
278    Composite,
279    /// Extension op supplied by another crate.
280    Extension,
281    /// Intrinsic op supplied by a backend or primitive table.
282    Intrinsic,
283}
284
285/// Frozen operation definition.
286#[derive(Debug, Clone)]
287pub struct OpDef {
288    /// Stable operation id.
289    pub id: &'static str,
290    /// Stable dialect namespace.
291    pub dialect: &'static str,
292    /// Operation category.
293    pub category: Category,
294    /// Operation signature.
295    pub signature: Signature,
296    /// Backend lowering entries.
297    pub lowerings: LoweringTable,
298    /// Algebraic laws declared for conformance.
299    pub laws: &'static [AlgebraicLaw],
300    /// Composition-inlinable program builder.
301    pub compose: Option<fn() -> Program>,
302}
303
304impl OpDef {
305    /// Stable operation id.
306    #[must_use]
307    pub const fn id(&self) -> &'static str {
308        self.id
309    }
310
311    /// Build the canonical composition program when the operation has one.
312    #[must_use]
313    pub fn program(&self) -> Option<Program> {
314        self.compose
315            .map(|compose| compose().with_entry_op_id(self.id))
316    }
317}
318
319impl Default for OpDef {
320    fn default() -> Self {
321        Self {
322            id: "",
323            dialect: "",
324            category: Category::Intrinsic,
325            signature: Signature {
326                inputs: &[],
327                outputs: &[],
328                attrs: &[],
329                bytes_extraction: false,
330            },
331            lowerings: LoweringTable::empty(),
332            laws: &[],
333            compose: None,
334        }
335    }
336}
337
338#[doc(hidden)]
339pub mod private {
340    pub trait Sealed {}
341}
342
343/// Minimal lookup surface consumed by foundation-side reference code.
344pub trait DialectLookup: private::Sealed + Send + Sync {
345    /// Stable identifier naming the provider implementation.
346    ///
347    /// Two installs sharing the same `provider_id` are treated as the same
348    /// logical provider  -  a second install is an idempotent no-op. Two
349    /// installs with different ids are a conflict returned from
350    /// [`install_dialect_lookup`] so callers can fail their own setup without
351    /// panicking inside foundation.
352    fn provider_id(&self) -> &'static str;
353
354    /// Intern a stable operation id.
355    fn intern_op(&self, name: &str) -> InternedOpId;
356
357    /// Resolve an interned operation id to its frozen definition.
358    fn lookup(&self, id: InternedOpId) -> Option<&'static OpDef>;
359}
360
361static DIALECT_LOOKUP: OnceLock<Arc<dyn DialectLookup>> = OnceLock::new();
362
363/// Install the process-wide dialect lookup provider.
364///
365/// First caller wins. A second install from a provider that reports the
366/// same [`DialectLookup::provider_id`] is a silent no-op so harnesses can
367/// defensively call this at the top of every test without racing. A second
368/// install from a provider reporting a DIFFERENT `provider_id` returns an error with
369/// both ids named, because two divergent providers mapping the same op ids
370/// would corrupt every lookup-dependent pass (validator, reference, shadow
371/// diff, conformance matrix) in ways that are hard to attribute back to the
372/// install site. Failing here keeps the 60-second root-cause trace from
373/// LAW 4 intact.
374///
375/// # Errors
376///
377/// Returns an actionable error when a different provider is already installed
378/// or when the process-global lookup reaches an impossible `OnceLock` state.
379pub fn install_dialect_lookup(lookup: Arc<dyn DialectLookup>) -> Result<(), String> {
380    match DIALECT_LOOKUP.get() {
381        Some(existing) => {
382            let existing_id = existing.provider_id();
383            let incoming_id = lookup.provider_id();
384            ensure_same_provider(existing_id, incoming_id)?;
385        }
386        None => {
387            if let Err(lookup) = DIALECT_LOOKUP.set(lookup) {
388                // Lost a race with another thread; still need to validate
389                // idempotency so a concurrent install with a different id
390                // does not silently corrupt the process-wide lookup.
391                let Some(existing) = DIALECT_LOOKUP.get() else {
392                    return Err(
393                        "dialect lookup install lost the value after OnceLock::set failed. Fix: report this impossible OnceLock state."
394                            .to_string(),
395                    );
396                };
397                let existing_id = existing.provider_id();
398                let incoming_id = lookup.provider_id();
399                ensure_same_provider(existing_id, incoming_id)?;
400            }
401        }
402    }
403    Ok(())
404}
405
406fn ensure_same_provider(existing_id: &str, incoming_id: &str) -> Result<(), String> {
407    if existing_id == incoming_id {
408        Ok(())
409    } else {
410        Err(format!(
411            "dialect lookup already installed by provider `{existing_id}`; second installer `{incoming_id}` reports a different id. Fix: pick one provider for the process or reuse the first provider's id. Silent replacement is refused because two divergent lookups would mis-resolve op ids at runtime."
412        ))
413    }
414}
415
416/// Return the installed process-wide dialect lookup provider.
417#[must_use]
418pub fn dialect_lookup() -> Option<&'static dyn DialectLookup> {
419    DIALECT_LOOKUP.get().map(Arc::as_ref)
420}
421
422#[cfg(test)]
423mod tests;