Skip to main content

silicera_runtime/
lib.rs

1//! # silicera-runtime
2//!
3//! Lightweight HNEP load and cheap decision-tree dispatch.
4//!
5//! This crate intentionally does **not** depend on `silicera-lab`. Specialized
6//! binaries embed or load a profile and select variants without re-running
7//! tournaments.
8//!
9//! On fingerprint mismatch the runtime reports `PROFILE MISMATCH` and falls
10//! back to the baseline variant unless `--strict-machine` / strict API is used.
11
12#![warn(missing_docs)]
13#![deny(unsafe_op_in_unsafe_fn)]
14
15use std::path::Path;
16
17use serde::{Deserialize, Serialize};
18use silicera::brand::{BrandInfo, NAME};
19use silicera::fingerprint::Fingerprint;
20use silicera::hardware::{detect_hardware, HardwareInfo};
21use silicera::hnep::HnepProfile;
22use silicera::specialize::DecisionTree;
23use silicera::{Result, SiliceraError};
24
25#[cfg(feature = "c-abi")]
26pub mod ffi;
27
28/// Embedder-facing about / identity block (version, phase, brand, funding).
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct RuntimeInfo {
31    /// Product name.
32    pub name: String,
33    /// Runtime crate version (same workspace semver).
34    pub version: String,
35    /// Phase marker.
36    pub phase: String,
37    /// Brand line.
38    pub brand_line: String,
39    /// SPDX license.
40    pub license: String,
41    /// Homepage.
42    pub homepage: String,
43    /// Repository URL.
44    pub repository: String,
45    /// Funding / thanks.dev URL.
46    pub funding_url: String,
47    /// Affiliation disclaimer.
48    pub affiliation: String,
49}
50
51impl RuntimeInfo {
52    /// Capture current runtime identity from the shared brand module.
53    pub fn current() -> Self {
54        let b = BrandInfo::current();
55        Self {
56            name: b.name.into(),
57            version: b.version.into(),
58            phase: b.phase.into(),
59            brand_line: b.brand_line.into(),
60            license: b.license.into(),
61            homepage: b.homepage.into(),
62            repository: b.repository.into(),
63            funding_url: b.funding_url.into(),
64            affiliation: b.affiliation.into(),
65        }
66    }
67
68    /// Pretty JSON for tooling.
69    pub fn to_json_pretty(&self) -> Result<String> {
70        Ok(serde_json::to_string_pretty(self)?)
71    }
72}
73
74/// Convenience: brand/version/phase for embedders.
75pub fn runtime_about() -> RuntimeInfo {
76    RuntimeInfo::current()
77}
78
79/// Runtime policy when the loaded profile does not match the host.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81pub enum MismatchPolicy {
82    /// Print/return mismatch and use baseline fallback.
83    FallbackBaseline,
84    /// Hard error — refuse to dispatch specialized variants.
85    StrictMachine,
86}
87
88impl Default for MismatchPolicy {
89    fn default() -> Self {
90        Self::FallbackBaseline
91    }
92}
93
94/// Outcome of loading a profile against the current machine.
95#[derive(Debug, Clone)]
96pub struct LoadedProfile {
97    /// Parsed HNEP.
98    pub profile: HnepProfile,
99    /// Host hardware at load time.
100    pub host: HardwareInfo,
101    /// Whether fingerprint matched exactly.
102    pub exact_match: bool,
103    /// Whether silicon class matched (vendor/microarch/family/model).
104    pub class_match: bool,
105    /// Active mismatch policy.
106    pub policy: MismatchPolicy,
107    /// True if dispatch should use baseline only.
108    pub force_baseline: bool,
109    /// Operator-facing status line.
110    pub status: String,
111}
112
113impl LoadedProfile {
114    /// Load from path and compare to live hardware.
115    pub fn load(path: &Path, policy: MismatchPolicy) -> Result<Self> {
116        let profile = HnepProfile::read_from(path)?;
117        let host = detect_hardware()?;
118        Self::from_parts(profile, host, policy)
119    }
120
121    /// Construct from already-loaded pieces (tests / inject mock host).
122    pub fn from_parts(
123        profile: HnepProfile,
124        host: HardwareInfo,
125        policy: MismatchPolicy,
126    ) -> Result<Self> {
127        // Callers can construct profiles directly; do not trust their digest.
128        let profile = HnepProfile::parse_str(&serde_json::to_string(&profile)?)?;
129        let host_fp = match &host.fingerprint {
130            Some(fp) => fp.clone(),
131            None => {
132                if policy == MismatchPolicy::StrictMachine {
133                    return Err(SiliceraError::ProfileMismatch(format!(
134                        "strict-machine ({NAME}): host unsupported ({})",
135                        host.support.message()
136                    )));
137                }
138                return Ok(Self {
139                    status: format!(
140                        "PROFILE MISMATCH ({NAME}): host unsupported ({})",
141                        host.support.message()
142                    ),
143                    profile,
144                    host,
145                    exact_match: false,
146                    class_match: false,
147                    policy,
148                    force_baseline: true,
149                });
150            }
151        };
152
153        let profile_fp = Fingerprint::parse(&profile.header.fingerprint)?;
154        let exact_match = host_fp.exact_match(&profile_fp);
155        let class_match = host_fp.same_silicon_class(&profile_fp);
156
157        let (force_baseline, status) = if exact_match {
158            (false, format!("profile match: exact fingerprint ({NAME})"))
159        } else if class_match {
160            match policy {
161                MismatchPolicy::FallbackBaseline => (
162                    true,
163                    format!(
164                        "PROFILE MISMATCH ({NAME}): topology/cache hash differs (class OK); falling back to baseline\n  profile: {}\n  host:    {}",
165                        profile_fp, host_fp
166                    ),
167                ),
168                MismatchPolicy::StrictMachine => {
169                    return Err(SiliceraError::ProfileMismatch(format!(
170                        "strict-machine ({NAME}): fingerprint mismatch\n  profile: {profile_fp}\n  host:    {host_fp}"
171                    )));
172                }
173            }
174        } else {
175            match policy {
176                MismatchPolicy::FallbackBaseline => (
177                    true,
178                    format!(
179                        "PROFILE MISMATCH ({NAME}): silicon class differs; falling back to baseline\n  profile: {}\n  host:    {}",
180                        profile_fp, host_fp
181                    ),
182                ),
183                MismatchPolicy::StrictMachine => {
184                    return Err(SiliceraError::ProfileMismatch(format!(
185                        "strict-machine ({NAME}): silicon class mismatch\n  profile: {profile_fp}\n  host:    {host_fp}"
186                    )));
187                }
188            }
189        };
190
191        Ok(Self {
192            profile,
193            host,
194            exact_match,
195            class_match,
196            policy,
197            force_baseline,
198            status,
199        })
200    }
201
202    /// Select variant for a workload by name (or baseline if forced).
203    pub fn select_workload(&self, workload: &str) -> &str {
204        if self.force_baseline {
205            return "baseline";
206        }
207        self.profile
208            .workloads
209            .iter()
210            .find(|w| w.name == workload)
211            .map(|w| w.winner.as_str())
212            .unwrap_or("baseline")
213    }
214
215    /// Evaluate size-dependent tree, or baseline on mismatch / missing tree.
216    pub fn select_size(&self, size_bytes: u64) -> &str {
217        if self.force_baseline {
218            return "baseline";
219        }
220        match &self.profile.decision_tree {
221            Some(tree) => tree.evaluate(size_bytes),
222            None => "baseline",
223        }
224    }
225
226    /// Access decision tree when present.
227    pub fn decision_tree(&self) -> Option<&DecisionTree> {
228        self.profile.decision_tree.as_ref()
229    }
230
231    /// Select a workload only when its winner is supported by the application
232    /// and meets its confidence floor. Always provide a real baseline handler.
233    /// The available IDs must already be filtered for the host's ISA support.
234    pub fn guarded_workload<'a>(
235        &'a self,
236        workload: &str,
237        available: &[&str],
238        minimum: silicera::hnep::Confidence,
239    ) -> DispatchDecision<'a> {
240        if self.force_baseline {
241            return DispatchDecision::baseline(DispatchReason::MachineMismatch);
242        }
243        let Some(entry) = self.profile.workloads.iter().find(|w| w.name == workload) else {
244            return DispatchDecision::baseline(DispatchReason::MissingWorkload);
245        };
246        if entry.winner == "baseline" {
247            return DispatchDecision::baseline(DispatchReason::ProfileBaseline);
248        }
249        if entry.confidence.rank() < minimum.rank() {
250            return DispatchDecision::baseline(DispatchReason::InsufficientConfidence);
251        }
252        if !available.contains(&entry.winner.as_str()) {
253            return DispatchDecision::baseline(DispatchReason::UnavailableVariant);
254        }
255        DispatchDecision {
256            variant: &entry.winner,
257            reason: DispatchReason::Selected,
258        }
259    }
260
261    /// Guard size dispatch against variant IDs absent from the host-compatible
262    /// application registry. This does not infer confidence from tree leaves.
263    pub fn guarded_size<'a>(&'a self, bytes: u64, available: &[&str]) -> DispatchDecision<'a> {
264        if self.force_baseline {
265            return DispatchDecision::baseline(DispatchReason::MachineMismatch);
266        }
267        let Some(tree) = &self.profile.decision_tree else {
268            return DispatchDecision::baseline(DispatchReason::MissingTree);
269        };
270        let variant = tree.evaluate(bytes);
271        if variant == "baseline" {
272            return DispatchDecision::baseline(DispatchReason::ProfileBaseline);
273        }
274        if !available.contains(&variant) {
275            return DispatchDecision::baseline(DispatchReason::UnavailableVariant);
276        }
277        DispatchDecision {
278            variant,
279            reason: DispatchReason::Selected,
280        }
281    }
282}
283
284/// Stable, machine-readable explanation of a guarded dispatch decision.
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
286#[serde(rename_all = "snake_case")]
287pub enum DispatchReason {
288    /// A supported specialized implementation was selected.
289    Selected,
290    /// The profile explicitly selected baseline.
291    ProfileBaseline,
292    /// The machine does not match the profile.
293    MachineMismatch,
294    /// The workload is absent from the profile.
295    MissingWorkload,
296    /// No size decision tree was recorded.
297    MissingTree,
298    /// The workload confidence is below the requested floor.
299    InsufficientConfidence,
300    /// The application cannot execute this variant on this host.
301    UnavailableVariant,
302}
303
304/// Allocation-free selection result for applications with a variant registry.
305#[derive(Debug, Clone, Copy, Serialize)]
306pub struct DispatchDecision<'a> {
307    /// Variant to execute; `baseline` must always be implemented by the caller.
308    pub variant: &'a str,
309    /// Why this variant was selected.
310    pub reason: DispatchReason,
311}
312
313impl DispatchDecision<'_> {
314    fn baseline(reason: DispatchReason) -> Self {
315        Self {
316            variant: "baseline",
317            reason,
318        }
319    }
320}
321
322/// Convenience: dispatch API used by specialized binaries.
323pub struct Dispatcher {
324    loaded: LoadedProfile,
325}
326
327impl Dispatcher {
328    /// Load profile with policy.
329    pub fn open(path: &Path, policy: MismatchPolicy) -> Result<Self> {
330        Ok(Self {
331            loaded: LoadedProfile::load(path, policy)?,
332        })
333    }
334
335    /// Status line (match / mismatch).
336    pub fn status(&self) -> &str {
337        &self.loaded.status
338    }
339
340    /// Whether baseline is forced.
341    pub fn using_baseline(&self) -> bool {
342        self.loaded.force_baseline
343    }
344
345    /// Select by workload name.
346    pub fn workload(&self, name: &str) -> &str {
347        self.loaded.select_workload(name)
348    }
349
350    /// Select by working-set size.
351    pub fn size(&self, bytes: u64) -> &str {
352        self.loaded.select_size(bytes)
353    }
354
355    /// Borrow loaded profile.
356    pub fn loaded(&self) -> &LoadedProfile {
357        &self.loaded
358    }
359
360    /// Brand / version about block for embedders.
361    pub fn about(&self) -> RuntimeInfo {
362        runtime_about()
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369    use silicera::brand::{
370        AFFILIATION_DISCLAIMER, BRAND_LINE, FUNDING_URL, HOMEPAGE, LICENSE, PHASE, REPOSITORY,
371        VERSION,
372    };
373    use silicera::hardware::{EnvironmentSnapshot, MockHardware, SupportStatus};
374    use silicera::hnep::{Confidence, HnepHeader, HnepProfile, IntegrityDigest, WorkloadEntry};
375    use silicera::knowledge::{KnowledgePack, Microarch};
376    use silicera::HardwareBackend;
377
378    fn make_profile(fp: &str) -> HnepProfile {
379        let header = HnepHeader {
380            format: silicera::hnep::HNEP_FORMAT.into(),
381            version: silicera::hnep::HNEP_VERSION,
382            silicera_version: silicera::VERSION.into(),
383            created_at: chrono::Utc::now().to_rfc3339(),
384            fingerprint: fp.into(),
385            label: "test".into(),
386        };
387        let environment = EnvironmentSnapshot::capture();
388        let workloads = vec![WorkloadEntry {
389            name: "demo".into(),
390            winner: "fast".into(),
391            confidence: Confidence::Medium,
392            rationale: "test".into(),
393            winner_median_ns: Some(90.0),
394            baseline_median_ns: Some(100.0),
395        }];
396        let size_classes: Vec<silicera::SizeClassEntry> = Vec::new();
397        let profile = HnepProfile {
398            header: header.clone(),
399            environment: environment.clone(),
400            workloads: workloads.clone(),
401            size_classes: size_classes.clone(),
402            decision_tree: None,
403            digest: IntegrityDigest {
404                alg: "sha256".into(),
405                hex: String::new(),
406            },
407        };
408        #[derive(serde::Serialize)]
409        struct Payload {
410            header: HnepHeader,
411            environment: EnvironmentSnapshot,
412            workloads: Vec<WorkloadEntry>,
413            size_classes: Vec<silicera::SizeClassEntry>,
414            decision_tree: Option<silicera::specialize::DecisionTree>,
415        }
416        let p = Payload {
417            header: profile.header.clone(),
418            environment: profile.environment.clone(),
419            workloads: profile.workloads.clone(),
420            size_classes: profile.size_classes.clone(),
421            decision_tree: None,
422        };
423        let bytes = serde_json::to_vec(&p).unwrap();
424        let digest = IntegrityDigest::sha256(&bytes);
425        HnepProfile { digest, ..profile }
426    }
427
428    #[test]
429    fn mismatch_falls_back() {
430        let kp = KnowledgePack::builtin();
431        let host = MockHardware::zen5_dual_ccd().discover(&kp).unwrap();
432        let fp = host.fingerprint.as_ref().unwrap().value.clone();
433        let profile = make_profile(&fp);
434        let loaded =
435            LoadedProfile::from_parts(profile, host, MismatchPolicy::FallbackBaseline).unwrap();
436        assert!(!loaded.force_baseline);
437        assert_eq!(loaded.select_workload("demo"), "fast");
438    }
439
440    #[test]
441    fn wrong_machine_baseline() {
442        let kp = KnowledgePack::builtin();
443        let host = MockHardware::zen5_dual_ccd().discover(&kp).unwrap();
444        let mut profile = make_profile("SLC:AMD:ZEN4:19:61:00:deadbeefdeadbeef:cafebabecafebabe");
445        // Fix digest for wrong fp profile
446        #[derive(serde::Serialize)]
447        struct Payload {
448            header: HnepHeader,
449            environment: EnvironmentSnapshot,
450            workloads: Vec<WorkloadEntry>,
451            size_classes: Vec<silicera::SizeClassEntry>,
452            decision_tree: Option<silicera::specialize::DecisionTree>,
453        }
454        let p = Payload {
455            header: profile.header.clone(),
456            environment: profile.environment.clone(),
457            workloads: profile.workloads.clone(),
458            size_classes: profile.size_classes.clone(),
459            decision_tree: None,
460        };
461        profile.digest = IntegrityDigest::sha256(&serde_json::to_vec(&p).unwrap());
462        let loaded =
463            LoadedProfile::from_parts(profile, host, MismatchPolicy::FallbackBaseline).unwrap();
464        assert!(loaded.force_baseline);
465        assert_eq!(loaded.select_workload("demo"), "baseline");
466        assert!(loaded.status.contains("PROFILE MISMATCH"));
467        assert!(loaded.status.contains(NAME));
468        let _ = SupportStatus::Unsupported { reason: "x".into() };
469        let _ = Microarch::Zen5;
470    }
471
472    #[test]
473    fn runtime_about_has_funding() {
474        let about = runtime_about();
475        assert_eq!(about.name, NAME);
476        assert_eq!(about.phase, PHASE);
477        assert_eq!(about.version, VERSION);
478        assert!(about.funding_url.contains("thanks.dev/u/gh/theworker02"));
479        assert_eq!(about.brand_line, BRAND_LINE);
480        assert_eq!(about.license, LICENSE);
481        assert_eq!(about.homepage, HOMEPAGE);
482        assert_eq!(about.repository, REPOSITORY);
483        assert_eq!(about.funding_url, FUNDING_URL);
484        assert_eq!(about.affiliation, AFFILIATION_DISCLAIMER);
485    }
486
487    #[test]
488    fn unsupported_host_obeys_strict_policy() {
489        for policy in [
490            MismatchPolicy::FallbackBaseline,
491            MismatchPolicy::StrictMachine,
492        ] {
493            let host = MockHardware::unsupported_intel()
494                .discover(&KnowledgePack::builtin())
495                .unwrap();
496            let profile = make_profile("SLC:AMD:ZEN4:19:61:00:deadbeefdeadbeef:cafebabecafebabe");
497            let result = LoadedProfile::from_parts(profile, host, policy);
498            if policy == MismatchPolicy::StrictMachine {
499                assert!(matches!(result, Err(SiliceraError::ProfileMismatch(_))));
500            } else {
501                let loaded = result.unwrap();
502                assert_eq!(
503                    loaded
504                        .guarded_workload("demo", &["fast"], Confidence::Medium)
505                        .reason,
506                    DispatchReason::MachineMismatch
507                );
508                assert_eq!(loaded.select_size(100), "baseline");
509            }
510        }
511    }
512
513    #[test]
514    fn direct_load_rejects_tampering_and_unknown_schema() {
515        let host = MockHardware::zen5_dual_ccd()
516            .discover(&KnowledgePack::builtin())
517            .unwrap();
518        let mut profile = make_profile(&host.fingerprint.as_ref().unwrap().value);
519        profile.workloads[0].winner = "tampered".into();
520        assert!(LoadedProfile::from_parts(
521            profile.clone(),
522            host.clone(),
523            MismatchPolicy::FallbackBaseline
524        )
525        .is_err());
526        profile.header.version = 999;
527        profile.recompute_digest().unwrap();
528        assert!(
529            LoadedProfile::from_parts(profile, host, MismatchPolicy::FallbackBaseline).is_err()
530        );
531    }
532
533    #[test]
534    fn guarded_dispatch_checks_registry_confidence_and_boundaries() {
535        let host = MockHardware::zen5_dual_ccd()
536            .discover(&KnowledgePack::builtin())
537            .unwrap();
538        let mut profile = make_profile(&host.fingerprint.as_ref().unwrap().value);
539        profile.decision_tree = Some(DecisionTree::from_thresholds(
540            10, 20, 30, "fast", "missing", "baseline", "fast", "baseline",
541        ));
542        profile.recompute_digest().unwrap();
543        let loaded =
544            LoadedProfile::from_parts(profile, host, MismatchPolicy::StrictMachine).unwrap();
545        assert_eq!(
546            loaded
547                .guarded_workload("demo", &["fast"], Confidence::Medium)
548                .variant,
549            "fast"
550        );
551        assert_eq!(
552            loaded
553                .guarded_workload("demo", &["fast"], Confidence::High)
554                .reason,
555            DispatchReason::InsufficientConfidence
556        );
557        assert_eq!(
558            loaded.guarded_workload("demo", &[], Confidence::Low).reason,
559            DispatchReason::UnavailableVariant
560        );
561        assert_eq!(
562            loaded
563                .guarded_workload("unknown", &["fast"], Confidence::Low)
564                .reason,
565            DispatchReason::MissingWorkload
566        );
567        assert_eq!(loaded.guarded_size(9, &["fast"]).variant, "fast");
568        assert_eq!(
569            loaded.guarded_size(10, &["fast"]).reason,
570            DispatchReason::UnavailableVariant
571        );
572        assert_eq!(
573            loaded.guarded_size(20, &["fast"]).reason,
574            DispatchReason::ProfileBaseline
575        );
576        assert_eq!(loaded.guarded_size(u64::MAX, &["fast"]).variant, "fast");
577    }
578}