solverforge-solver 0.18.0

Solver engine for SolverForge
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
use solverforge_config::{
    ConstructionHeuristicConfig, ConstructionHeuristicType, LocalSearchConfig, MoveSelectorConfig,
    PartitionedSearchConfig, SelectionOrder, SolverConfig, UnionSelectionOrder, UnionWeighting,
};

use crate::builder::{
    RuntimeCandidateMetricBinding, RuntimeProviderHandle, RuntimeScalarSlotId, ScalarGroupBinding,
    SearchContext,
};
use crate::descriptor::ResolvedVariableBinding;
use crate::phase::construction::{ScalarConstructionSchedule, ScalarOrMixedSlotOrder};

use super::defaults::DefaultRuntimeBindings;
use super::types::{CompiledListSlot, CompiledScalarSlot};

/// Scalar leaf families whose execution will be shared by typed and dynamic
/// `ScalarAccess` kernels.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ScalarLeafKind {
    Change,
    Swap,
    NearbyChange,
    NearbySwap,
    PillarChange,
    PillarSwap,
    RuinRecreate,
}

/// List leaf families whose execution will be shared by typed and dynamic
/// `ListAccess` kernels.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ListLeafKind {
    Change,
    NearbyChange,
    Swap,
    Permute,
    Precedence,
    NearbySwap,
    SublistChange,
    SublistSwap,
    Reverse,
    KOpt,
    Ruin,
}

/// Exhaustive compiled list-construction family.
///
/// The executor must match this discriminator directly. In particular,
/// Clarke-Wright is never a cheapest-insertion alias: it owns its savings,
/// merge, and completion candidate trace semantics before any separately
/// configured K-opt phase runs.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ListConstructionKind {
    RoundRobin,
    CheapestInsertion,
    RegretInsertion,
    ClarkeWright,
    KOpt,
}

impl ListConstructionKind {
    pub(super) fn from_heuristic(heuristic: ConstructionHeuristicType) -> Option<Self> {
        match heuristic {
            ConstructionHeuristicType::ListRoundRobin => Some(Self::RoundRobin),
            ConstructionHeuristicType::ListCheapestInsertion => Some(Self::CheapestInsertion),
            ConstructionHeuristicType::ListRegretInsertion => Some(Self::RegretInsertion),
            ConstructionHeuristicType::ListClarkeWright => Some(Self::ClarkeWright),
            ConstructionHeuristicType::ListKOpt => Some(Self::KOpt),
            _ => None,
        }
    }
}

/// Immutable binding for one source inside the common provider cursor.
///
/// Compilation never opens/pulls a provider.  The cursor receives only these
/// frozen handles, so `limited(0)`, unreached union branches, and Cartesian
/// right branches after an impossible left preview cannot observe callback
/// delivery.  A static Rust function and a host callback therefore share one
/// cursor/store/normalizer while retaining their documented ordering policy.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ProviderBindingPlan {
    pub handle: RuntimeProviderHandle,
    /// Immutable source-schema provenance; never substitute a filtered vector
    /// position when emitting trace/provenance.
    pub declared_schema_index: usize,
    pub allowed_slots: Vec<RuntimeScalarSlotId>,
    pub policy: ProviderBindingPolicy,
    pub candidate_contract: ProviderCandidateContract,
}

