Skip to main content

xpile_backend/
lib.rs

1//! Backend trait — code-lane emission abstraction.
2//!
3//! Every target language in xpile (Rust, Ruchy, PTX, WGSL, SPIR-V, Lean)
4//! provides one type implementing [`Backend`]. The trait is intentionally
5//! narrow: take meta-HIR and a config, return an [`Artifact`].
6//!
7//! Sibling of `xpile-frontend::Frontend`. Architectural invariants
8//! codified in `contracts/xpile-backend-trait-v1.yaml`.
9
10use serde::{Deserialize, Serialize};
11use xpile_contracts::ContractId;
12use xpile_meta_hir::Module;
13
14/// Target language a backend can emit.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
16pub enum Target {
17    /// Idiomatic Rust source. Implemented by `xpile-rust-codegen`.
18    Rust,
19    /// Ruchy source. Implemented by `xpile-ruchy-codegen`.
20    Ruchy,
21    /// NVIDIA PTX text. Implemented by `xpile-ptx-codegen`.
22    Ptx,
23    /// WebGPU Shading Language. Implemented by `xpile-wgsl-codegen`.
24    Wgsl,
25    /// SPIR-V text or binary. Implemented by `xpile-spirv-codegen` (future).
26    Spirv,
27    /// Lean 4 executable code (def, partial def, inductive, ...).
28    /// Implemented by `xpile-lean-codegen`. The proof-lane Lean (theorems)
29    /// goes through `xpile-lean-contract-backend` instead.
30    Lean,
31    /// POSIX shell (sh / bash / zsh) — the bashrs merger domain.
32    /// Implemented by `bashrs-backend` (scaffold at v0.1.0; full emit
33    /// at v0.2.0 once the bashrs source folding lands). PMAT-037 /
34    /// XPILE-BASHRS-MERGER-001. See `sub/bashrs-merger.md` Layer A.
35    Shell,
36}
37
38/// Lowering profile — the two-mHIR asymmetric decision for Rust↔Ruchy.
39///
40/// See `docs/specifications/sub/bidirectional-ruchy.md` (planned).
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42pub enum Profile {
43    /// meta-HIR normalized for Rust emission (default for most targets).
44    RustOut,
45    /// meta-HIR normalized for Ruchy emission (pipeline operator
46    /// reconstructed at emission time).
47    RuchyOut,
48}
49
50/// Hardware profile for targets whose emission depends on hardware
51/// capabilities (PTX `compute_capability`, WGSL feature set, SPIR-V version).
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub enum HwProfile {
54    Ptx {
55        /// e.g., "sm_80", "sm_89", "sm_90".
56        compute_capability: String,
57    },
58    Wgsl {
59        /// e.g., ["timestamp-query", "f16"].
60        features: Vec<String>,
61    },
62    Spirv {
63        version: (u32, u32),
64    },
65}
66
67/// Configuration passed to [`Backend::lower`].
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct BackendConfig {
70    pub target: Target,
71    pub profile: Profile,
72    pub hardware: Option<HwProfile>,
73}
74
75/// Emitted artifact — primary source/IR text plus sidecar files plus
76/// the structural citation chain to Layer-5 compile contracts.
77///
78/// The `citations` field is the structural channel that closes the
79/// audit chain: every target-specific IR construct in `primary` cites
80/// a Layer-5 compile contract by ID. Recovery is via this field, NOT
81/// via regex over `primary` text. See
82/// `contracts/xpile-backend-trait-v1.yaml` equation `compile_contract_citation`.
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub struct Artifact {
85    /// Emitted source / IR text (Rust source, Ruchy source, PTX text,
86    /// WGSL source, SPIR-V text, Lean source).
87    pub primary: String,
88    /// Optional binaries, manifests, debug maps, accompanying the primary.
89    pub sidecars: Vec<(String, Vec<u8>)>,
90    /// Layer-5 compile contracts sanctioning every target-specific
91    /// construct in `primary`. Structural — not regex-recoverable.
92    pub citations: Vec<ContractId>,
93    /// Multi-emitter quorum status (PMAT-262 / Section 29). At v0.1.0
94    /// every Backend impl emits exactly one artifact, so this is always
95    /// `QuorumStatus::Single { emitter: <backend_name> }`. Multi-emitter
96    /// backends (future rustc_codegen_nvvm + aprender-gpu quorum on PTX)
97    /// populate `QuorumStatus::Multi { ... }` with the diff_exec result.
98    ///
99    /// Defaults via serde to `Single { emitter: "unknown" }` for
100    /// backward-compatible deserialization of older JSON payloads.
101    #[serde(default = "default_quorum_status")]
102    pub quorum_status: QuorumStatus,
103}
104
105fn default_quorum_status() -> QuorumStatus {
106    QuorumStatus::Single {
107        emitter: "unknown".to_string(),
108    }
109}
110
111#[derive(Debug, thiserror::Error)]
112pub enum BackendError {
113    #[error("unsupported target: {0:?}")]
114    UnsupportedTarget(Target),
115    #[error("missing hardware profile for target {0:?}")]
116    MissingHardware(Target),
117    #[error("lowering error: {0}")]
118    Lower(String),
119    #[error("compile-contract citation missing for emitted construct: {0}")]
120    MissingCompileContractCitation(String),
121}
122
123/// Code-lane emission trait. See `docs/specifications/sub/backend-trait.md`.
124pub trait Backend: Send + Sync {
125    /// Human-readable backend name, e.g. "rust", "ptx", "wgsl".
126    fn name(&self) -> &'static str;
127
128    /// Targets this backend can emit. Each [`Target`] variant is
129    /// owned by exactly one Backend impl (`target_ownership` invariant).
130    fn targets(&self) -> &[Target];
131
132    /// Lower a meta-HIR module under the given config to an [`Artifact`].
133    ///
134    /// Invariants: deterministic per `(module, config)`; frame-pure
135    /// (no mutation of inputs); every target-specific IR construct
136    /// in `Artifact.primary` cited via `Artifact.citations`.
137    fn lower(&self, module: &Module, config: &BackendConfig) -> Result<Artifact, BackendError>;
138}
139
140// ─── PMAT-261 / Section 29: Multi-emitter quorum scaffolding ────────────
141//
142// Types codifying the design in
143// `docs/specifications/sub/layer5-multi-emitter-quorum.md`. Pure
144// scaffolding at PMAT-261 — no Backend impl yet uses these. Future PRs
145// (rustc_codegen_nvvm wiring, aprender-gpu bridge, DiffExec engine)
146// build against this stable API surface.
147
148/// Role of a backend emitter within a multi-emitter quorum.
149///
150/// `compile_targets.via.role` in the YAML schema corresponds to this
151/// enum. At most one `General` per quorum (the mandatory fallback);
152/// any number of `Specialist` (each with its own `shape_filter`).
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
154#[serde(rename_all = "snake_case")]
155pub enum EmitterRole {
156    /// Handles any contract-conforming input. Mandatory fallback —
157    /// `pv lint` (post-PMAT-262) will require at least one `General`
158    /// emitter per Layer-5 contract's `compile_targets.via`.
159    /// Examples: `rustc_codegen_nvvm` (PTX), `naga` (WGSL),
160    /// `rspirv` (SPIR-V).
161    General,
162    /// Handles a domain-specific subset via hand-tuned templates.
163    /// Optional — degrades gracefully to single-emitter when missing.
164    /// Examples: `aprender-gpu` (GEMM/MMA PTX kernels),
165    /// `bashrs-realistic` (corpus-tuned POSIX patterns).
166    Specialist,
167}
168
169/// Policy for combining outputs when both General and Specialist
170/// emitters fire on the same input. Configured per Layer-5 contract
171/// via `compile_targets.quorum_policy` in the YAML.
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173#[serde(rename_all = "snake_case", tag = "kind")]
174pub enum QuorumPolicy {
175    /// If the specialist handles the kernel, use its output. Falls
176    /// back to general otherwise. Single-vote Runtime stratum.
177    PreferSpecialist,
178    /// Emit via BOTH, run BOTH on test inputs, compare numerical
179    /// outputs within tolerance. Multi-vote Runtime stratum.
180    /// **Falsifies the contract on divergence** — this is the
181    /// stratum-upgrading policy that closes the §4 "Run=1 demo
182    /// fixture" caveat from audit-design.md.
183    DiffExec {
184        /// Maximum allowed absolute difference between corresponding
185        /// numerical outputs of the two emitters.
186        tolerance: f64,
187    },
188    /// Strict text-equality between PTX outputs. Useful for
189    /// regression-locking, NOT for falsification — different valid
190    /// PTX programs commonly produce identical execution results via
191    /// different instruction sequences.
192    Strict,
193}
194
195/// Status of a multi-emitter quorum vote, attached to an [`Artifact`]
196/// produced by a multi-emitter backend.
197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
198#[serde(tag = "kind", rename_all = "snake_case")]
199pub enum QuorumStatus {
200    /// Only one emitter fired (specialist missing for this shape, or
201    /// quorum policy is `PreferSpecialist` and specialist matched).
202    /// Runtime stratum vote count: 1.
203    Single { emitter: String },
204    /// Both emitters fired; outputs were combined under the policy.
205    /// Runtime stratum vote count: 2.
206    Multi {
207        emitters: Vec<String>,
208        diff_exec: Option<DiffExecResult>,
209    },
210}
211
212/// Result of a `DiffExec` quorum policy execution. Two PTX (or WGSL/
213/// SPIR-V) programs were run on test inputs; the engine compared
214/// their numerical outputs.
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216#[serde(tag = "kind", rename_all = "snake_case")]
217pub enum DiffExecResult {
218    /// Outputs matched within tolerance. Records the max absolute
219    /// difference observed for audit trail.
220    Match { max_abs_diff: f64 },
221    /// Outputs diverged beyond tolerance. **Contract violation** —
222    /// CI fails. Records the divergence for diagnosis.
223    Divergent { max_abs_diff: f64, tolerance: f64 },
224    /// Engine did not run (e.g., test hardware unavailable). Vote
225    /// downgrades from Runtime to a placeholder. The substrate
226    /// records this rather than silently dropping it.
227    NotRun { reason: String },
228}
229
230/// A single emitter entry from `compile_targets.via` in the YAML
231/// schema. Mirrors the structured-record form spec'd in
232/// `sub/layer5-multi-emitter-quorum.md` §"Contract YAML schema extension".
233///
234/// At v0.1.0 the YAML schema is still a flat `[String]`; this struct
235/// is the v0.2.0+ target representation `pv lint` will deserialize.
236#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
237pub struct ViaEntry {
238    /// Emitter name, e.g. "rustc_codegen_nvvm", "aprender-gpu".
239    pub emitter: String,
240    /// Role within the quorum.
241    pub role: EmitterRole,
242    /// Local crate that registers this emitter (for `role: general`).
243    #[serde(default, skip_serializing_if = "Option::is_none")]
244    pub crate_name: Option<String>,
245    /// Cross-repo binding target (for `role: specialist` cases where
246    /// the emitter lives in a different fleet repo, e.g. aprender).
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub cross_repo: Option<String>,
249    /// Optional shape filter — for specialists, identifies which
250    /// input shapes this emitter handles. Tied to a sub-contract
251    /// (e.g., `gemm_fp16_mma_64x128` matches aprender's
252    /// `C-COMPUTE-GEMM-FP16-MMA` contract).
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub shape_filter: Option<String>,
255}
256
257// ─── PMAT-263 / Section 29: TargetEmitter trait + MultiEmitterBackend ────
258//
259// Routing layer for multi-emitter backends. A [`MultiEmitterBackend`]
260// composes a general emitter (mandatory fallback) + an optional
261// specialist emitter under a [`QuorumPolicy`]. Single-emitter and
262// multi-emitter cases produce explicit `QuorumStatus` on the emitted
263// [`Artifact`].
264
265/// Plain text emitted by a single [`TargetEmitter`] before the
266/// multi-emitter routing decides what to put in the final [`Artifact`].
267/// Doesn't carry `QuorumStatus` (that's chosen by the wrapper).
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
269pub struct EmittedText {
270    pub primary: String,
271    #[serde(default, skip_serializing_if = "Vec::is_empty")]
272    pub citations: Vec<ContractId>,
273}
274
275/// Single-emitter trait — sub-trait of a multi-emitter backend.
276/// One [`TargetEmitter`] handles emission for one logical path
277/// (general vs specialist). Each [`MultiEmitterBackend`] wraps
278/// one mandatory general emitter and optionally one specialist.
279pub trait TargetEmitter: Send + Sync {
280    /// Human-readable emitter name (used in [`QuorumStatus`]).
281    fn name(&self) -> &str;
282
283    /// Attempt to emit for this input. Specialists return `None`
284    /// when their shape filter doesn't match — the wrapper then
285    /// uses only the general emitter. General emitters should
286    /// always return `Some(...)` for any contract-conforming input.
287    fn try_emit(
288        &self,
289        module: &Module,
290        config: &BackendConfig,
291    ) -> Option<Result<EmittedText, BackendError>>;
292}
293
294/// PMAT-486 (§30 Track 4): engine that executes two emitted programs on
295/// contract-fixture inputs and numerically compares the outputs within a
296/// tolerance — the Runtime-stratum half of the §29 quorum. The trait +
297/// hook land here (free CI); the real CUDA / Vulkan implementations
298/// (PMAT-488 / PMAT-490) run out-of-band on self-hosted GPU runners.
299///
300/// Error posture (per the §30 Track-4 review): with **no engine
301/// installed** the `DiffExec` policy records `NotRun { reason: no-engine }`
302/// (benign — free CI stays green). An **installed** engine that returns
303/// `Err` propagates a hard [`BackendError`] that fails the job — a broken
304/// GPU run must NOT masquerade as "not run".
305pub trait DiffExecEngine: Send + Sync {
306    /// Execute `general_text` and `specialist_text` on the contract's
307    /// fixture inputs and compare. `Ok(Match|Divergent)` records the
308    /// vote; `Err(msg)` is a hard failure (e.g. driver fault / launch
309    /// error) the caller turns into a `BackendError`.
310    fn execute_and_compare(
311        &self,
312        general_text: &str,
313        specialist_text: &str,
314        module: &Module,
315        config: &BackendConfig,
316        tolerance: f64,
317    ) -> Result<DiffExecResult, String>;
318}
319
320/// Multi-emitter backend wrapper. Composes a general emitter
321/// (mandatory) + an optional specialist under a [`QuorumPolicy`].
322/// Implements [`Backend`] so it slots into the existing dispatch
323/// without touching `TranspileSession`.
324pub struct MultiEmitterBackend {
325    /// Single target this multi-emitter backend serves
326    /// (e.g., [`Target::Ptx`]).
327    pub target: Target,
328    /// Mandatory general emitter. Must handle any
329    /// contract-conforming input as a fallback.
330    pub general: Box<dyn TargetEmitter>,
331    /// Optional specialist emitter. Returns `None` from `try_emit`
332    /// when its shape filter doesn't match the input.
333    pub specialist: Option<Box<dyn TargetEmitter>>,
334    /// How to combine outputs when both emitters fire.
335    pub quorum_policy: QuorumPolicy,
336    /// PMAT-486: optional `DiffExec` execution engine. `None` (the
337    /// default) records `NotRun { no-engine }` under `QuorumPolicy::
338    /// DiffExec`; `Some(engine)` runs the Runtime-stratum comparison.
339    pub diff_exec_engine: Option<std::sync::Arc<dyn DiffExecEngine>>,
340}
341
342impl MultiEmitterBackend {
343    pub fn new_single(target: Target, general: Box<dyn TargetEmitter>) -> Self {
344        Self {
345            target,
346            general,
347            specialist: None,
348            quorum_policy: QuorumPolicy::PreferSpecialist,
349            diff_exec_engine: None,
350        }
351    }
352
353    pub fn new_with_specialist(
354        target: Target,
355        general: Box<dyn TargetEmitter>,
356        specialist: Box<dyn TargetEmitter>,
357        quorum_policy: QuorumPolicy,
358    ) -> Self {
359        Self {
360            target,
361            general,
362            specialist: Some(specialist),
363            quorum_policy,
364            diff_exec_engine: None,
365        }
366    }
367
368    /// PMAT-486: install a `DiffExec` engine (builder style). The real
369    /// CUDA / Vulkan engines (PMAT-488 / PMAT-490) plug in here on the
370    /// self-hosted GPU runners; on free CI the engine stays `None`.
371    pub fn with_diff_exec_engine(mut self, engine: std::sync::Arc<dyn DiffExecEngine>) -> Self {
372        self.diff_exec_engine = Some(engine);
373        self
374    }
375}
376
377impl Backend for MultiEmitterBackend {
378    fn name(&self) -> &'static str {
379        // The wrapper name is generic; per-emitter names live in
380        // QuorumStatus on each emitted Artifact for audit recovery.
381        "multi-emitter"
382    }
383
384    fn targets(&self) -> &[Target] {
385        std::slice::from_ref(&self.target)
386    }
387
388    fn lower(&self, module: &Module, config: &BackendConfig) -> Result<Artifact, BackendError> {
389        let general_result = self.general.try_emit(module, config).ok_or_else(|| {
390            BackendError::Lower(format!(
391                "general emitter {} must always match contract-conforming input",
392                self.general.name()
393            ))
394        })??;
395
396        let specialist_result = self.specialist.as_ref().and_then(|s| {
397            s.try_emit(module, config)
398                .map(|r| (s.name().to_string(), r))
399        });
400
401        match specialist_result {
402            None => {
403                // Only general fired — Single-vote Runtime stratum.
404                Ok(Artifact {
405                    primary: general_result.primary,
406                    sidecars: Vec::new(),
407                    citations: general_result.citations,
408                    quorum_status: QuorumStatus::Single {
409                        emitter: self.general.name().to_string(),
410                    },
411                })
412            }
413            Some((specialist_name, specialist_emit)) => {
414                let specialist_text = specialist_emit?;
415                match &self.quorum_policy {
416                    QuorumPolicy::PreferSpecialist => {
417                        // Use specialist's output; general was emitted for
418                        // sanity but isn't reported as a vote.
419                        Ok(Artifact {
420                            primary: specialist_text.primary,
421                            sidecars: Vec::new(),
422                            citations: specialist_text.citations,
423                            quorum_status: QuorumStatus::Single {
424                                emitter: specialist_name,
425                            },
426                        })
427                    }
428                    QuorumPolicy::Strict => {
429                        // Text-equality check.
430                        let diff_exec = if general_result.primary == specialist_text.primary {
431                            Some(DiffExecResult::Match { max_abs_diff: 0.0 })
432                        } else {
433                            Some(DiffExecResult::Divergent {
434                                max_abs_diff: f64::INFINITY,
435                                tolerance: 0.0,
436                            })
437                        };
438                        Ok(Artifact {
439                            primary: general_result.primary.clone(),
440                            sidecars: vec![(
441                                "specialist_emission".to_string(),
442                                specialist_text.primary.into_bytes(),
443                            )],
444                            citations: general_result.citations,
445                            quorum_status: QuorumStatus::Multi {
446                                emitters: vec![self.general.name().to_string(), specialist_name],
447                                diff_exec,
448                            },
449                        })
450                    }
451                    QuorumPolicy::DiffExec { tolerance } => {
452                        // PMAT-486: run the installed engine, or record
453                        // NotRun{no-engine} when none is installed (free
454                        // CI). An installed engine that errors propagates
455                        // a hard BackendError — a broken GPU run must NOT
456                        // masquerade as "not run".
457                        let diff_exec = match &self.diff_exec_engine {
458                            Some(engine) => engine
459                                .execute_and_compare(
460                                    &general_result.primary,
461                                    &specialist_text.primary,
462                                    module,
463                                    config,
464                                    *tolerance,
465                                )
466                                .map_err(|e| {
467                                    BackendError::Lower(format!(
468                                        "DiffExec engine for {:?} failed: {e}",
469                                        self.target
470                                    ))
471                                })?,
472                            None => DiffExecResult::NotRun {
473                                reason: format!(
474                                    "no DiffExec engine installed (tolerance was {tolerance})"
475                                ),
476                            },
477                        };
478                        Ok(Artifact {
479                            primary: general_result.primary.clone(),
480                            sidecars: vec![(
481                                "specialist_emission".to_string(),
482                                specialist_text.primary.into_bytes(),
483                            )],
484                            citations: general_result.citations,
485                            quorum_status: QuorumStatus::Multi {
486                                emitters: vec![self.general.name().to_string(), specialist_name],
487                                diff_exec: Some(diff_exec),
488                            },
489                        })
490                    }
491                }
492            }
493        }
494    }
495}
496
497#[cfg(test)]
498mod quorum_scaffolding_tests {
499    use super::*;
500
501    #[test]
502    fn emitter_role_serde_round_trip() {
503        let general = EmitterRole::General;
504        let s = serde_json::to_string(&general).unwrap();
505        assert_eq!(s, "\"general\"");
506        let back: EmitterRole = serde_json::from_str(&s).unwrap();
507        assert_eq!(back, general);
508
509        let specialist = EmitterRole::Specialist;
510        let s = serde_json::to_string(&specialist).unwrap();
511        assert_eq!(s, "\"specialist\"");
512    }
513
514    #[test]
515    fn quorum_policy_diff_exec_carries_tolerance() {
516        let policy = QuorumPolicy::DiffExec { tolerance: 1.0e-3 };
517        let s = serde_json::to_string(&policy).unwrap();
518        assert!(s.contains("diff_exec"));
519        assert!(s.contains("0.001"));
520        let back: QuorumPolicy = serde_json::from_str(&s).unwrap();
521        assert_eq!(back, policy);
522    }
523
524    #[test]
525    fn quorum_status_multi_records_emitters_and_diff() {
526        let status = QuorumStatus::Multi {
527            emitters: vec!["rustc_codegen_nvvm".into(), "aprender-gpu".into()],
528            diff_exec: Some(DiffExecResult::Match {
529                max_abs_diff: 1.3e-4,
530            }),
531        };
532        let s = serde_json::to_string(&status).unwrap();
533        assert!(s.contains("multi"));
534        assert!(s.contains("rustc_codegen_nvvm"));
535        assert!(s.contains("aprender-gpu"));
536    }
537
538    #[test]
539    fn diff_exec_divergent_carries_both_diff_and_tolerance() {
540        let r = DiffExecResult::Divergent {
541            max_abs_diff: 0.5,
542            tolerance: 0.001,
543        };
544        let s = serde_json::to_string(&r).unwrap();
545        let back: DiffExecResult = serde_json::from_str(&s).unwrap();
546        assert_eq!(back, r);
547    }
548
549    #[test]
550    fn via_entry_general_has_no_specialist_fields() {
551        let v = ViaEntry {
552            emitter: "rustc_codegen_nvvm".into(),
553            role: EmitterRole::General,
554            crate_name: Some("xpile-ptx-codegen".into()),
555            cross_repo: None,
556            shape_filter: None,
557        };
558        let s = serde_json::to_string(&v).unwrap();
559        // Optional None fields skipped from output.
560        assert!(!s.contains("cross_repo"));
561        assert!(!s.contains("shape_filter"));
562        assert!(s.contains("general"));
563    }
564
565    #[test]
566    fn via_entry_specialist_carries_cross_repo_and_shape_filter() {
567        let v = ViaEntry {
568            emitter: "aprender-gpu".into(),
569            role: EmitterRole::Specialist,
570            crate_name: None,
571            cross_repo: Some("aprender".into()),
572            shape_filter: Some("gemm_fp16_mma_64x128".into()),
573        };
574        let s = serde_json::to_string(&v).unwrap();
575        assert!(s.contains("specialist"));
576        assert!(s.contains("aprender"));
577        assert!(s.contains("gemm_fp16_mma_64x128"));
578    }
579
580    /// PMAT-262: Artifact carries QuorumStatus; default deserialization
581    /// gracefully populates Single { emitter: "unknown" } for older JSON
582    /// payloads that predate the field.
583    #[test]
584    fn artifact_quorum_status_defaults_for_older_payloads() {
585        let legacy_json = r#"{"primary":"// test","sidecars":[],"citations":[]}"#;
586        let a: Artifact = serde_json::from_str(legacy_json).unwrap();
587        assert_eq!(
588            a.quorum_status,
589            QuorumStatus::Single {
590                emitter: "unknown".to_string()
591            }
592        );
593    }
594
595    /// PMAT-262: Artifact round-trips QuorumStatus::Single produced by
596    /// every single-emitter backend at v0.1.0.
597    #[test]
598    fn artifact_quorum_status_single_round_trips() {
599        let a = Artifact {
600            primary: "// test".into(),
601            sidecars: Vec::new(),
602            citations: Vec::new(),
603            quorum_status: QuorumStatus::Single {
604                emitter: "xpile-rust-codegen".to_string(),
605            },
606        };
607        let s = serde_json::to_string(&a).unwrap();
608        let back: Artifact = serde_json::from_str(&s).unwrap();
609        assert_eq!(back, a);
610    }
611
612    // ─── PMAT-263: MultiEmitterBackend routing tests with mock emitters ─
613
614    /// Mock emitter returning a fixed primary string and always matching
615    /// (use for `general` role). Cloneable to construct two copies for
616    /// match cases.
617    struct MockGeneral {
618        name: &'static str,
619        body: String,
620    }
621    impl TargetEmitter for MockGeneral {
622        fn name(&self) -> &str {
623            self.name
624        }
625        fn try_emit(
626            &self,
627            _module: &Module,
628            _config: &BackendConfig,
629        ) -> Option<Result<EmittedText, BackendError>> {
630            Some(Ok(EmittedText {
631                primary: self.body.clone(),
632                citations: Vec::new(),
633            }))
634        }
635    }
636
637    /// Mock specialist emitter — matches conditionally and returns a
638    /// configurable body.
639    struct MockSpecialist {
640        name: &'static str,
641        matches: bool,
642        body: String,
643    }
644    impl TargetEmitter for MockSpecialist {
645        fn name(&self) -> &str {
646            self.name
647        }
648        fn try_emit(
649            &self,
650            _module: &Module,
651            _config: &BackendConfig,
652        ) -> Option<Result<EmittedText, BackendError>> {
653            if self.matches {
654                Some(Ok(EmittedText {
655                    primary: self.body.clone(),
656                    citations: Vec::new(),
657                }))
658            } else {
659                None
660            }
661        }
662    }
663
664    fn dummy_module() -> Module {
665        Module {
666            name: "test".into(),
667            source_lang: xpile_meta_hir::SourceLang::Rust,
668            items: Vec::new(),
669            ffi_boundaries: Vec::new(),
670        }
671    }
672
673    fn dummy_config() -> BackendConfig {
674        BackendConfig {
675            target: Target::Ptx,
676            profile: Profile::RustOut,
677            hardware: None,
678        }
679    }
680
681    #[test]
682    fn multi_emitter_specialist_missing_falls_back_to_general() {
683        let backend = MultiEmitterBackend::new_single(
684            Target::Ptx,
685            Box::new(MockGeneral {
686                name: "general",
687                body: "general output".into(),
688            }),
689        );
690        let artifact = backend.lower(&dummy_module(), &dummy_config()).unwrap();
691        assert_eq!(artifact.primary, "general output");
692        assert_eq!(
693            artifact.quorum_status,
694            QuorumStatus::Single {
695                emitter: "general".to_string()
696            }
697        );
698    }
699
700    #[test]
701    fn multi_emitter_specialist_unmatched_falls_back_to_general() {
702        let backend = MultiEmitterBackend::new_with_specialist(
703            Target::Ptx,
704            Box::new(MockGeneral {
705                name: "general",
706                body: "general output".into(),
707            }),
708            Box::new(MockSpecialist {
709                name: "specialist",
710                matches: false,
711                body: "specialist output".into(),
712            }),
713            QuorumPolicy::DiffExec { tolerance: 1e-3 },
714        );
715        let artifact = backend.lower(&dummy_module(), &dummy_config()).unwrap();
716        assert_eq!(artifact.primary, "general output");
717        // Specialist returned None → Single vote.
718        assert_eq!(
719            artifact.quorum_status,
720            QuorumStatus::Single {
721                emitter: "general".to_string()
722            }
723        );
724    }
725
726    #[test]
727    fn multi_emitter_prefer_specialist_uses_specialist_output() {
728        let backend = MultiEmitterBackend::new_with_specialist(
729            Target::Ptx,
730            Box::new(MockGeneral {
731                name: "general",
732                body: "general".into(),
733            }),
734            Box::new(MockSpecialist {
735                name: "specialist",
736                matches: true,
737                body: "specialist tuned".into(),
738            }),
739            QuorumPolicy::PreferSpecialist,
740        );
741        let artifact = backend.lower(&dummy_module(), &dummy_config()).unwrap();
742        assert_eq!(artifact.primary, "specialist tuned");
743        assert_eq!(
744            artifact.quorum_status,
745            QuorumStatus::Single {
746                emitter: "specialist".to_string()
747            }
748        );
749    }
750
751    #[test]
752    fn multi_emitter_strict_match_records_zero_diff() {
753        let backend = MultiEmitterBackend::new_with_specialist(
754            Target::Ptx,
755            Box::new(MockGeneral {
756                name: "general",
757                body: "same output".into(),
758            }),
759            Box::new(MockSpecialist {
760                name: "specialist",
761                matches: true,
762                body: "same output".into(),
763            }),
764            QuorumPolicy::Strict,
765        );
766        let artifact = backend.lower(&dummy_module(), &dummy_config()).unwrap();
767        match artifact.quorum_status {
768            QuorumStatus::Multi {
769                emitters,
770                diff_exec,
771            } => {
772                assert_eq!(emitters, vec!["general", "specialist"]);
773                assert_eq!(diff_exec, Some(DiffExecResult::Match { max_abs_diff: 0.0 }));
774            }
775            _ => panic!("expected Multi quorum status"),
776        }
777        // Specialist's output recorded as sidecar for audit trail.
778        assert_eq!(artifact.sidecars.len(), 1);
779        assert_eq!(artifact.sidecars[0].0, "specialist_emission");
780    }
781
782    #[test]
783    fn multi_emitter_strict_divergence_records_infinity() {
784        let backend = MultiEmitterBackend::new_with_specialist(
785            Target::Ptx,
786            Box::new(MockGeneral {
787                name: "general",
788                body: "general output".into(),
789            }),
790            Box::new(MockSpecialist {
791                name: "specialist",
792                matches: true,
793                body: "different output".into(),
794            }),
795            QuorumPolicy::Strict,
796        );
797        let artifact = backend.lower(&dummy_module(), &dummy_config()).unwrap();
798        match artifact.quorum_status {
799            QuorumStatus::Multi { diff_exec, .. } => {
800                assert!(matches!(diff_exec, Some(DiffExecResult::Divergent { .. })));
801            }
802            _ => panic!("expected Multi quorum status"),
803        }
804    }
805
806    #[test]
807    fn multi_emitter_diff_exec_records_not_run_until_engine_plugged_in() {
808        let backend = MultiEmitterBackend::new_with_specialist(
809            Target::Ptx,
810            Box::new(MockGeneral {
811                name: "rustc_codegen_nvvm",
812                body: "ptx general".into(),
813            }),
814            Box::new(MockSpecialist {
815                name: "aprender-gpu",
816                matches: true,
817                body: "ptx specialist".into(),
818            }),
819            QuorumPolicy::DiffExec { tolerance: 1e-3 },
820        );
821        let artifact = backend.lower(&dummy_module(), &dummy_config()).unwrap();
822        match artifact.quorum_status {
823            QuorumStatus::Multi {
824                emitters,
825                diff_exec,
826            } => {
827                assert_eq!(emitters, vec!["rustc_codegen_nvvm", "aprender-gpu"]);
828                // DiffExec engine isn't plugged in yet — should record NotRun.
829                assert!(matches!(diff_exec, Some(DiffExecResult::NotRun { .. })));
830            }
831            _ => panic!("expected Multi quorum status"),
832        }
833    }
834
835    // ─── PMAT-266: Adversarial invariants for MultiEmitterBackend ───
836    //
837    // These tests pin down security-relevant contract behavior that the
838    // PMAT-263 happy-path tests don't cover: citation provenance, error
839    // propagation, hidden-divergence documentation, and observability of
840    // the `NotRun` reason. They guard against silent regressions in the
841    // routing layer that would weaken the Section 29 oracle.
842
843    /// Mock emitter with configurable citations — used to verify which
844    /// emitter's citations end up in the final Artifact.
845    struct MockGeneralWithCitations {
846        body: String,
847        citations: Vec<ContractId>,
848    }
849    impl TargetEmitter for MockGeneralWithCitations {
850        fn name(&self) -> &str {
851            "general-with-cites"
852        }
853        fn try_emit(
854            &self,
855            _module: &Module,
856            _config: &BackendConfig,
857        ) -> Option<Result<EmittedText, BackendError>> {
858            Some(Ok(EmittedText {
859                primary: self.body.clone(),
860                citations: self.citations.clone(),
861            }))
862        }
863    }
864
865    /// Mock specialist that always matches and carries configurable
866    /// citations distinct from `MockGeneralWithCitations`.
867    struct MockSpecialistWithCitations {
868        body: String,
869        citations: Vec<ContractId>,
870    }
871    impl TargetEmitter for MockSpecialistWithCitations {
872        fn name(&self) -> &str {
873            "specialist-with-cites"
874        }
875        fn try_emit(
876            &self,
877            _module: &Module,
878            _config: &BackendConfig,
879        ) -> Option<Result<EmittedText, BackendError>> {
880            Some(Ok(EmittedText {
881                primary: self.body.clone(),
882                citations: self.citations.clone(),
883            }))
884        }
885    }
886
887    /// Mock emitter that always fails — used to verify error
888    /// propagation from each role.
889    struct MockFailingEmitter {
890        name: &'static str,
891        err: String,
892    }
893    impl TargetEmitter for MockFailingEmitter {
894        fn name(&self) -> &str {
895            self.name
896        }
897        fn try_emit(
898            &self,
899            _module: &Module,
900            _config: &BackendConfig,
901        ) -> Option<Result<EmittedText, BackendError>> {
902            Some(Err(BackendError::Lower(self.err.clone())))
903        }
904    }
905
906    /// Mock emitter that returns `None` — for `general`, this is a
907    /// contract violation (general MUST match contract-conforming
908    /// input); the wrapper must surface it as a hard `BackendError`.
909    struct MockNoneEmitter;
910    impl TargetEmitter for MockNoneEmitter {
911        fn name(&self) -> &str {
912            "always-none"
913        }
914        fn try_emit(
915            &self,
916            _module: &Module,
917            _config: &BackendConfig,
918        ) -> Option<Result<EmittedText, BackendError>> {
919            None
920        }
921    }
922
923    #[test]
924    fn strict_divergence_preserves_general_citations_not_specialist() {
925        // Security invariant: under `Strict`, citations come from
926        // `general`. The proof lane relies on this — citations identify
927        // which contracts the artifact was authored against. A
928        // specialist that disagrees with general must not be able to
929        // silently swap its own citations into the audit trail.
930        let backend = MultiEmitterBackend::new_with_specialist(
931            Target::Ptx,
932            Box::new(MockGeneralWithCitations {
933                body: "general output".into(),
934                citations: vec![ContractId::new("C-GENERAL-CITED")],
935            }),
936            Box::new(MockSpecialistWithCitations {
937                body: "different output".into(),
938                citations: vec![ContractId::new("C-SPECIALIST-CITED")],
939            }),
940            QuorumPolicy::Strict,
941        );
942        let artifact = backend.lower(&dummy_module(), &dummy_config()).unwrap();
943        assert_eq!(artifact.citations.len(), 1);
944        assert_eq!(artifact.citations[0].as_str(), "C-GENERAL-CITED");
945        // Specialist's body is still recoverable from the sidecar even
946        // though its citations are dropped.
947        assert_eq!(artifact.sidecars.len(), 1);
948        assert_eq!(
949            artifact.sidecars[0].1,
950            b"different output".to_vec(),
951            "specialist body should be preserved in sidecar"
952        );
953    }
954
955    #[test]
956    fn prefer_specialist_hides_divergence_by_design() {
957        // Documented trade-off: `PreferSpecialist` is the
958        // single-vote-runtime stratum — it intentionally does NOT
959        // compare general vs specialist. Use `Strict` or `DiffExec`
960        // when divergence detection matters. This test pins down the
961        // behavior so a future "helpful" refactor can't accidentally
962        // turn this into a quiet divergence detector.
963        let backend = MultiEmitterBackend::new_with_specialist(
964            Target::Ptx,
965            Box::new(MockGeneralWithCitations {
966                body: "general thinks the answer is 42".into(),
967                citations: vec![ContractId::new("C-GENERAL")],
968            }),
969            Box::new(MockSpecialistWithCitations {
970                body: "specialist thinks the answer is 99".into(),
971                citations: vec![ContractId::new("C-SPECIALIST")],
972            }),
973            QuorumPolicy::PreferSpecialist,
974        );
975        let artifact = backend.lower(&dummy_module(), &dummy_config()).unwrap();
976        // Specialist's body wins; specialist's citations win;
977        // QuorumStatus reports Single (no divergence captured).
978        assert!(artifact.primary.contains("99"));
979        assert_eq!(artifact.citations[0].as_str(), "C-SPECIALIST");
980        match artifact.quorum_status {
981            QuorumStatus::Single { emitter } => {
982                assert_eq!(emitter, "specialist-with-cites");
983            }
984            other => panic!("expected Single quorum status, got {other:?}"),
985        }
986        // No sidecar — general's emission isn't even captured.
987        assert!(artifact.sidecars.is_empty());
988    }
989
990    #[test]
991    fn general_emitter_failure_propagates() {
992        // If `general` returns Some(Err(...)), the wrapper must
993        // propagate the error — never silently fall through to
994        // specialist. General is the mandatory fallback; its failure
995        // is the whole backend's failure.
996        let backend = MultiEmitterBackend::new_with_specialist(
997            Target::Ptx,
998            Box::new(MockFailingEmitter {
999                name: "general-broken",
1000                err: "general blew up".into(),
1001            }),
1002            Box::new(MockSpecialist {
1003                name: "specialist",
1004                matches: true,
1005                body: "specialist output".into(),
1006            }),
1007            QuorumPolicy::PreferSpecialist,
1008        );
1009        let err = backend.lower(&dummy_module(), &dummy_config()).unwrap_err();
1010        match err {
1011            BackendError::Lower(msg) => assert!(msg.contains("general blew up")),
1012            other => panic!("expected Lower error, got {other:?}"),
1013        }
1014    }
1015
1016    #[test]
1017    fn specialist_emitter_failure_propagates_when_matched() {
1018        // If `specialist` matches and then errors, propagate. This is
1019        // a real partial-failure mode for shape-tuned emitters
1020        // (matched on shape but failed during lowering).
1021        let backend = MultiEmitterBackend::new_with_specialist(
1022            Target::Ptx,
1023            Box::new(MockGeneral {
1024                name: "general",
1025                body: "general output".into(),
1026            }),
1027            Box::new(MockFailingEmitter {
1028                name: "specialist-broken",
1029                err: "specialist blew up after matching".into(),
1030            }),
1031            QuorumPolicy::Strict,
1032        );
1033        let err = backend.lower(&dummy_module(), &dummy_config()).unwrap_err();
1034        match err {
1035            BackendError::Lower(msg) => {
1036                assert!(msg.contains("specialist blew up after matching"))
1037            }
1038            other => panic!("expected Lower error, got {other:?}"),
1039        }
1040    }
1041
1042    #[test]
1043    fn general_returning_none_is_a_hard_contract_violation() {
1044        // `general` returning `None` from `try_emit` means it refused
1045        // to handle contract-conforming input — that's a hard error.
1046        // (Specialists are allowed to return None; general isn't.)
1047        let backend = MultiEmitterBackend::new_single(Target::Ptx, Box::new(MockNoneEmitter));
1048        let err = backend.lower(&dummy_module(), &dummy_config()).unwrap_err();
1049        match err {
1050            BackendError::Lower(msg) => {
1051                assert!(
1052                    msg.contains("always-none"),
1053                    "error should name the offending emitter; got: {msg}"
1054                );
1055                assert!(msg.contains("must always match"));
1056            }
1057            other => panic!("expected Lower error, got {other:?}"),
1058        }
1059    }
1060
1061    #[test]
1062    fn diff_exec_not_run_reason_records_tolerance_for_observability() {
1063        // The `NotRun` reason is the user-facing breadcrumb pointing
1064        // at "DiffExec engine not yet wired" — and it must carry the
1065        // configured tolerance so debug output is actionable.
1066        let backend = MultiEmitterBackend::new_with_specialist(
1067            Target::Ptx,
1068            Box::new(MockGeneral {
1069                name: "general",
1070                body: "g".into(),
1071            }),
1072            Box::new(MockSpecialist {
1073                name: "specialist",
1074                matches: true,
1075                body: "s".into(),
1076            }),
1077            QuorumPolicy::DiffExec { tolerance: 2.5e-4 },
1078        );
1079        let artifact = backend.lower(&dummy_module(), &dummy_config()).unwrap();
1080        match artifact.quorum_status {
1081            QuorumStatus::Multi {
1082                diff_exec: Some(DiffExecResult::NotRun { reason }),
1083                ..
1084            } => {
1085                assert!(
1086                    reason.contains("0.00025")
1087                        || reason.contains("2.5e-4")
1088                        || reason.contains("0.000250"),
1089                    "tolerance should appear in NotRun reason; got: {reason}"
1090                );
1091            }
1092            other => panic!("expected Multi NotRun status, got {other:?}"),
1093        }
1094    }
1095
1096    #[test]
1097    fn diff_exec_does_not_short_circuit_on_text_equality() {
1098        // Architectural invariant: even when general and specialist
1099        // emit byte-identical text, `DiffExec` policy must still
1100        // record `NotRun` (because the real engine compares numerical
1101        // outputs after execution, not source text). A future
1102        // optimization that says "skip diff if text matches" would
1103        // break this invariant — the engine's job is to check the
1104        // RUNTIME behavior, and identical source could still produce
1105        // divergent runtime values on different hardware.
1106        let backend = MultiEmitterBackend::new_with_specialist(
1107            Target::Ptx,
1108            Box::new(MockGeneral {
1109                name: "general",
1110                body: "byte identical".into(),
1111            }),
1112            Box::new(MockSpecialist {
1113                name: "specialist",
1114                matches: true,
1115                body: "byte identical".into(),
1116            }),
1117            QuorumPolicy::DiffExec { tolerance: 1e-6 },
1118        );
1119        let artifact = backend.lower(&dummy_module(), &dummy_config()).unwrap();
1120        match artifact.quorum_status {
1121            QuorumStatus::Multi { diff_exec, .. } => {
1122                assert!(
1123                    matches!(diff_exec, Some(DiffExecResult::NotRun { .. })),
1124                    "DiffExec policy must NOT short-circuit on text equality \
1125                     — engine compares runtime values, not source text"
1126                );
1127            }
1128            other => panic!("expected Multi quorum status, got {other:?}"),
1129        }
1130    }
1131
1132    // ─── PMAT-486: DiffExecEngine trait + hook ──────────────────────
1133
1134    /// Stub engine returning a fixed result (or a hard error).
1135    struct StubEngine {
1136        result: Result<DiffExecResult, String>,
1137    }
1138    impl DiffExecEngine for StubEngine {
1139        fn execute_and_compare(
1140            &self,
1141            _g: &str,
1142            _s: &str,
1143            _m: &Module,
1144            _c: &BackendConfig,
1145            _tol: f64,
1146        ) -> Result<DiffExecResult, String> {
1147            self.result.clone()
1148        }
1149    }
1150
1151    fn diff_exec_backend() -> MultiEmitterBackend {
1152        MultiEmitterBackend::new_with_specialist(
1153            Target::Ptx,
1154            Box::new(MockGeneral {
1155                name: "general",
1156                body: "g".into(),
1157            }),
1158            Box::new(MockSpecialist {
1159                name: "specialist",
1160                matches: true,
1161                body: "s".into(),
1162            }),
1163            QuorumPolicy::DiffExec { tolerance: 1e-6 },
1164        )
1165    }
1166
1167    /// PMAT-486: an installed engine's `Ok(Match)` becomes the recorded
1168    /// Runtime vote (replacing NotRun).
1169    #[test]
1170    fn diff_exec_engine_records_match() {
1171        let backend = diff_exec_backend().with_diff_exec_engine(std::sync::Arc::new(StubEngine {
1172            result: Ok(DiffExecResult::Match { max_abs_diff: 0.0 }),
1173        }));
1174        let artifact = backend.lower(&dummy_module(), &dummy_config()).unwrap();
1175        match artifact.quorum_status {
1176            QuorumStatus::Multi {
1177                diff_exec: Some(DiffExecResult::Match { .. }),
1178                ..
1179            } => {}
1180            other => panic!("expected Multi Match, got {other:?}"),
1181        }
1182    }
1183
1184    /// PMAT-486: an installed engine that errors propagates a hard
1185    /// `BackendError` — it must NOT be swallowed into `NotRun`.
1186    #[test]
1187    fn diff_exec_engine_error_is_a_hard_failure() {
1188        let backend = diff_exec_backend().with_diff_exec_engine(std::sync::Arc::new(StubEngine {
1189            result: Err("driver fault: CUDA_ERROR_LAUNCH_FAILED".into()),
1190        }));
1191        let err = backend
1192            .lower(&dummy_module(), &dummy_config())
1193            .expect_err("engine error must surface as a hard BackendError");
1194        assert!(matches!(err, BackendError::Lower(_)));
1195    }
1196
1197    /// PMAT-486: with no engine installed, the policy still records the
1198    /// benign `NotRun { no-engine }` (free CI stays green).
1199    #[test]
1200    fn diff_exec_no_engine_records_not_run() {
1201        let backend = diff_exec_backend();
1202        let artifact = backend.lower(&dummy_module(), &dummy_config()).unwrap();
1203        match artifact.quorum_status {
1204            QuorumStatus::Multi {
1205                diff_exec: Some(DiffExecResult::NotRun { reason }),
1206                ..
1207            } => assert!(reason.contains("no DiffExec engine"), "got: {reason}"),
1208            other => panic!("expected Multi NotRun, got {other:?}"),
1209        }
1210    }
1211}