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
//! Core constraint trait and supporting types.
use Debug;
use crateDomain;
use crateVariable;
/// Unique variable identifier (index into the variable array).
pub type VarId = u32;
/// Result of running `revise` on a constraint.
/// Thread-safety marker for `Constraint`, scoped to the `py` feature.
///
/// The `pyo3::Python::allow_threads` boundary — compiled **only** under the
/// `py` feature — releases the GIL and runs the solver off-thread, which
/// requires the whole `Csp<D>` (and therefore every `Box<dyn Constraint<D>>`
/// it holds) to be `Send`. That requirement is real *only* at the py boundary.
///
/// Making `Send + Sync` an unconditional supertrait of `Constraint` (the prior
/// shape) rejected `!Send`/`!Sync` external implementors — concretely
/// bbnf-lang's `RefConstraint`, which holds an `Rc<RefCell<…>>` obligation
/// sink (E0277). The old "non-breaking, purely additive" claim was therefore
/// false: it broke a real downstream consumer.
///
/// This marker carries `Send + Sync` **under `py`** and is **vacuous
/// otherwise**, so:
/// * `--features py` → `dyn Constraint<D>: Send + Sync` transitively via the
/// supertrait chain, hence `Csp<D>: Send` and `allow_threads` compiles; a
/// `!Send` constraint is rejected loudly at its `impl` site (fail-explicit,
/// never a silent downgrade).
/// * default build (bbnf's) → the bound is inert; `RefConstraint` and every
/// other lattice/reference constraint compiles unchanged, zero bbnf edits.
///
/// The two configurations are kept honest by the sync gate, which compiles
/// BOTH (`--features py` and the default). See `scripts/sync-csp-solver-vendor`.
/// A constraint over one or more CSP variables.
/// Default unary revision: prune every domain value unsupported by
/// `check()` in isolation. Iterates the domain's owned snapshot directly
/// (`Domain::iter`'s `+ use<Self>` bound), no heap `Vec`. A free function
/// (not a trait method) so it doesn't grow `Constraint`'s public surface —
/// it's purely `revise`'s own dispatch target.
///
/// `changed` folds in [`Variable::prune`]'s bool: a pruned value that was
/// already absent (or that the domain refuses to remove) does not count as
/// a revision. See `revise_binary_default` for why this honesty is
/// load-bearing rather than cosmetic.
Sized>
/// Default binary revision: the "change-mask" rewrite of the previous
/// 5-`Vec` implementation (one `vars.len()`-sized `assignment`, plus four
/// `domain.values()` snapshots — `vals_i`/`vals_j` each collected twice,
/// once per direction pass, per the Pass-1 constraint audit's F5).
///
/// The `assignment` scratch is kept (its size is dictated by
/// `Constraint::check`'s existing `&[Option<D::Value>]` contract, indexed
/// by absolute `VarId` — narrowing it would be a breaking signature change
/// reaching every hand-rolled `revise`/`check` in bbnf-lang's 11+ `Custom`
/// constraint types). The four domain snapshots are eliminated: each pass
/// calls `domain.iter()` fresh inside the loop instead of pre-collecting
/// into a `Vec` — for `BitsetDomain` (the crate's only production domain)
/// re-deriving the iterator is a cheap `u128` copy, not a re-scan, so this
/// costs nothing extra while dropping 4 allocations to 0. The second
/// pass's `vals_i` must still reflect pass 1's pruning (correctness-
/// critical: pass 1 may have narrowed `xi`), which a live
/// `vars[xi].domain.iter()` gives for free — no explicit re-collection
/// needed.
///
/// # Reporting `Changed` honestly (the lattice-hang repair)
///
/// `changed` folds in [`Variable::prune`]'s bool return rather than being
/// set unconditionally in the unsupported branch. On a finite domain this
/// is behavior-neutral — the enumerate loop only ever prunes a value the
/// domain currently holds, so `prune` always removes it and returns
/// `true`. On a **lattice** domain it is the difference between converging
/// and hanging: `BitsetLatticeDomain::remove` is a no-op (lattice values
/// only grow via `join`, never shrink), so every `prune` returns `false`
/// and this arc-consistency revise legitimately changes nothing. The old
/// unconditional `changed = true` reported `Revision::Changed` while
/// pruning nothing — a lie that made AC-3 (and the monotonic sweep)
/// re-enqueue the same arcs forever on a disjoint-seed lattice chain (W1
/// bench-attribution F1). Honoring the bool lets the worklist drain.
/// A check-based constraint cannot *grow* a lattice domain regardless;
/// genuine lattice propagators supply their own join-based `revise`.
Sized>