/// Source-specific behavior interpreted by the one future provider cursor.
/// These variants are data, not selector implementations.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ProviderBindingPolicy {
    /// Host callback group: normalize/dedupe, take the callback cap (explicit
    /// zero clamps to one), then rotate the resulting move order.
    CallbackGroup {
        rotation_seed_salt: u64,
        pull_timing: ProviderPullTiming,
    },
    /// Existing Rust candidate group: pull/open behavior and rotate-before-
    /// normalization semantics remain explicit while using the same cursor.
    StaticGroup {
        rotation_seed_salt: u64,
        declared_max_moves_per_step: Option<usize>,
        pull_timing: ProviderPullTiming,
    },
    /// Host repair: rotate the full declaration vector before filtering,
    /// invoke a matching multi-constraint provider at most once, and reset
    /// deduplication for every provider result.
    CallbackRepair {
        rotation_seed_salt: u64,
        pull_timing: ProviderPullTiming,
    },
    /// Existing Rust repair: rotate configured constraints, matching provider
    /// indexes, and returned specs with the historic salts; dedup globally
    /// across its configured repair stream.
    StaticRepair {
        constraint_rotation_seed_salt: u64,
        provider_rotation_seed_salt: u64,
        spec_rotation_seed_salt: u64,
        pull_timing: ProviderPullTiming,
    },
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ProviderPullTiming {
    /// Generic host callbacks are pulled only once the first reachable cursor
    /// `next_candidate` needs them.
    FirstReachableNext,
    /// Native Rust providers open eagerly; callback providers pull only when
    /// their first candidate is reached.
    OpenCursor,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ProviderSchedule {
    Group {
        value_candidate_limit: Option<usize>,
        requested_max_moves_per_step: Option<usize>,
    },
    Repair {
        constraints: Vec<String>,
        max_matches_per_step: usize,
        max_repairs_per_match: usize,
        max_moves_per_step: usize,
        include_soft_matches: bool,
    },
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct CompiledProviderPlan {
    /// The existing dynamic public contract prefixes the tabu/ownership
    /// signature with this kind. Callback reason is deliberately excluded.
    pub move_kind: ProviderMoveKind,
    pub schedule: ProviderSchedule,
    pub bindings: Vec<ProviderBindingPlan>,
}

/// Stable provider-family discriminator retained in move/tabu ownership.
/// Values match the public wrapper's existing dynamic compound signatures.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(u8)]
pub(crate) enum ProviderMoveKind {
    Grouped = 1,
    ConflictRepair = 2,
    CompoundConflictRepair = 3,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ProviderCandidateContract {
    pub reason_storage: ProviderReasonStorage,
    pub deduplication: ProviderCandidateDeduplication,
    pub tabu_identity: ProviderTabuIdentity,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ProviderReasonStorage {
    /// Provider labels are interned into the solve-owned reason arena when
    /// raw callback output crosses the normalization boundary. Candidate moves
    /// thereafter carry this compact ID, never an `Arc<str>`.
    PerRunInternedId,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ProviderCandidateDeduplication {
    /// Same ordered edits with a different reason are distinct callback
    /// candidates, exactly as Python's existing declaration contract.
    PerProviderReasonAndOrderedEdits,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ProviderTabuIdentity {
    /// Tabu keys contain the provider-kind prefix and ordered edit
    /// targets/values.  User callback labels never affect seeded randomness,
    /// but the same edits emitted by Grouped, ConflictRepair, and
    /// CompoundConflictRepair remain distinct move families.
    ProviderKindAndOrderedEdits,
}

pub(super) const GROUP_PROVIDER_ROTATION_SALT: u64 = 0xC0A1_E5CE_DA7A_0001;
pub(super) const REPAIR_PROVIDER_ROTATION_SALT: u64 = 0xC0AF_11C7_DA7A_0001;
pub(super) const STATIC_GROUP_PROVIDER_ROTATION_SALT: u64 = 0xC0A1_E5CE_AAA0_0001;
pub(super) const STATIC_REPAIR_CONSTRAINT_ROTATION_SALT: u64 = 0xC0AF_11C7_0000_0001;
pub(super) const STATIC_REPAIR_PROVIDER_ROTATION_SALT: u64 = 0xC0AF_11C7_0000_0002;
pub(super) const STATIC_REPAIR_SPEC_ROTATION_SALT: u64 = 0xC0AF_11C7_0000_0003;

pub(super) const PYTHON_PROVIDER_CANDIDATE_CONTRACT: ProviderCandidateContract =
    ProviderCandidateContract {
        reason_storage: ProviderReasonStorage::PerRunInternedId,
        deduplication: ProviderCandidateDeduplication::PerProviderReasonAndOrderedEdits,
        tabu_identity: ProviderTabuIdentity::ProviderKindAndOrderedEdits,
    };

/// Recursive immutable selector graph.  `Limited`, `Union`, and `Cartesian`
/// are true recursive nodes so nested Cartesian graphs work identically for
/// acceptor-forager local search and VND neighborhoods.
#[derive(Clone, Debug)]
#[expect(
    clippy::large_enum_variant,
    reason = "compiled selector nodes are an immutable value-owned graph"
)]
pub(crate) enum CompiledSelectorNode<S, V, DM, IDM> {
    Scalar {
        kind: ScalarLeafKind,
        config: MoveSelectorConfig,
        candidate_order: SelectionOrder,
        candidate_metric: Option<RuntimeCandidateMetricBinding<S>>,
        slots: Vec<CompiledScalarSlot<S>>,
    },
    List {
        kind: ListLeafKind,
        config: MoveSelectorConfig,
        candidate_order: SelectionOrder,
        candidate_metric: Option<RuntimeCandidateMetricBinding<S>>,
        slots: Vec<CompiledListSlot<S, V, DM, IDM>>,
    },
    GroupedScalar {
        config: MoveSelectorConfig,
        candidate_order: SelectionOrder,
        candidate_metric: Option<RuntimeCandidateMetricBinding<S>>,
        group_index: usize,
        group: ScalarGroupBinding<S>,
    },
    /// Every non-assignment group/repair source uses this one provider-node
    /// shape.  The eventual cursor dispatches only the frozen handle/policy;
    /// it does not choose a typed versus callback selector tree.
    Provider {
        config: MoveSelectorConfig,
        candidate_order: SelectionOrder,
        candidate_metric: Option<RuntimeCandidateMetricBinding<S>>,
        plan: CompiledProviderPlan,
    },
    Limited {
        selected_count_limit: usize,
        selector: Box<Self>,
    },
    Union {
        selection_order: UnionSelectionOrder,
        weighting: UnionWeighting,
        weights: Vec<u64>,
        children: Vec<Self>,
    },
    Cartesian {
        require_hard_improvement: bool,
        left: Box<Self>,
        right: Box<Self>,
    },
}

impl<S, V, DM, IDM> CompiledSelectorNode<S, V, DM, IDM> {
    pub(super) fn requires_score_during_move(&self) -> bool {
        match self {
            Self::Scalar { kind, .. } => matches!(kind, ScalarLeafKind::RuinRecreate),
            Self::List { kind, .. } => matches!(kind, ListLeafKind::Ruin),
            Self::GroupedScalar { .. } | Self::Provider { .. } => false,
            Self::Limited { selector, .. } => selector.requires_score_during_move(),
            Self::Union { children, .. } => children.iter().any(Self::requires_score_during_move),
            Self::Cartesian { left, right, .. } => {
                left.requires_score_during_move() || right.requires_score_during_move()
            }
        }
    }

    pub(super) fn contains_provider(&self) -> bool {
        match self {
            Self::Provider { .. } => true,
            Self::Limited { selector, .. } => selector.contains_provider(),
            Self::Union { children, .. } => children.iter().any(Self::contains_provider),
            Self::Cartesian { left, right, .. } => {
                left.contains_provider() || right.contains_provider()
            }
            Self::Scalar { .. } | Self::List { .. } | Self::GroupedScalar { .. } => false,
        }
    }
}

#[derive(Clone, Debug)]
#[expect(
    clippy::large_enum_variant,
    reason = "compiled construction stays value-owned through preparation"
)]
pub(crate) enum CompiledConstruction<S, V, DM, IDM> {
    ScalarOrMixed {
        config: ConstructionHeuristicConfig,
        /// Frozen selection semantics. The executor must consume this
        /// directly instead of rediscovering typed/dynamic shape from a
        /// model or solution.
        schedule: ScalarConstructionSchedule,
        scalar_slots: Vec<CompiledScalarSlot<S>>,
        list_slots: Vec<CompiledListSlot<S, V, DM, IDM>>,
        /// Original declaration order plus the construction-frontier slot
        /// identity for each carrier. Scalar and list payloads are stored in
        /// compact separate vectors, but their execution order is never
        /// reconstructed from those vectors.
        slot_order: Vec<ScalarOrMixedSlotOrder>,
    },
    List {
        kind: ListConstructionKind,
        config: ConstructionHeuristicConfig,
        slots: Vec<CompiledListSlot<S, V, DM, IDM>>,
    },
    GroupedScalar {
        config: ConstructionHeuristicConfig,
        group_index: usize,
        group: ScalarGroupBinding<S>,
        /// Descriptor-resolved scalar construction bindings are frozen with
        /// the group.  The runtime executor must not rediscover them from a
        /// descriptor while constructing a candidate group.
        scalar_bindings: Vec<ResolvedVariableBinding<S>>,
    },
}

#[derive(Clone, Debug)]
#[expect(
    clippy::large_enum_variant,
    reason = "compiled selector declarations stay value-owned"
)]
pub(crate) enum CompiledAcceptorForagerSelector<S, V, DM, IDM> {
    /// A user-declared selector compiled directly into the immutable graph.
    Explicit(CompiledSelectorNode<S, V, DM, IDM>),
    /// This phase intentionally consumes `DefaultRuntimeBindings`' one
    /// capability-resolved declaration. It is not an absent selector or a
    /// late-selected alternative.
    OmittedDefault,
}

