Skip to main content

launchbound_space/
spec.rs

1//! Kernel spec: the `kernel.toml` sitting next to a corpus kernel's source.
2
3use crate::SpaceError;
4use crate::constraint::Constraint;
5use serde::Deserialize;
6use std::fmt;
7use std::path::{Path, PathBuf};
8
9/// One value a dimension can take.
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
11pub enum Value {
12    Int(u64),
13    Str(String),
14}
15
16impl fmt::Display for Value {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            Value::Int(n) => write!(f, "{n}"),
20            Value::Str(s) => f.write_str(s),
21        }
22    }
23}
24
25/// What a dimension controls.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum DimRole {
28    /// Launch geometry only: no source change, no recompile. `block_x`,
29    /// `block_y`, `block_z` default to this.
30    Launch,
31    /// Compile-time specialization: a distinct generated source, therefore a
32    /// distinct reconverge verdict and a distinct compiled artifact.
33    Spec,
34}
35
36/// One tunable dimension.
37#[derive(Debug, Clone)]
38pub struct Dim {
39    pub name: String,
40    pub role: DimRole,
41    pub values: Vec<Value>,
42}
43
44/// What the corpus documents about a kernel's expected gate behaviour, so
45/// the gate is tested in both directions (corpus/README.md).
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum SafetyExpectation {
48    /// Known to flip safety with block size (the ยง2.2 family).
49    Flip,
50    /// Known not to flip at any block size in its space.
51    Stable,
52    /// No documented expectation.
53    None,
54}
55
56/// A kernel's declared tuning space, loaded from `kernel.toml`.
57#[derive(Debug, Clone)]
58pub struct KernelSpec {
59    pub name: String,
60    /// The `#[kernel]` entry function name.
61    pub entry: String,
62    /// Launch-contract domain (1, 2 or 3).
63    pub domain: u8,
64    /// Minimum compute capability the kernel needs, e.g. "7.0".
65    pub needs_cc: Option<String>,
66    pub known: SafetyExpectation,
67    /// Directory holding the kernel crate (where kernel.toml lives).
68    pub dir: PathBuf,
69    pub dims: Vec<Dim>,
70    pub constraints: Vec<Constraint>,
71}
72
73#[derive(Deserialize)]
74struct RawSpec {
75    kernel: RawKernel,
76    #[serde(default)]
77    dims: toml::value::Table,
78    #[serde(default)]
79    constraints: RawConstraints,
80}
81
82#[derive(Deserialize)]
83struct RawKernel {
84    name: String,
85    entry: String,
86    domain: u8,
87    #[serde(default)]
88    needs_cc: Option<String>,
89    #[serde(default)]
90    known: Option<String>,
91}
92
93#[derive(Deserialize, Default)]
94struct RawConstraints {
95    #[serde(default)]
96    exprs: Vec<String>,
97}
98
99#[derive(Deserialize)]
100struct RawDim {
101    #[serde(default)]
102    role: Option<String>,
103    values: Vec<toml::Value>,
104}
105
106impl KernelSpec {
107    /// Load `<dir>/kernel.toml`.
108    pub fn load(dir: &Path) -> Result<Self, SpaceError> {
109        let path = dir.join("kernel.toml");
110        let text = std::fs::read_to_string(&path).map_err(|source| SpaceError::Io {
111            path: path.display().to_string(),
112            source,
113        })?;
114        Self::parse(&path.display().to_string(), &text, dir.to_path_buf())
115    }
116
117    /// Parse from a string (tests).
118    pub fn from_toml_str(origin: &str, text: &str) -> Result<Self, SpaceError> {
119        Self::parse(origin, text, PathBuf::from("."))
120    }
121
122    fn parse(origin: &str, text: &str, dir: PathBuf) -> Result<Self, SpaceError> {
123        let raw: RawSpec = toml::from_str(text).map_err(|source| SpaceError::Parse {
124            path: origin.to_string(),
125            source: Box::new(source),
126        })?;
127
128        if !(1..=3).contains(&raw.kernel.domain) {
129            return Err(SpaceError::Invalid(format!(
130                "domain must be 1..=3, got {}",
131                raw.kernel.domain
132            )));
133        }
134        let known = match raw.kernel.known.as_deref() {
135            Some("flip") => SafetyExpectation::Flip,
136            Some("stable") => SafetyExpectation::Stable,
137            None => SafetyExpectation::None,
138            Some(other) => {
139                return Err(SpaceError::Invalid(format!(
140                    "known must be \"flip\" or \"stable\", got {other:?}"
141                )));
142            }
143        };
144
145        let mut dims = Vec::new();
146        for (name, value) in raw.dims {
147            if !name
148                .chars()
149                .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
150            {
151                return Err(SpaceError::Invalid(format!(
152                    "dimension name {name:?} must be [a-z0-9_]"
153                )));
154            }
155            let raw_dim: RawDim = value
156                .try_into()
157                .map_err(|e| SpaceError::Invalid(format!("dimension {name}: {e}")))?;
158            let role = match raw_dim.role.as_deref() {
159                Some("launch") => DimRole::Launch,
160                Some("spec") => DimRole::Spec,
161                None if name.starts_with("block_") => DimRole::Launch,
162                None => DimRole::Spec,
163                Some(other) => {
164                    return Err(SpaceError::Invalid(format!(
165                        "dimension {name}: role must be \"launch\" or \"spec\", got {other:?}"
166                    )));
167                }
168            };
169            let mut values = Vec::new();
170            for v in raw_dim.values {
171                match v {
172                    toml::Value::Integer(n) if n >= 0 => values.push(Value::Int(n as u64)),
173                    toml::Value::String(s) => values.push(Value::Str(s)),
174                    other => {
175                        return Err(SpaceError::Invalid(format!(
176                            "dimension {name}: values must be non-negative integers or strings, got {other}"
177                        )));
178                    }
179                }
180            }
181            if values.is_empty() {
182                return Err(SpaceError::Invalid(format!(
183                    "dimension {name} has no values"
184                )));
185            }
186            let mut seen = values.clone();
187            seen.sort();
188            seen.dedup();
189            if seen.len() != values.len() {
190                return Err(SpaceError::Invalid(format!(
191                    "dimension {name} has duplicate values"
192                )));
193            }
194            dims.push(Dim { name, role, values });
195        }
196        if dims.is_empty() {
197            return Err(SpaceError::Invalid("spec declares no dimensions".into()));
198        }
199        dims.sort_by(|a, b| a.name.cmp(&b.name));
200
201        let dim_names: Vec<&str> = dims.iter().map(|d| d.name.as_str()).collect();
202        let constraints = raw
203            .constraints
204            .exprs
205            .iter()
206            .map(|e| Constraint::parse(e, &dim_names))
207            .collect::<Result<Vec<_>, _>>()?;
208
209        Ok(KernelSpec {
210            name: raw.kernel.name,
211            entry: raw.kernel.entry,
212            domain: raw.kernel.domain,
213            needs_cc: raw.kernel.needs_cc,
214            known,
215            dir,
216            dims,
217            constraints,
218        })
219    }
220
221    pub fn dim(&self, name: &str) -> Option<&Dim> {
222        self.dims.iter().find(|d| d.name == name)
223    }
224
225    /// Dimensions in canonical (name-sorted) order.
226    pub fn dims_sorted(&self) -> Vec<&Dim> {
227        self.dims.iter().collect()
228    }
229}