1use crate::registry::VariantEntry;
4use crate::rules::registry::{reduction_entries, AggregateReduceFn, ReduceFn, ReductionEntry};
5use crate::rules::DynReductionResult;
6use serde::Serialize;
7use std::any::Any;
8use std::collections::{BTreeMap, BTreeSet};
9use std::sync::OnceLock;
10
11#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
13pub struct ExactProblemKey {
14 pub name: String,
15 pub variant: BTreeMap<String, String>,
16}
17
18impl ExactProblemKey {
19 pub fn new(name: impl Into<String>, variant: BTreeMap<String, String>) -> Self {
20 Self {
21 name: name.into(),
22 variant,
23 }
24 }
25
26 fn from_static(step: &StaticProblemStep) -> Self {
27 Self::new(
28 step.name,
29 step.variant
30 .iter()
31 .map(|&(key, value)| (key.to_string(), value.to_string()))
32 .collect(),
33 )
34 }
35
36 pub fn label(&self) -> String {
38 if self.variant.is_empty() {
39 return self.name.clone();
40 }
41 let values = self
42 .variant
43 .values()
44 .cloned()
45 .collect::<Vec<_>>()
46 .join(", ");
47 format!("{}<{values}>", self.name)
48 }
49
50 fn is_supported_ilp(&self) -> bool {
51 self.name == "ILP"
52 && matches!(
53 self.variant.get("variable").map(String::as_str),
54 Some("bool" | "i64")
55 )
56 && self.variant.get("coefficient").map(String::as_str) == Some("f64")
57 }
58}
59
60#[derive(Clone, Copy)]
62pub(crate) struct StaticProblemStep {
63 pub name: &'static str,
64 pub variant: &'static [(&'static str, &'static str)],
65}
66
67pub(crate) struct IlpPipelineRegistration {
73 pub(crate) path: &'static [StaticProblemStep],
74}
75
76inventory::collect!(IlpPipelineRegistration);
77
78type CustomizedSolveFn = fn(&dyn Any) -> Result<Option<serde_json::Value>, super::SolveError>;
79
80#[derive(Debug)]
82pub(crate) struct CustomizedSolverRegistration {
83 pub(crate) source_name: &'static str,
84 pub(crate) source_variant_fn: fn() -> Vec<(&'static str, &'static str)>,
85 pub(crate) implementation: &'static str,
86 pub(crate) solve_fn: CustomizedSolveFn,
87}
88
89impl CustomizedSolverRegistration {
90 fn source_key(&self) -> ExactProblemKey {
91 ExactProblemKey::new(
92 self.source_name,
93 (self.source_variant_fn)()
94 .into_iter()
95 .map(|(key, value)| (key.to_string(), value.to_string()))
96 .collect(),
97 )
98 }
99}
100
101inventory::collect!(CustomizedSolverRegistration);
102
103#[derive(Debug)]
104pub(crate) struct CompiledIlpPipeline {
105 path: Vec<ExactProblemKey>,
106 reducers: Vec<(ReduceFn, Option<AggregateReduceFn>)>,
107}
108
109impl CompiledIlpPipeline {
110 pub(crate) fn path(&self) -> &[ExactProblemKey] {
111 &self.path
112 }
113
114 pub(crate) fn path_labels(&self) -> Vec<String> {
115 self.path.iter().map(ExactProblemKey::label).collect()
116 }
117
118 fn solve_with<R>(
119 &self,
120 source: &dyn Any,
121 solver: &super::ILPSolver,
122 finish: impl FnOnce(
123 Box<dyn Any>,
124 Option<&dyn DynReductionResult>,
125 ) -> Result<R, super::ILPSolveError>,
126 ) -> Result<R, super::ILPSolveError> {
127 if self.reducers.is_empty() {
128 return finish(Box::new(solver.solve_dyn(source)?), None);
129 }
130
131 let mut reductions: Vec<Box<dyn DynReductionResult>> = Vec::new();
132 for (reducer, _) in &self.reducers {
133 let input = reductions
134 .last()
135 .map(|step| step.target_problem_any())
136 .unwrap_or(source);
137 reductions.push(reducer(input)?);
138 }
139
140 let target = reductions
141 .last()
142 .expect("non-empty fixed pipeline must produce a target")
143 .target_problem_any();
144 let solution = solver.solve_dyn(target)?;
145 let mut source_solution: Box<dyn Any> = Box::new(solution);
146 for (index, step) in reductions.iter().enumerate().rev() {
147 if let Some(reduce) = self.reducers[index].1 {
148 let input = if index == 0 {
149 source
150 } else {
151 reductions[index - 1].target_problem_any()
152 };
153 let aggregate = reduce(input)?;
154 let value = aggregate.extract_value_from_solution_dyn(source_solution.as_ref())?;
157 if value.downcast_ref::<crate::types::Or>() == Some(&crate::types::Or(false)) {
158 return Err(super::ILPSolveError::UnresolvedDecision(
159 self.path[index].label(),
160 ));
161 }
162 }
163 source_solution = step.extract_solution_dyn(source_solution.as_ref())?;
164 }
165 finish(source_solution, Some(reductions[0].as_ref()))
166 }
167
168 pub(crate) fn solve(
169 &self,
170 source: &dyn Any,
171 solver: &super::ILPSolver,
172 ) -> Result<serde_json::Value, super::ILPSolveError> {
173 self.solve_with(source, solver, |solution, first_reduction| {
174 if let Some(reduction) = first_reduction {
175 return reduction
176 .source_solution_json(solution.as_ref())
177 .map_err(super::ILPSolveError::from);
178 }
179 Ok(serde_json::to_value(
180 *solution
181 .downcast::<Vec<i64>>()
182 .expect("ILP backend returned the wrong solution type"),
183 )
184 .expect("ILP solution serialization failed"))
185 })
186 }
187
188 pub(crate) fn solve_typed<S: 'static>(
189 &self,
190 source: &dyn Any,
191 solver: &super::ILPSolver,
192 ) -> Result<S, super::ILPSolveError> {
193 self.solve_with(source, solver, |solution, _| {
194 solution
195 .downcast::<S>()
196 .map(|solution| *solution)
197 .map_err(|_| {
198 super::ILPSolveError::PipelineTypeMismatch(
199 self.path
200 .first()
201 .expect("compiled pipeline has a source")
202 .label(),
203 )
204 })
205 })
206 }
207}
208
209#[derive(Clone, Copy)]
210pub(crate) struct RegisteredSolverCapabilities<'a> {
211 pub(crate) customized: Option<&'static CustomizedSolverRegistration>,
212 pub(crate) ilp: Option<&'a CompiledIlpPipeline>,
213 pub(crate) brute_force: Option<&'static super::BruteForceRegistration>,
214}
215
216impl std::fmt::Debug for RegisteredSolverCapabilities<'_> {
217 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218 f.debug_struct("SolverCapabilities")
219 .field(
220 "customized",
221 &self.customized.map(|entry| entry.implementation),
222 )
223 .field("ilp", &self.ilp.map(CompiledIlpPipeline::path))
224 .field("brute_force", &self.brute_force.is_some())
225 .finish()
226 }
227}
228
229#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
231pub struct CustomizedSolverCapability {
232 pub implementation: &'static str,
233}
234
235#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
237pub struct IlpSolverCapability {
238 path: Vec<ExactProblemKey>,
239}
240
241impl IlpSolverCapability {
242 pub fn path(&self) -> &[ExactProblemKey] {
243 &self.path
244 }
245
246 pub fn path_labels(&self) -> Vec<String> {
247 self.path.iter().map(ExactProblemKey::label).collect()
248 }
249}
250
251#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
253pub struct SolverCapabilities {
254 pub customized: Option<CustomizedSolverCapability>,
255 pub ilp: Option<IlpSolverCapability>,
256 pub brute_force: bool,
257}
258
259#[derive(Debug, Default)]
260pub(crate) struct SolverCapabilityRegistry {
261 customized: BTreeMap<ExactProblemKey, &'static CustomizedSolverRegistration>,
262 ilp: BTreeMap<ExactProblemKey, CompiledIlpPipeline>,
263 brute_force: BTreeMap<ExactProblemKey, &'static super::BruteForceRegistration>,
264}
265
266impl SolverCapabilityRegistry {
267 pub(crate) fn lookup(&self, key: &ExactProblemKey) -> RegisteredSolverCapabilities<'_> {
268 RegisteredSolverCapabilities {
269 customized: self.customized.get(key).copied(),
270 ilp: self.ilp.get(key),
271 brute_force: self.brute_force.get(key).copied(),
272 }
273 }
274}
275
276#[derive(Debug, thiserror::Error)]
277pub enum RegistryBuildError {
278 #[error("solver registration references unknown exact variant {0}")]
279 UnknownVariant(String),
280 #[error("duplicate customized solver registration for {0}")]
281 DuplicateCustomized(String),
282 #[error("duplicate ILP pipeline registration for {0}")]
283 DuplicateIlp(String),
284 #[error("duplicate brute-force registration for {0}")]
285 DuplicateBruteForce(String),
286 #[error("exact variant {0} has no registered solver capability")]
287 MissingSolverCapability(String),
288 #[error("ILP pipeline must contain at least one node")]
289 EmptyPipeline,
290 #[error("ILP pipeline for {0} does not end at an f64-coefficient ILP")]
291 UnsupportedTarget(String),
292 #[error("ILP pipeline for {0} continues after reaching a supported ILP node")]
293 ContinuesAfterIlp(String),
294 #[error("ILP pipeline edge {source_label} -> {target_label} resolves to {matches} witness reductions")]
295 InvalidEdge {
296 source_label: String,
297 target_label: String,
298 matches: usize,
299 },
300}
301
302fn registered_variant_keys() -> BTreeSet<ExactProblemKey> {
303 inventory::iter::<VariantEntry>()
304 .map(|entry| ExactProblemKey::new(entry.name, entry.variant_map()))
305 .collect()
306}
307
308fn edge_key(entry: &ReductionEntry, source: bool) -> ExactProblemKey {
309 let (name, variant) = if source {
310 (entry.source_name, entry.source_variant())
311 } else {
312 (entry.target_name, entry.target_variant())
313 };
314 ExactProblemKey::new(
315 name,
316 variant
317 .into_iter()
318 .map(|(key, value)| (key.to_string(), value.to_string()))
319 .collect(),
320 )
321}
322
323fn build_registry(
324 variants: &BTreeSet<ExactProblemKey>,
325 customized_entries: impl IntoIterator<Item = &'static CustomizedSolverRegistration>,
326 pipeline_entries: impl IntoIterator<Item = &'static IlpPipelineRegistration>,
327 brute_force_entries: impl IntoIterator<Item = &'static super::BruteForceRegistration>,
328 reductions: &[&'static ReductionEntry],
329) -> Result<SolverCapabilityRegistry, RegistryBuildError> {
330 let mut registry = SolverCapabilityRegistry::default();
331 let mut reduction_index =
332 BTreeMap::<(ExactProblemKey, ExactProblemKey), Vec<&'static ReductionEntry>>::new();
333 for entry in reductions
334 .iter()
335 .copied()
336 .filter(|entry| entry.reduce_fn.is_some())
337 {
338 reduction_index
339 .entry((edge_key(entry, true), edge_key(entry, false)))
340 .or_default()
341 .push(entry);
342 }
343
344 for customized in customized_entries {
345 let source = customized.source_key();
346 if !variants.contains(&source) {
347 return Err(RegistryBuildError::UnknownVariant(source.label()));
348 }
349 if registry
350 .customized
351 .insert(source.clone(), customized)
352 .is_some()
353 {
354 return Err(RegistryBuildError::DuplicateCustomized(source.label()));
355 }
356 }
357
358 for brute_force in brute_force_entries {
359 let source = ExactProblemKey::new(
360 brute_force.source_name,
361 crate::export::variant_to_map((brute_force.source_variant_fn)()),
362 );
363 if !variants.contains(&source) {
364 return Err(RegistryBuildError::UnknownVariant(source.label()));
365 }
366 if registry
367 .brute_force
368 .insert(source.clone(), brute_force)
369 .is_some()
370 {
371 return Err(RegistryBuildError::DuplicateBruteForce(source.label()));
372 }
373 }
374
375 for registration in pipeline_entries {
376 let path = registration
377 .path
378 .iter()
379 .map(ExactProblemKey::from_static)
380 .collect::<Vec<_>>();
381 let source = path
382 .first()
383 .cloned()
384 .ok_or(RegistryBuildError::EmptyPipeline)?;
385
386 for step in &path {
387 if !variants.contains(step) {
388 return Err(RegistryBuildError::UnknownVariant(step.label()));
389 }
390 }
391 if !path.last().is_some_and(ExactProblemKey::is_supported_ilp) {
392 return Err(RegistryBuildError::UnsupportedTarget(source.label()));
393 }
394 if path[..path.len() - 1]
395 .iter()
396 .any(ExactProblemKey::is_supported_ilp)
397 {
398 return Err(RegistryBuildError::ContinuesAfterIlp(source.label()));
399 }
400
401 let mut reducers = Vec::with_capacity(path.len().saturating_sub(1));
402 for pair in path.windows(2) {
403 let matches = reduction_index
404 .get(&(pair[0].clone(), pair[1].clone()))
405 .map(Vec::as_slice)
406 .unwrap_or_default();
407 if matches.len() != 1 {
408 return Err(RegistryBuildError::InvalidEdge {
409 source_label: pair[0].label(),
410 target_label: pair[1].label(),
411 matches: matches.len(),
412 });
413 }
414 reducers.push((
415 matches[0]
416 .reduce_fn
417 .expect("indexed only entries with reduce_fn"),
418 matches[0].reduce_aggregate_fn,
419 ));
420 }
421
422 if registry
423 .ilp
424 .insert(source.clone(), CompiledIlpPipeline { path, reducers })
425 .is_some()
426 {
427 return Err(RegistryBuildError::DuplicateIlp(source.label()));
428 }
429 }
430
431 for variant in variants {
432 if !registry.customized.contains_key(variant)
433 && !registry.ilp.contains_key(variant)
434 && !registry.brute_force.contains_key(variant)
435 {
436 return Err(RegistryBuildError::MissingSolverCapability(variant.label()));
437 }
438 }
439
440 Ok(registry)
441}
442
443static REGISTRY: OnceLock<Result<SolverCapabilityRegistry, RegistryBuildError>> = OnceLock::new();
444
445pub(crate) fn solver_capability_registry(
446) -> Result<&'static SolverCapabilityRegistry, &'static RegistryBuildError> {
447 REGISTRY
448 .get_or_init(|| {
449 build_registry(
450 ®istered_variant_keys(),
451 inventory::iter::<CustomizedSolverRegistration>(),
452 inventory::iter::<IlpPipelineRegistration>(),
453 inventory::iter::<super::BruteForceRegistration>(),
454 &reduction_entries(),
455 )
456 })
457 .as_ref()
458}
459
460pub fn solver_capabilities(
462 key: &ExactProblemKey,
463) -> Result<SolverCapabilities, &'static RegistryBuildError> {
464 let registered = solver_capability_registry()?.lookup(key);
465 Ok(SolverCapabilities {
466 customized: registered
467 .customized
468 .map(|entry| CustomizedSolverCapability {
469 implementation: entry.implementation,
470 }),
471 ilp: registered.ilp.map(|pipeline| IlpSolverCapability {
472 path: pipeline.path.clone(),
473 }),
474 brute_force: registered.brute_force.is_some(),
475 })
476}
477
478pub(crate) fn brute_force_registration(
479 key: &ExactProblemKey,
480) -> Result<Option<&'static super::BruteForceRegistration>, &'static RegistryBuildError> {
481 Ok(solver_capability_registry()?.lookup(key).brute_force)
482}
483
484#[doc(hidden)]
486pub fn brute_force_dimensions(
487 problem: &crate::registry::LoadedDynProblem,
488) -> Result<Option<Vec<usize>>, &'static RegistryBuildError> {
489 let key = ExactProblemKey::new(problem.problem_name(), problem.variant_map());
490 Ok(brute_force_registration(&key)?
491 .map(|registration| (registration.dimensions_fn)(problem.as_any())))
492}
493
494#[cfg(test)]
495#[path = "../unit_tests/solvers/registry.rs"]
496mod tests;