Skip to main content

vyre_reference/
dialect_dispatch.rs

1//! Dispatch entry point that routes through the `DialectRegistry`.
2//!
3//! B-B4 routes CPU reference calls through the dialect registry. The
4//! execution tree keeps the direct fast path for built-ins while this
5//! module provides the extensible dispatch contract that cross-backend
6//! comparison, demos, and third-party dialect crates consume.
7//!
8//! The registry-driven path gives one concrete benefit: external
9//! dialect crates can
10//! `inventory::submit!` an `OpDef` (`vyre_driver::OpDef`) with an
11//! executable `cpu_ref` function, and the reference interpreter
12//! immediately knows how to run it — no patch to vyre-reference
13//! required.
14//!
15//! The BackendRegistration exported below makes the capability
16//! layer aware of the reference backend so Programs that reference
17//! unsupported dialects surface a clean `Unsupported` error
18//! instead of panicking at dispatch time.
19
20use crate::execution::call::invoke_cpu_ref;
21use vyre::{cpu_op::is_fallback_cpu_ref, Error, OpDef};
22
23/// Run a single op against its registered CPU reference.
24///
25/// Category C IO ops and foundation's structured CPU fallback are rejected
26/// before invocation so the reference backend reports unsupported capability
27/// instead of executing a non-executable structured fallback.
28///
29/// # Errors
30///
31/// Returns `Error::Interp` when:
32///
33/// * The op id is not registered with any dialect.
34/// * The registered op is a Category C IO op, which has no portable CPU path.
35/// * The registered op still points at foundation's structured CPU fallback.
36pub fn dispatch_op(op_id: &str, input: &[u8], output: &mut Vec<u8>) -> Result<(), Error> {
37    let lookup = vyre::dialect_lookup().ok_or_else(|| {
38        Error::interp(format!(
39            "reference interpreter: no DialectLookup is installed. Fix: initialize vyre-driver before dispatching `{op_id}`."
40        ))
41    })?;
42    let interned = lookup.intern_op(op_id);
43    let op_def = lookup.lookup(interned).ok_or_else(|| {
44        Error::interp(format!(
45            "reference interpreter: op `{op_id}` is not registered. Fix: link the dialect crate that provides `{op_id}`."
46        ))
47    })?;
48
49    reject_unsupported_cpu_dispatch(op_id, op_def)?;
50
51    invoke_cpu_ref(op_id, op_def.lowerings.cpu_ref, input, output)
52}
53
54fn reject_unsupported_cpu_dispatch(op_id: &str, op_def: &OpDef) -> Result<(), Error> {
55    if op_def.dialect == "io" {
56        return Err(Error::interp(format!(
57            "unsupported capability for `{op_id}` on reference/CPU backend: Category C IO ops are registered for composition but require a backend lowering for zero-copy NVMe/GDS execution. Fix: select or register a backend that advertises the `io` dialect capability, or reject the program during capability negotiation before reference dispatch."
58        )));
59    }
60
61    if is_fallback_cpu_ref(op_def.lowerings.cpu_ref) {
62        return Err(Error::interp(format!(
63            "unsupported CPU reference dispatch for `{op_id}`: the op is registered with foundation's structured intrinsic fallback, not an executable flat-ABI CPU implementation. Fix: implement a typed reference adapter for `{op_id}` or route the program to a backend that declares a native lowering for this capability."
64        )));
65    }
66
67    Ok(())
68}
69
70/// Capabilities advertised by the reference backend.
71///
72/// The reference interpreter supports every dialect whose ops
73/// declare a non-trivial `cpu_ref`. The registration below is
74/// discovered via `inventory::iter`; callers ask the registry
75/// "does this program fit the reference backend" and the answer
76/// folds into the capability-negotiation layer (B-B5).
77pub const REFERENCE_BACKEND_NAME: &str = "reference";
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use std::sync::Arc;
83    use vyre::{
84        install_dialect_lookup, intern_string, DialectLookup, InternedOpId, LoweringTable, OpDef,
85    };
86
87    struct TestLookup;
88
89    fn echo_ref(input: &[u8], output: &mut Vec<u8>) {
90        output.extend_from_slice(input);
91    }
92
93    fn panic_ref(_: &[u8], output: &mut Vec<u8>) {
94        output.extend_from_slice(&[0xDE, 0xAD]);
95        panic!("malformed primitive input");
96    }
97
98    static IO_DEF: std::sync::OnceLock<OpDef> = std::sync::OnceLock::new();
99    static ECHO_DEF: std::sync::OnceLock<OpDef> = std::sync::OnceLock::new();
100    static FALLBACK_DEF: std::sync::OnceLock<OpDef> = std::sync::OnceLock::new();
101    static PANIC_DEF: std::sync::OnceLock<OpDef> = std::sync::OnceLock::new();
102
103    impl vyre_foundation::dialect_lookup::private::Sealed for TestLookup {}
104
105    impl DialectLookup for TestLookup {
106        fn provider_id(&self) -> &'static str {
107            "vyre-reference::dialect_dispatch::TestLookup"
108        }
109
110        fn intern_op(&self, name: &str) -> InternedOpId {
111            intern_string(name)
112        }
113
114        fn lookup(&self, id: InternedOpId) -> Option<&'static OpDef> {
115            if id == intern_string("io.dma_from_nvme") {
116                return Some(IO_DEF.get_or_init(|| OpDef {
117                    id: "io.dma_from_nvme",
118                    dialect: "io",
119                    lowerings: LoweringTable::new(echo_ref),
120                    ..OpDef::default()
121                }));
122            }
123            if id == intern_string("test.echo") {
124                return Some(ECHO_DEF.get_or_init(|| OpDef {
125                    id: "test.echo",
126                    dialect: "test",
127                    lowerings: LoweringTable::new(echo_ref),
128                    ..OpDef::default()
129                }));
130            }
131            if id == intern_string("test.structured_fallback") {
132                return Some(FALLBACK_DEF.get_or_init(|| OpDef {
133                    id: "test.structured_fallback",
134                    dialect: "test",
135                    lowerings: LoweringTable::empty(),
136                    ..OpDef::default()
137                }));
138            }
139            if id == intern_string("test.panics") {
140                return Some(PANIC_DEF.get_or_init(|| OpDef {
141                    id: "test.panics",
142                    dialect: "test",
143                    lowerings: LoweringTable::new(panic_ref),
144                    ..OpDef::default()
145                }));
146            }
147            None
148        }
149    }
150
151    fn install_test_lookup() {
152        install_dialect_lookup(Arc::new(TestLookup))
153            .expect("Fix: test dialect lookup install should succeed or be idempotent");
154    }
155
156    #[test]
157    fn unknown_op_surfaces_clean_error() {
158        install_test_lookup();
159        let mut out = Vec::new();
160        let err = dispatch_op("nonexistent.op", &[], &mut out).expect_err("must fail");
161        let msg = format!("{err}");
162        assert!(
163            msg.contains("nonexistent.op"),
164            "error message must name the op: {msg}"
165        );
166        assert!(
167            msg.contains("Fix:"),
168            "error must carry actionable Fix: hint"
169        );
170    }
171
172    #[test]
173    fn registered_executable_cpu_ref_dispatches() {
174        install_test_lookup();
175        let mut out = Vec::new();
176        dispatch_op("test.echo", &[9, 8, 7], &mut out)
177            .expect("Fix: echo has executable cpu_ref; restore this invariant before continuing.");
178        assert_eq!(out, [9, 8, 7]);
179    }
180
181    #[test]
182    fn panicking_cpu_ref_returns_structured_error_without_output_drift() {
183        install_test_lookup();
184        let mut out = vec![0xAA];
185        let err = dispatch_op("test.panics", &[1, 2, 3], &mut out)
186            .expect_err("panicking cpu_ref must become a structured interpreter error");
187        let msg = format!("{err}");
188        assert!(
189            msg.contains("test.panics") && msg.contains("panicked") && msg.contains("Fix:"),
190            "panic error must name the op and stay actionable: {msg}"
191        );
192        assert_eq!(
193            out,
194            vec![0xAA],
195            "failed CPU refs must not leak partial output bytes"
196        );
197    }
198
199    #[test]
200    fn io_cat_c_op_refuses_reference_cpu_dispatch() {
201        install_test_lookup();
202        let mut out = vec![0xAA];
203        let err = dispatch_op("io.dma_from_nvme", &[], &mut out)
204            .expect_err("Category C io must not execute on reference/CPU");
205        let msg = format!("{err}");
206        assert!(
207            msg.contains("io.dma_from_nvme"),
208            "error message must name the op: {msg}"
209        );
210        assert!(
211            msg.contains("unsupported capability"),
212            "error must classify this as unsupported capability: {msg}"
213        );
214        assert!(
215            msg.contains("Category C IO"),
216            "error must identify Category C IO: {msg}"
217        );
218        assert!(
219            msg.contains("Fix:"),
220            "error must carry actionable Fix: hint: {msg}"
221        );
222        assert_eq!(
223            out,
224            vec![0xAA],
225            "dispatcher must reject before invoking the CPU sentinel"
226        );
227    }
228
229    #[test]
230    fn structured_cpu_fallback_refuses_reference_dispatch() {
231        install_test_lookup();
232        let mut out = vec![0xAA];
233        let err = dispatch_op("test.structured_fallback", &[1, 2], &mut out)
234            .expect_err("structured fallback must not look executable");
235        let msg = format!("{err}");
236        assert!(
237            msg.contains("test.structured_fallback"),
238            "error message must name the op: {msg}"
239        );
240        assert!(
241            msg.contains("structured intrinsic fallback"),
242            "error must name foundation's fallback path: {msg}"
243        );
244        assert!(
245            msg.contains("Fix:"),
246            "error must carry actionable Fix: hint: {msg}"
247        );
248        assert_eq!(
249            out,
250            vec![0xAA],
251            "dispatcher must reject before invoking structured_intrinsic_cpu"
252        );
253    }
254
255    #[test]
256    fn reference_backend_name_is_stable() {
257        install_test_lookup();
258        assert_eq!(REFERENCE_BACKEND_NAME, "reference");
259    }
260}