1use std::cmp::Ordering;
36
37use ruff_index::{Idx, IndexVec};
38use rustc_hash::FxHashMap;
39
40use crate::ast_ids::ScopedUseId;
41use crate::predicate::ScopedPredicateId;
42use crate::rank::{RankBitBox, RankBitBoxVec};
43use crate::scope::FileScopeId;
44
45#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)]
50pub struct ScopedNarrowingConstraint(u32);
51
52impl ScopedNarrowingConstraint {
53 pub const ALWAYS_TRUE: Self = Self(u32::MAX);
54 pub const ALWAYS_FALSE: Self = Self(u32::MAX - 1);
55
56 pub fn is_terminal(self) -> bool {
57 self.0 >= Self::ALWAYS_FALSE.0
58 }
59}
60
61impl Idx for ScopedNarrowingConstraint {
62 fn new(value: usize) -> Self {
63 assert!(value < Self::ALWAYS_FALSE.0 as usize);
64 #[expect(clippy::cast_possible_truncation)]
65 Self(value as u32)
66 }
67
68 fn index(self) -> usize {
69 debug_assert!(!self.is_terminal());
70 self.0 as usize
71 }
72}
73
74const ALWAYS_TRUE: ScopedNarrowingConstraint = ScopedNarrowingConstraint::ALWAYS_TRUE;
75const ALWAYS_FALSE: ScopedNarrowingConstraint = ScopedNarrowingConstraint::ALWAYS_FALSE;
76
77const MAX_INTERIOR_NODES: usize = 512 * 1024;
80
81#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)]
82pub struct InteriorNode {
83 pub atom: ScopedPredicateId,
85 pub if_true: ScopedNarrowingConstraint,
87 pub if_uncertain: ScopedNarrowingConstraint,
89 pub if_false: ScopedNarrowingConstraint,
91}
92
93#[derive(Debug, PartialEq, Eq, get_size2::GetSize)]
94pub struct NarrowingConstraints {
95 used_interiors: Box<[InteriorNode]>,
96 used_indices: Option<RankBitBox>,
97}
98
99impl NarrowingConstraints {
100 pub fn from_test_nodes(nodes: Vec<InteriorNode>) -> Self {
105 Self {
106 used_interiors: nodes.into_boxed_slice(),
107 used_indices: None,
108 }
109 }
110
111 pub(crate) fn is_empty(&self) -> bool {
112 self.used_interiors.is_empty()
113 }
114
115 pub fn get_interior_node(&self, id: ScopedNarrowingConstraint) -> InteriorNode {
116 debug_assert!(!id.is_terminal());
117 let raw_index = id.0 as usize;
118 if let Some(used_indices) = &self.used_indices {
119 debug_assert!(
120 used_indices.get_bit(raw_index).unwrap_or(false),
121 "all used narrowing constraints should have been marked as used",
122 );
123 self.used_interiors[used_indices.rank(raw_index) as usize]
124 } else {
125 self.used_interiors[raw_index]
126 }
127 }
128}
129
130#[derive(Debug, Default, PartialEq, Eq)]
131pub struct NarrowingConstraintsBuilder {
132 interiors: IndexVec<ScopedNarrowingConstraint, InteriorNode>,
133 interior_used: RankBitBoxVec,
134 interior_cache: FxHashMap<InteriorNode, ScopedNarrowingConstraint>,
135 and_cache: FxHashMap<
136 (ScopedNarrowingConstraint, ScopedNarrowingConstraint),
137 ScopedNarrowingConstraint,
138 >,
139 or_cache: FxHashMap<
140 (ScopedNarrowingConstraint, ScopedNarrowingConstraint),
141 ScopedNarrowingConstraint,
142 >,
143}
144
145impl NarrowingConstraintsBuilder {
146 pub(crate) fn build(self) -> NarrowingConstraints {
147 if self.interior_used.first_zero().is_none() {
148 NarrowingConstraints {
149 used_interiors: self.interiors.raw.into_boxed_slice(),
150 used_indices: None,
151 }
152 } else {
153 let used_interiors = self
154 .interiors
155 .into_iter()
156 .zip(&self.interior_used)
157 .filter_map(|(interior, used)| used.then_some(interior))
158 .collect();
159 let used_indices = RankBitBox::from_bits(self.interior_used);
160 NarrowingConstraints {
161 used_interiors,
162 used_indices: Some(used_indices),
163 }
164 }
165 }
166
167 pub(crate) fn mark_used(&mut self, node: ScopedNarrowingConstraint) {
168 if !node.is_terminal() && !self.interior_used[node.index()] {
169 self.interior_used.set(node.index(), true);
170 let node = self.interiors[node];
171 self.mark_used(node.if_true);
172 self.mark_used(node.if_uncertain);
173 self.mark_used(node.if_false);
174 }
175 }
176
177 fn cmp_atoms(&self, a: ScopedNarrowingConstraint, b: ScopedNarrowingConstraint) -> Ordering {
178 if a == b || (a.is_terminal() && b.is_terminal()) {
179 Ordering::Equal
180 } else if a.is_terminal() {
181 Ordering::Greater
182 } else if b.is_terminal() {
183 Ordering::Less
184 } else {
185 self.interiors[a]
186 .atom
187 .cmp(&self.interiors[b].atom)
188 .reverse()
189 }
190 }
191
192 fn add_interior(&mut self, node: InteriorNode) -> ScopedNarrowingConstraint {
193 if node.if_uncertain == ALWAYS_TRUE {
194 return ALWAYS_TRUE;
195 }
196 if node.if_true == node.if_false && node.if_true == node.if_uncertain {
197 return node.if_true;
198 }
199
200 let when_true = self.add_or_constraint(node.if_true, node.if_uncertain);
205 let when_false = self.add_or_constraint(node.if_false, node.if_uncertain);
206 if when_true == when_false {
207 return when_true;
208 }
209 if when_true == ALWAYS_TRUE
210 && !(node.if_true == ALWAYS_TRUE && node.if_false == ALWAYS_FALSE)
211 {
212 return self.add_interior(InteriorNode {
213 atom: node.atom,
214 if_true: ALWAYS_TRUE,
215 if_uncertain: when_false,
216 if_false: ALWAYS_FALSE,
217 });
218 }
219 if when_false == ALWAYS_TRUE
220 && !(node.if_true == ALWAYS_FALSE && node.if_false == ALWAYS_TRUE)
221 {
222 return self.add_interior(InteriorNode {
223 atom: node.atom,
224 if_true: ALWAYS_FALSE,
225 if_uncertain: when_true,
226 if_false: ALWAYS_TRUE,
227 });
228 }
229
230 *self.interior_cache.entry(node).or_insert_with(|| {
231 self.interior_used.push(false);
232 self.interiors.push(node)
233 })
234 }
235
236 pub(crate) fn add_atom(&mut self, predicate: ScopedPredicateId) -> ScopedNarrowingConstraint {
237 if predicate == ScopedPredicateId::ALWAYS_FALSE {
238 ALWAYS_FALSE
239 } else if predicate == ScopedPredicateId::ALWAYS_TRUE {
240 ALWAYS_TRUE
241 } else {
242 self.add_interior(InteriorNode {
243 atom: predicate,
244 if_true: ALWAYS_TRUE,
245 if_uncertain: ALWAYS_FALSE,
246 if_false: ALWAYS_FALSE,
247 })
248 }
249 }
250
251 pub(crate) fn add_negated_atom(
252 &mut self,
253 predicate: ScopedPredicateId,
254 ) -> ScopedNarrowingConstraint {
255 if predicate == ScopedPredicateId::ALWAYS_FALSE {
256 ALWAYS_TRUE
257 } else if predicate == ScopedPredicateId::ALWAYS_TRUE {
258 ALWAYS_FALSE
259 } else {
260 self.add_interior(InteriorNode {
261 atom: predicate,
262 if_true: ALWAYS_FALSE,
263 if_uncertain: ALWAYS_FALSE,
264 if_false: ALWAYS_TRUE,
265 })
266 }
267 }
268
269 pub(crate) fn add_conditional(
271 &mut self,
272 predicate: ScopedPredicateId,
273 if_true: ScopedNarrowingConstraint,
274 if_false: ScopedNarrowingConstraint,
275 ) -> ScopedNarrowingConstraint {
276 let node = InteriorNode {
277 atom: predicate,
278 if_true,
279 if_uncertain: ALWAYS_FALSE,
280 if_false,
281 };
282 if let Some(cached) = self.interior_cache.get(&node) {
283 return *cached;
284 }
285 if self.interiors.len() >= MAX_INTERIOR_NODES {
286 return ALWAYS_TRUE;
287 }
288
289 self.add_interior(node)
290 }
291
292 pub(crate) fn add_or_constraint(
293 &mut self,
294 a: ScopedNarrowingConstraint,
295 b: ScopedNarrowingConstraint,
296 ) -> ScopedNarrowingConstraint {
297 match (a, b) {
298 (ALWAYS_TRUE, _) | (_, ALWAYS_TRUE) => return ALWAYS_TRUE,
299 (ALWAYS_FALSE, other) | (other, ALWAYS_FALSE) => return other,
300 _ if a == b => return a,
301 _ => {}
302 }
303
304 let (a, b) = if b.0 < a.0 { (b, a) } else { (a, b) };
305 if let Some(cached) = self.or_cache.get(&(a, b)) {
306 return *cached;
307 }
308 if self.interiors.len() >= MAX_INTERIOR_NODES {
309 return ALWAYS_TRUE;
310 }
311
312 let result = match self.cmp_atoms(a, b) {
316 Ordering::Equal => {
317 let a_node = self.interiors[a];
318 let b_node = self.interiors[b];
319 let if_true = self.add_or_constraint(a_node.if_true, b_node.if_true);
320 let if_uncertain = self.add_or_constraint(a_node.if_uncertain, b_node.if_uncertain);
321 let if_false = self.add_or_constraint(a_node.if_false, b_node.if_false);
322 self.add_interior(InteriorNode {
323 atom: a_node.atom,
324 if_true,
325 if_uncertain,
326 if_false,
327 })
328 }
329 ordering @ (Ordering::Less | Ordering::Greater) => {
330 let (node, other) = if ordering == Ordering::Less {
331 (self.interiors[a], b)
332 } else {
333 (self.interiors[b], a)
334 };
335 let if_uncertain = self.add_or_constraint(node.if_uncertain, other);
336 self.add_interior(InteriorNode {
337 atom: node.atom,
338 if_true: node.if_true,
339 if_uncertain,
340 if_false: node.if_false,
341 })
342 }
343 };
344
345 self.or_cache.insert((a, b), result);
346 result
347 }
348
349 pub(crate) fn add_and_constraint(
350 &mut self,
351 a: ScopedNarrowingConstraint,
352 b: ScopedNarrowingConstraint,
353 ) -> ScopedNarrowingConstraint {
354 match (a, b) {
355 (ALWAYS_FALSE, _) | (_, ALWAYS_FALSE) => return ALWAYS_FALSE,
356 (ALWAYS_TRUE, other) | (other, ALWAYS_TRUE) => return other,
357 _ if a == b => return a,
358 _ => {}
359 }
360
361 let (a, b) = if b.0 < a.0 { (b, a) } else { (a, b) };
362 if let Some(cached) = self.and_cache.get(&(a, b)) {
363 return *cached;
364 }
365 if self.interiors.len() >= MAX_INTERIOR_NODES {
366 return ALWAYS_TRUE;
367 }
368
369 let result = match self.cmp_atoms(a, b) {
372 Ordering::Equal => {
373 let a_node = self.interiors[a];
374 let b_node = self.interiors[b];
375
376 let b_true_or_uncertain =
377 self.add_or_constraint(b_node.if_true, b_node.if_uncertain);
378 let true_from_a = self.add_and_constraint(a_node.if_true, b_true_or_uncertain);
379 let true_from_uncertain =
380 self.add_and_constraint(a_node.if_uncertain, b_node.if_true);
381 let if_true = self.add_or_constraint(true_from_a, true_from_uncertain);
382
383 let if_uncertain =
384 self.add_and_constraint(a_node.if_uncertain, b_node.if_uncertain);
385
386 let b_false_or_uncertain =
387 self.add_or_constraint(b_node.if_false, b_node.if_uncertain);
388 let false_from_a = self.add_and_constraint(a_node.if_false, b_false_or_uncertain);
389 let false_from_uncertain =
390 self.add_and_constraint(a_node.if_uncertain, b_node.if_false);
391 let if_false = self.add_or_constraint(false_from_a, false_from_uncertain);
392
393 self.add_interior(InteriorNode {
394 atom: a_node.atom,
395 if_true,
396 if_uncertain,
397 if_false,
398 })
399 }
400 ordering @ (Ordering::Less | Ordering::Greater) => {
401 let (node, other) = if ordering == Ordering::Less {
402 (self.interiors[a], b)
403 } else {
404 (self.interiors[b], a)
405 };
406 let if_true = self.add_and_constraint(node.if_true, other);
407 let if_uncertain = self.add_and_constraint(node.if_uncertain, other);
408 let if_false = self.add_and_constraint(node.if_false, other);
409 self.add_interior(InteriorNode {
410 atom: node.atom,
411 if_true,
412 if_uncertain,
413 if_false,
414 })
415 }
416 };
417
418 self.and_cache.insert((a, b), result);
419 result
420 }
421}
422
423#[derive(Clone, Copy, Debug, Eq, PartialEq)]
424pub enum ConstraintKey {
425 NarrowingConstraint(ScopedNarrowingConstraint),
426 NestedScope(FileScopeId),
427 UseId(ScopedUseId),
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 fn predicate(index: usize) -> ScopedPredicateId {
435 ScopedPredicateId::new(index)
436 }
437
438 fn evaluate(
439 constraints: &NarrowingConstraintsBuilder,
440 constraint: ScopedNarrowingConstraint,
441 values: &[bool],
442 ) -> bool {
443 match constraint {
444 ALWAYS_TRUE => true,
445 ALWAYS_FALSE => false,
446 _ => {
447 let node = constraints.interiors[constraint];
448 evaluate(constraints, node.if_uncertain, values)
449 || if values[node.atom.index()] {
450 evaluate(constraints, node.if_true, values)
451 } else {
452 evaluate(constraints, node.if_false, values)
453 }
454 }
455 }
456 }
457
458 #[test]
459 fn boolean_operations_match_their_truth_tables() {
460 let mut constraints = NarrowingConstraintsBuilder::default();
461 let a = constraints.add_atom(predicate(0));
462 let b = constraints.add_atom(predicate(1));
463
464 let a_or_b = constraints.add_or_constraint(a, b);
465 let not_c = constraints.add_negated_atom(predicate(2));
466 let formula = constraints.add_and_constraint(a_or_b, not_c);
467
468 for mask in 0_u8..8 {
469 let values = [mask & 0b001 != 0, mask & 0b010 != 0, mask & 0b100 != 0];
470 assert_eq!(
471 evaluate(&constraints, formula, &values),
472 (values[0] || values[1]) && !values[2],
473 );
474 }
475 }
476
477 #[test]
478 fn union_parks_the_other_operand_in_the_uncertain_branch() {
479 let mut constraints = NarrowingConstraintsBuilder::default();
480 let a = constraints.add_atom(predicate(0));
481 let b = constraints.add_atom(predicate(1));
482
483 let union = constraints.add_or_constraint(a, b);
484 let root = constraints.interiors[union];
485
486 assert_eq!(root.atom, predicate(1));
487 assert_eq!(root.if_true, ALWAYS_TRUE);
488 assert_eq!(root.if_uncertain, a);
489 assert_eq!(root.if_false, ALWAYS_FALSE);
490 }
491
492 #[test]
493 fn absorption_drops_failed_check_when_preceding_branch_reaches_merge() {
494 let mut constraints = NarrowingConstraintsBuilder::default();
495 let a = constraints.add_atom(predicate(0));
496 let b = constraints.add_atom(predicate(1));
497 let not_a = constraints.add_negated_atom(predicate(0));
498 let later_branch = constraints.add_and_constraint(not_a, b);
499
500 let merged = constraints.add_or_constraint(a, later_branch);
501 let root = constraints.interiors[merged];
502
503 assert_eq!(root.atom, predicate(1));
504 assert_eq!(root.if_true, ALWAYS_TRUE);
505 assert_eq!(root.if_uncertain, a);
506 assert_eq!(root.if_false, ALWAYS_FALSE);
507
508 for mask in 0_u8..4 {
509 let values = [mask & 0b01 != 0, mask & 0b10 != 0];
510 assert_eq!(
511 evaluate(&constraints, merged, &values),
512 values[0] || values[1]
513 );
514 }
515 }
516
517 #[test]
518 fn absorption_keeps_common_failed_check_from_terminal_branch() {
519 let mut constraints = NarrowingConstraintsBuilder::default();
520 let not_a = constraints.add_negated_atom(predicate(0));
521 let b = constraints.add_atom(predicate(1));
522 let not_b = constraints.add_negated_atom(predicate(1));
523 let c = constraints.add_atom(predicate(2));
524
525 let b_branch = constraints.add_and_constraint(not_a, b);
526 let c_branch = constraints.add_and_constraint(not_a, not_b);
527 let c_branch = constraints.add_and_constraint(c_branch, c);
528 let merged = constraints.add_or_constraint(b_branch, c_branch);
529
530 for mask in 0_u8..8 {
531 let values = [mask & 0b001 != 0, mask & 0b010 != 0, mask & 0b100 != 0];
532 assert_eq!(
533 evaluate(&constraints, merged, &values),
534 !values[0] && (values[1] || values[2]),
535 );
536 }
537 }
538}