solverforge_solver/heuristic/move/traits.rs
1// Move trait definition.
2
3use std::fmt::Debug;
4
5use solverforge_core::domain::PlanningSolution;
6use solverforge_scoring::Director;
7
8use super::MoveTabuSignature;
9use crate::stats::CandidateTraceIdentity;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct MoveAffectedEntity<'a> {
13 pub descriptor_index: usize,
14 pub entity_index: usize,
15 pub variable_name: &'a str,
16}
17
18/// A move that modifies one or more planning variables.
19///
20/// Moves are fully monomorphized for maximum performance - no boxing, no virtual dispatch.
21/// Undo is handled by move-owned typed data, not boxed director callbacks.
22///
23/// # Type Parameters
24/// * `S` - The planning solution type
25///
26/// # Implementation Notes
27/// - Moves should be lightweight
28/// - Moves are NEVER cloned - ownership transfers via arena indices
29/// - Methods are generic over D to allow use with concrete directors
30pub trait Move<S: PlanningSolution>: Send + Sync + Debug {
31 type Undo: Send;
32
33 /* Returns true if this move can be executed in the current state.
34
35 A move is not doable if:
36 - The source value equals the destination value (no change)
37 - The move's own structural and value-domain rules reject it
38 - The move would violate hard constraints that can be detected early
39 */
40 fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool;
41
42 /* Executes this move, modifying the working solution.
43
44 This method modifies the planning variables through the score director.
45 Returns concrete undo data that can be passed back to `undo_move`.
46 */
47 fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo;
48
49 fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo);
50
51 /* Trial path with a retained solve scratch pool of retired undo
52 journals. The default ignores the pool and delegates to the plain pair;
53 long-journal families override both to recycle undo storage across
54 trials, so arbitrary-long edit sets do not grow the heap per trial. */
55 fn do_move_with_scratch<D: Director<S>>(
56 &self,
57 score_director: &mut D,
58 journal_pool: &mut Vec<Self::Undo>,
59 ) -> Self::Undo {
60 let _ = journal_pool;
61 self.do_move(score_director)
62 }
63
64 fn undo_move_with_scratch<D: Director<S>>(
65 &self,
66 score_director: &mut D,
67 undo: Self::Undo,
68 journal_pool: &mut Vec<Self::Undo>,
69 ) {
70 let _ = journal_pool;
71 self.undo_move(score_director, undo)
72 }
73
74 /* Doability check that may run an explicit working preview on the LIVE
75 director.
76
77 Default: identical to `is_doable` with the director reborrowed shared.
78 Sequential/compound families override this where their later legs read
79 the earlier legs' effects; the phase trial path calls THIS variant so the
80 preview mutation is owned by the phase-level orchestration instead of a
81 clone-out substitute director.
82 */
83 fn is_doable_with_working_preview<D: Director<S>>(&self, score_director: &mut D) -> bool {
84 self.is_doable(score_director)
85 }
86
87 fn descriptor_index(&self) -> usize;
88
89 fn entity_indices(&self) -> &[usize];
90
91 fn variable_name(&self) -> &str;
92
93 fn telemetry_label(&self) -> &'static str {
94 "move"
95 }
96
97 fn requires_hard_improvement(&self) -> bool {
98 false
99 }
100
101 fn requires_score_improvement(&self) -> bool {
102 false
103 }
104
105 fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature;
106
107 /* Borrows this move's prebuilt signature instead of building a fresh one.
108
109 Moves that own their completed signature (static compositions) return
110 `Some`. Dynamic builders return `None`, and callers fall back to
111 `tabu_signature`. */
112 fn borrowed_tabu_signature(&self) -> Option<&MoveTabuSignature> {
113 None
114 }
115
116 /// Returns the canonical identity used by opt-in candidate-pull tracing.
117 ///
118 /// Returns an explicit logical-coordinate identity for candidate tracing.
119 ///
120 /// This intentionally does *not* derive from tabu metadata: tabu keys may
121 /// contain representation-specific hashes. A move family that has not
122 /// supplied a cross-representation coordinate grammar returns `None`, and
123 /// the resulting trace is explicitly non-qualifying rather than faking a
124 /// comparable key.
125 fn candidate_trace_identity(&self) -> Option<CandidateTraceIdentity> {
126 None
127 }
128
129 /// Visits every entity this move changes, passing a concrete visitor by
130 /// value. `V` stays monomorphized end to end: no `dyn` dispatch, no
131 /// intermediate collection, every affected entity reported exactly once
132 /// in the move's own canonical order.
133 fn for_each_affected_entity<V: FnMut(MoveAffectedEntity<'_>)>(&self, visitor: &mut V) {
134 let descriptor_index = self.descriptor_index();
135 let variable_name = self.variable_name();
136 for &entity_index in self.entity_indices() {
137 visitor(MoveAffectedEntity {
138 descriptor_index,
139 entity_index,
140 variable_name,
141 });
142 }
143 }
144}