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