#[derive(Clone, Debug)]
#[expect(
    clippy::large_enum_variant,
    reason = "compiled local search is an immutable value-owned declaration"
)]
pub(crate) enum CompiledLocalSearch<S, V, DM, IDM> {
    AcceptorForager {
        config: LocalSearchConfig,
        selector: CompiledAcceptorForagerSelector<S, V, DM, IDM>,
    },
    VariableNeighborhoodDescent {
        config: LocalSearchConfig,
        neighborhoods: Vec<CompiledSelectorNode<S, V, DM, IDM>>,
    },
}

#[derive(Clone, Debug)]
#[expect(
    clippy::large_enum_variant,
    reason = "compiled phases remain one value-owned execution graph"
)]
pub(crate) enum CompiledRuntimePhase<S, V, DM, IDM> {
    Construction(CompiledConstruction<S, V, DM, IDM>),
    LocalSearch(CompiledLocalSearch<S, V, DM, IDM>),
    Extension(CompiledRuntimeExtension),
    /// The omitted-phase profile is deliberately a first-class, per-solve
    /// graph node.  Its construction children depend on the current
    /// solution's unassigned rows and route content; its optional default
    /// local-search child depends on the effective solver termination.  A
    /// future instantiator resolves it from this compiled model and records
    /// that exact result for candidate trace provenance.
    DefaultRuntime,
}

