1mod constraint;
10mod spec;
11
12pub use constraint::{Constraint, eval_arith_expr};
13pub use spec::{Dim, DimRole, KernelSpec, SafetyExpectation, Value};
14
15use sha2::{Digest, Sha256};
16use std::collections::BTreeMap;
17use std::fmt;
18
19#[derive(Debug, thiserror::Error)]
20pub enum SpaceError {
21 #[error("failed to read {path}: {source}")]
22 Io {
23 path: String,
24 source: std::io::Error,
25 },
26 #[error("failed to parse {path}: {source}")]
27 Parse {
28 path: String,
29 source: Box<toml::de::Error>,
30 },
31 #[error("invalid kernel spec: {0}")]
32 Invalid(String),
33 #[error("invalid constraint `{expr}`: {reason}")]
34 Constraint { expr: String, reason: String },
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Config {
42 kernel: String,
43 values: BTreeMap<String, Value>,
44}
45
46impl Config {
47 pub fn kernel(&self) -> &str {
48 &self.kernel
49 }
50
51 pub fn get(&self, dim: &str) -> Option<&Value> {
52 self.values.get(dim)
53 }
54
55 pub fn values(&self) -> impl Iterator<Item = (&str, &Value)> {
56 self.values.iter().map(|(k, v)| (k.as_str(), v))
57 }
58
59 pub fn block_threads(&self) -> u64 {
62 ["block_x", "block_y", "block_z"]
63 .iter()
64 .map(|d| match self.values.get(*d) {
65 Some(Value::Int(n)) => *n,
66 _ => 1,
67 })
68 .product()
69 }
70
71 pub fn id(&self) -> ConfigId {
75 let mut hasher = Sha256::new();
76 hasher.update(b"launchbound.config.v1\0");
77 hasher.update(self.kernel.as_bytes());
78 hasher.update(b"\0");
79 for (name, value) in &self.values {
80 hasher.update(name.as_bytes());
81 hasher.update(b"=");
82 match value {
83 Value::Int(n) => hasher.update(n.to_string().as_bytes()),
84 Value::Str(s) => hasher.update(s.as_bytes()),
85 }
86 hasher.update(b"\n");
87 }
88 let digest = hasher.finalize();
89 let mut hex = String::with_capacity(16);
90 for byte in &digest[..8] {
91 hex.push_str(&format!("{byte:02x}"));
92 }
93 ConfigId(format!("c1-{hex}"))
94 }
95
96 pub fn spec_key(&self, spec: &KernelSpec) -> String {
100 let mut parts = Vec::new();
101 for (name, value) in &self.values {
102 if spec.dim(name).is_some_and(|d| d.role == DimRole::Spec) {
103 parts.push(format!("{name}={value}"));
104 }
105 }
106 parts.join(",")
107 }
108}
109
110impl fmt::Display for Config {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 let mut first = true;
113 for (name, value) in &self.values {
114 if !first {
115 write!(f, " ")?;
116 }
117 write!(f, "{name}={value}")?;
118 first = false;
119 }
120 Ok(())
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
126pub struct ConfigId(String);
127
128impl ConfigId {
129 pub fn as_str(&self) -> &str {
130 &self.0
131 }
132}
133
134impl fmt::Display for ConfigId {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 f.write_str(&self.0)
137 }
138}
139
140pub fn enumerate(spec: &KernelSpec) -> Result<Vec<Config>, SpaceError> {
145 let dims: Vec<&Dim> = spec.dims_sorted();
146 let mut out = Vec::new();
147 if dims.is_empty() {
148 return Ok(out);
149 }
150 let mut indices = vec![0usize; dims.len()];
151 'outer: loop {
152 let mut values = BTreeMap::new();
153 for (dim, &idx) in dims.iter().zip(&indices) {
154 values.insert(dim.name.clone(), dim.values[idx].clone());
155 }
156 let config = Config {
157 kernel: spec.name.clone(),
158 values,
159 };
160 if spec
161 .constraints
162 .iter()
163 .try_fold(true, |ok, c| c.eval(&config).map(|v| ok && v))?
164 {
165 out.push(config);
166 }
167 for pos in (0..dims.len()).rev() {
169 indices[pos] += 1;
170 if indices[pos] < dims[pos].values.len() {
171 continue 'outer;
172 }
173 indices[pos] = 0;
174 }
175 break;
176 }
177 Ok(out)
178}
179
180pub fn raw_size(spec: &KernelSpec) -> u64 {
182 spec.dims_sorted()
183 .iter()
184 .map(|d| d.values.len() as u64)
185 .product()
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 fn toy_spec() -> KernelSpec {
193 KernelSpec::from_toml_str(
194 "toy",
195 r#"
196 [kernel]
197 name = "toy"
198 entry = "toy"
199 domain = 1
200 [dims.block_x]
201 values = [32, 64, 128]
202 [dims.tile]
203 role = "spec"
204 values = [128, 256]
205 [constraints]
206 exprs = ["tile % block_x == 0"]
207 "#,
208 )
209 .unwrap()
210 }
211
212 #[test]
213 fn enumeration_is_deterministic_and_filtered() {
214 let spec = toy_spec();
215 let a = enumerate(&spec).unwrap();
216 let b = enumerate(&spec).unwrap();
217 assert_eq!(a, b);
218 assert_eq!(raw_size(&spec), 6);
222 assert_eq!(a.len(), 6);
223 }
224
225 #[test]
226 fn constraint_actually_filters() {
227 let spec = KernelSpec::from_toml_str(
228 "toy",
229 r#"
230 [kernel]
231 name = "toy"
232 entry = "toy"
233 domain = 1
234 [dims.block_x]
235 values = [32, 48]
236 [dims.tile]
237 values = [64]
238 [constraints]
239 exprs = ["tile % block_x == 0"]
240 "#,
241 )
242 .unwrap();
243 let configs = enumerate(&spec).unwrap();
244 assert_eq!(configs.len(), 1);
245 assert_eq!(configs[0].get("block_x"), Some(&Value::Int(32)));
246 }
247
248 #[test]
249 fn ids_are_stable_and_distinct() {
250 let spec = toy_spec();
251 let configs = enumerate(&spec).unwrap();
252 let ids: Vec<_> = configs.iter().map(|c| c.id()).collect();
253 let mut unique = ids.clone();
254 unique.sort();
255 unique.dedup();
256 assert_eq!(unique.len(), ids.len(), "duplicate config IDs");
257 let first = &configs[0];
260 assert_eq!(first.get("block_x"), Some(&Value::Int(32)));
261 assert_eq!(first.get("tile"), Some(&Value::Int(128)));
262 assert_eq!(first.id().as_str(), configs[0].id().as_str());
263 assert!(first.id().as_str().starts_with("c1-"));
264 assert_eq!(first.id().as_str().len(), 3 + 16);
265 }
266
267 #[test]
268 fn block_threads_multiplies_and_defaults() {
269 let spec = toy_spec();
270 let configs = enumerate(&spec).unwrap();
271 assert_eq!(configs[0].block_threads(), 32);
272 }
273
274 #[test]
275 fn spec_key_covers_only_spec_dims() {
276 let spec = toy_spec();
277 let configs = enumerate(&spec).unwrap();
278 assert_eq!(configs[0].spec_key(&spec), "tile=128");
279 }
280}