1use crate::SpaceError;
4use crate::constraint::Constraint;
5use serde::Deserialize;
6use std::fmt;
7use std::path::{Path, PathBuf};
8
9#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum DimRole {
28 Launch,
31 Spec,
34}
35
36#[derive(Debug, Clone)]
38pub struct Dim {
39 pub name: String,
40 pub role: DimRole,
41 pub values: Vec<Value>,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum SafetyExpectation {
48 Flip,
50 Stable,
52 None,
54}
55
56#[derive(Debug, Clone)]
58pub struct KernelSpec {
59 pub name: String,
60 pub entry: String,
62 pub domain: u8,
64 pub needs_cc: Option<String>,
66 pub known: SafetyExpectation,
67 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 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 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 pub fn dims_sorted(&self) -> Vec<&Dim> {
227 self.dims.iter().collect()
228 }
229}