/// Immutable declaration of one typed extension phase.
///
/// It contains only the selected registry name and frozen configuration.
/// Concrete extension builders are deliberately deferred to per-solve graph
/// execution so compilation has no callback, allocation, or phase side effect.
#[derive(Clone, Debug)]
pub(crate) enum CompiledRuntimeExtension {
    Custom {
        name: String,
    },
    Partitioned {
        name: String,
        config: PartitionedSearchConfig,
    },
}

/// Immutable per-config graph.  It deliberately contains no current solution,
/// callback view, thread-local state, or fresh selector cursor.  Those are
/// created only by `instantiate()` at the future atomic cutover.
pub(crate) struct CompiledRuntimeGraph<S, V, DM, IDM, E>
where
    S: solverforge_core::domain::PlanningSolution,
{
    /// One descriptor-resolved context is owned by the graph together with
    /// its concrete extension registry. The executor must use this exact pair
    /// rather than rebuilding model metadata or registry state per phase.
    pub(super) context: SearchContext<S, V, DM, IDM>,
    pub(super) extensions: E,
    pub(super) config: SolverConfig,
    /// Frozen declaration-order bindings for omitted runtime phases. Per-solve
    /// default expansion reads solution state through these slots but never
    /// reconstructs a model/schema or delegates construction to a wrapper.
    pub(super) default_bindings: DefaultRuntimeBindings<S, V, DM, IDM>,
    pub(super) phases: Vec<CompiledRuntimePhase<S, V, DM, IDM>>,
}

impl<S, V, DM, IDM, E> std::fmt::Debug for CompiledRuntimeGraph<S, V, DM, IDM, E>
where
    S: solverforge_core::domain::PlanningSolution,
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CompiledRuntimeGraph")
            .field("config", &self.config)
            .finish_non_exhaustive()
    }
}

impl<S, V, DM, IDM, E> CompiledRuntimeGraph<S, V, DM, IDM, E>
where
    S: solverforge_core::domain::PlanningSolution,
{
    pub(crate) fn context(&self) -> &SearchContext<S, V, DM, IDM> {
        &self.context
    }

    pub(crate) fn config(&self) -> &SolverConfig {
        &self.config
    }

    pub(crate) fn phases(&self) -> &[CompiledRuntimePhase<S, V, DM, IDM>] {
        &self.phases
    }

    pub(crate) fn extensions(&self) -> &E {
        &self.extensions
    }

    pub(crate) fn default_bindings(&self) -> &DefaultRuntimeBindings<S, V, DM, IDM> {
        &self.default_bindings
    }
}