1use super::{
2 AtomKind, RewriteError, RuleFailureInvalidation, RulePrefilter, RuleSet,
3 resolve_rules::RuleData,
4};
5use crate::{
6 Model,
7 ast::{
8 Atom, Expression as Expr, ExpressionArena, ExpressionNodeId, Metadata, Name,
9 discriminant_from_value, finish_root_evaluator_normalisation, normalise_evaluator_local,
10 normalise_root_constraint_deep,
11 },
12 bug,
13 domain_tightening::tighten_domains_from_constraints,
14 objective::introduce_objective_auxiliary,
15 rule_engine::{
16 get_rules_grouped,
17 rewriter_common::{
18 NoopObserver, RuleAttemptObserver, RuleAttemptStatus, RuleResult,
19 VariableDeclarationSnapshot, choose_rule_result_index, log_rule_application,
20 root_variable_snapshot_for_default_trace, snapshot_symbols_after_effect,
21 try_rewrite_value_letting_once,
22 },
23 },
24 settings::{
25 RewriteConfig, Rewriter, default_rule_trace_enabled, rule_attempt_trace_enabled,
26 rule_trace_enabled, set_current_rewriter,
27 },
28 stats::RewriterStats,
29};
30
31use itertools::Itertools;
32use std::{
33 cmp::Ordering,
34 collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet},
35 fmt::Write as FmtWrite,
36 fs::{self, OpenOptions},
37 io::Write as IoWrite,
38 path::PathBuf,
39 time::Instant,
40};
41use tracing::trace;
42
43#[cfg(debug_assertions)]
63use crate::ast::assertions::debug_assert_model_well_formed;
64
65type ApplicableRule<'a, CtxFnType> = (
66 RuleResult<'a>,
67 usize,
68 Expr,
69 CtxFnType,
70 Option<VariableDeclarationSnapshot>,
71);
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74enum ScheduledMode {
75 CheckNode,
77 TraverseSubtreeRoot,
79 TraverseSubtreeDescendant,
81}
82
83impl ScheduledMode {
84 fn includes(self, other: Self) -> bool {
85 matches!(
86 (self, other),
87 (ScheduledMode::TraverseSubtreeRoot, ScheduledMode::CheckNode)
88 | (
89 ScheduledMode::TraverseSubtreeRoot,
90 ScheduledMode::TraverseSubtreeRoot
91 )
92 | (
93 ScheduledMode::TraverseSubtreeRoot,
94 ScheduledMode::TraverseSubtreeDescendant
95 )
96 | (
97 ScheduledMode::TraverseSubtreeDescendant,
98 ScheduledMode::CheckNode
99 )
100 | (
101 ScheduledMode::TraverseSubtreeDescendant,
102 ScheduledMode::TraverseSubtreeDescendant
103 )
104 | (ScheduledMode::CheckNode, ScheduledMode::CheckNode)
105 )
106 }
107
108 fn descends_on_failure(self) -> bool {
109 matches!(
110 self,
111 ScheduledMode::TraverseSubtreeRoot | ScheduledMode::TraverseSubtreeDescendant
112 )
113 }
114
115 fn next_self_mode(self) -> Option<Self> {
116 match self {
117 ScheduledMode::CheckNode => Some(ScheduledMode::CheckNode),
118 ScheduledMode::TraverseSubtreeRoot => Some(ScheduledMode::TraverseSubtreeRoot),
119 ScheduledMode::TraverseSubtreeDescendant => None,
120 }
121 }
122
123 fn advances_as_subtree(self) -> bool {
124 matches!(self, ScheduledMode::TraverseSubtreeRoot)
125 }
126}
127
128#[derive(Default, Debug)]
129struct WorklistModeCounts {
130 check_node: usize,
131 traverse_subtree: usize,
132}
133
134impl WorklistModeCounts {
135 fn increment(&mut self, mode: ScheduledMode) {
136 self.add(mode, 1);
137 }
138
139 fn add(&mut self, mode: ScheduledMode, value: usize) {
140 match mode {
141 ScheduledMode::CheckNode => self.check_node += value,
142 ScheduledMode::TraverseSubtreeRoot | ScheduledMode::TraverseSubtreeDescendant => {
143 self.traverse_subtree += value
144 }
145 }
146 }
147}
148
149#[derive(Clone, Copy, Debug)]
150enum WorklistStaleReason {
151 ModeMismatch,
152 MissingSurface,
153 InactiveSurface,
154 UnreachableNode,
155 GenerationMismatch,
156}
157
158#[derive(Default, Debug)]
159struct WorklistStaleReasonCounts {
160 mode_mismatch: WorklistModeCounts,
161 missing_surface: WorklistModeCounts,
162 inactive_surface: WorklistModeCounts,
163 unreachable_node: WorklistModeCounts,
164 generation_mismatch: WorklistModeCounts,
165}
166
167impl WorklistStaleReasonCounts {
168 fn increment(&mut self, reason: WorklistStaleReason, mode: ScheduledMode) {
169 match reason {
170 WorklistStaleReason::ModeMismatch => self.mode_mismatch.increment(mode),
171 WorklistStaleReason::MissingSurface => self.missing_surface.increment(mode),
172 WorklistStaleReason::InactiveSurface => self.inactive_surface.increment(mode),
173 WorklistStaleReason::UnreachableNode => self.unreachable_node.increment(mode),
174 WorklistStaleReason::GenerationMismatch => self.generation_mismatch.increment(mode),
175 }
176 }
177}
178
179#[derive(Default)]
180struct DirtyTrace {
181 enabled: bool,
182 destination: DirtyTraceDestination,
183 passes: usize,
184 priority_scans: usize,
185 expression_visits: usize,
186 attempted_expressions: usize,
187 rule_attempts: usize,
188 rewrites: usize,
189 value_letting_rewrites: usize,
190 side_effects_kept_in_arena: usize,
191 replacement_subtree_clears: usize,
192 worklist_enqueues: usize,
193 worklist_pops: usize,
194 worklist_stale_pops: usize,
195 worklist_enqueues_by_mode: WorklistModeCounts,
196 worklist_pops_by_mode: WorklistModeCounts,
197 worklist_stale_pops_by_mode: WorklistModeCounts,
198 worklist_stale_pops_by_reason: WorklistStaleReasonCounts,
199 worklist_no_candidate_pops_by_mode: WorklistModeCounts,
200 worklist_rule_attempt_pops_by_mode: WorklistModeCounts,
201 worklist_child_descents_by_mode: WorklistModeCounts,
202 rule_attempts_by_priority: BTreeMap<u16, usize>,
203 rule_attempts_by_rule: BTreeMap<String, usize>,
204 rewrites_by_rule: BTreeMap<String, usize>,
205 side_effect_rewrites_by_rule: BTreeMap<String, usize>,
206}
207
208#[derive(Default, Debug, PartialEq, Eq)]
209enum DirtyTraceDestination {
210 #[default]
211 Stderr,
212 File(PathBuf),
213 Directory(PathBuf),
214}
215
216impl DirtyTrace {
217 fn from_env() -> Self {
218 let Some(destination) = std::env::var_os("CONJURE_DIRTY_TRACE") else {
219 return Self::default();
220 };
221
222 Self {
223 enabled: true,
224 destination: dirty_trace_destination_from_env_value(destination),
225 ..Self::default()
226 }
227 }
228
229 fn record_rewrite(&mut self, rule_name: &str, side_effects: bool) {
230 if !self.enabled {
231 return;
232 }
233 self.rewrites += 1;
234 *self
235 .rewrites_by_rule
236 .entry(rule_name.to_owned())
237 .or_default() += 1;
238 if side_effects {
239 *self
240 .side_effect_rewrites_by_rule
241 .entry(rule_name.to_owned())
242 .or_default() += 1;
243 }
244 }
245
246 fn record_side_effect_kept_in_arena(&mut self) {
247 if !self.enabled {
248 return;
249 }
250 self.side_effects_kept_in_arena += 1;
251 }
252
253 fn record_rule_attempt(&mut self, priority: u16, rule_name: &str) {
254 self.rule_attempts += 1;
255 if !self.enabled {
256 return;
257 }
258 *self.rule_attempts_by_priority.entry(priority).or_default() += 1;
259 *self
260 .rule_attempts_by_rule
261 .entry(rule_name.to_owned())
262 .or_default() += 1;
263 }
264
265 fn record_worklist_enqueue(&mut self, mode: ScheduledMode) {
266 if !self.enabled {
267 return;
268 }
269 self.worklist_enqueues += 1;
270 self.worklist_enqueues_by_mode.increment(mode);
271 }
272
273 fn record_worklist_pop(&mut self, mode: ScheduledMode) {
274 if !self.enabled {
275 return;
276 }
277 self.worklist_pops += 1;
278 self.worklist_pops_by_mode.increment(mode);
279 }
280
281 fn record_worklist_stale_pop(&mut self, mode: ScheduledMode, reason: WorklistStaleReason) {
282 if !self.enabled {
283 return;
284 }
285 self.worklist_stale_pops += 1;
286 self.worklist_stale_pops_by_mode.increment(mode);
287 self.worklist_stale_pops_by_reason.increment(reason, mode);
288 }
289
290 fn record_worklist_no_candidate_pop(&mut self, mode: ScheduledMode) {
291 if !self.enabled {
292 return;
293 }
294 self.worklist_no_candidate_pops_by_mode.increment(mode);
295 }
296
297 fn record_worklist_rule_attempt_pop(&mut self, mode: ScheduledMode) {
298 if !self.enabled {
299 return;
300 }
301 self.worklist_rule_attempt_pops_by_mode.increment(mode);
302 }
303
304 fn record_worklist_child_descent(&mut self, mode: ScheduledMode, children: usize) {
305 if !self.enabled {
306 return;
307 }
308 self.worklist_child_descents_by_mode.add(mode, children);
309 }
310
311 fn finish(&self, stats: &RewriterStats) {
312 if !self.enabled {
313 return;
314 }
315
316 let mut output = String::new();
317 writeln!(output, "[dirty-trace] passes={}", self.passes).unwrap();
318 writeln!(
319 output,
320 "[dirty-trace] priority_scans={}",
321 self.priority_scans
322 )
323 .unwrap();
324 writeln!(
325 output,
326 "[dirty-trace] expression_visits={}",
327 self.expression_visits
328 )
329 .unwrap();
330 writeln!(
331 output,
332 "[dirty-trace] attempted_expressions={}",
333 self.attempted_expressions
334 )
335 .unwrap();
336 writeln!(
337 output,
338 "[dirty-trace] rule_attempts_counted={}",
339 self.rule_attempts
340 )
341 .unwrap();
342 writeln!(
343 output,
344 "[dirty-trace] stats_rule_attempts={}",
345 stats.rewriter_rule_application_attempts.unwrap_or(0)
346 )
347 .unwrap();
348 writeln!(output, "[dirty-trace] rewrites={}", self.rewrites).unwrap();
349 writeln!(
350 output,
351 "[dirty-trace] value_letting_rewrites={}",
352 self.value_letting_rewrites
353 )
354 .unwrap();
355 writeln!(
356 output,
357 "[dirty-trace] side_effects_kept_in_arena={}",
358 self.side_effects_kept_in_arena
359 )
360 .unwrap();
361 writeln!(
362 output,
363 "[dirty-trace] replacement_subtree_clears={}",
364 self.replacement_subtree_clears
365 )
366 .unwrap();
367 writeln!(
368 output,
369 "[dirty-trace] rule_attempts_by_priority={:?}",
370 self.rule_attempts_by_priority
371 )
372 .unwrap();
373 writeln!(
374 output,
375 "[dirty-trace] rule_attempts_by_rule={:?}",
376 self.rule_attempts_by_rule
377 )
378 .unwrap();
379 writeln!(
380 output,
381 "[dirty-trace] rewrites_by_rule={:?}",
382 self.rewrites_by_rule
383 )
384 .unwrap();
385 writeln!(
386 output,
387 "[dirty-trace] side_effect_rewrites_by_rule={:?}",
388 self.side_effect_rewrites_by_rule
389 )
390 .unwrap();
391 writeln!(
392 output,
393 "[dirty-trace] worklist_enqueues={}",
394 self.worklist_enqueues
395 )
396 .unwrap();
397 writeln!(
398 output,
399 "[dirty-trace] worklist_enqueues_by_mode={:?}",
400 self.worklist_enqueues_by_mode
401 )
402 .unwrap();
403 writeln!(output, "[dirty-trace] worklist_pops={}", self.worklist_pops).unwrap();
404 writeln!(
405 output,
406 "[dirty-trace] worklist_pops_by_mode={:?}",
407 self.worklist_pops_by_mode
408 )
409 .unwrap();
410 writeln!(
411 output,
412 "[dirty-trace] worklist_stale_pops={}",
413 self.worklist_stale_pops
414 )
415 .unwrap();
416 writeln!(
417 output,
418 "[dirty-trace] worklist_stale_pops_by_mode={:?}",
419 self.worklist_stale_pops_by_mode
420 )
421 .unwrap();
422 writeln!(
423 output,
424 "[dirty-trace] worklist_stale_pops_by_reason={:?}",
425 self.worklist_stale_pops_by_reason
426 )
427 .unwrap();
428 writeln!(
429 output,
430 "[dirty-trace] worklist_no_candidate_pops_by_mode={:?}",
431 self.worklist_no_candidate_pops_by_mode
432 )
433 .unwrap();
434 writeln!(
435 output,
436 "[dirty-trace] worklist_rule_attempt_pops_by_mode={:?}",
437 self.worklist_rule_attempt_pops_by_mode
438 )
439 .unwrap();
440 writeln!(
441 output,
442 "[dirty-trace] worklist_child_descents_by_mode={:?}",
443 self.worklist_child_descents_by_mode
444 )
445 .unwrap();
446
447 self.write_output(&output);
448 }
449
450 fn write_output(&self, output: &str) {
451 let path = match &self.destination {
452 DirtyTraceDestination::Stderr => {
453 eprint!("{output}");
454 return;
455 }
456 DirtyTraceDestination::File(path) => path.clone(),
457 DirtyTraceDestination::Directory(directory) => directory.join(format!(
458 "dirty-trace-{}.txt",
459 current_test_name_for_dirty_trace()
460 )),
461 };
462
463 if let Some(parent) = path
464 .parent()
465 .filter(|parent| !parent.as_os_str().is_empty())
466 && let Err(error) = fs::create_dir_all(parent)
467 {
468 eprintln!(
469 "[dirty-trace] failed to create trace directory {}: {error}",
470 parent.display()
471 );
472 eprint!("{output}");
473 return;
474 }
475
476 match OpenOptions::new().create(true).append(true).open(&path) {
477 Ok(mut file) => {
478 if let Err(error) = file.write_all(output.as_bytes()) {
479 eprintln!(
480 "[dirty-trace] failed to write trace file {}: {error}",
481 path.display()
482 );
483 eprint!("{output}");
484 }
485 }
486 Err(error) => {
487 eprintln!(
488 "[dirty-trace] failed to open trace file {}: {error}",
489 path.display()
490 );
491 eprint!("{output}");
492 }
493 }
494 }
495}
496
497fn dirty_trace_destination_from_env_value(
498 destination: std::ffi::OsString,
499) -> DirtyTraceDestination {
500 if destination.is_empty()
501 || destination == "1"
502 || destination
503 .to_str()
504 .is_some_and(|value| value.eq_ignore_ascii_case("true"))
505 {
506 return DirtyTraceDestination::Stderr;
507 }
508
509 let path = PathBuf::from(destination);
510 if path.is_file() {
511 return DirtyTraceDestination::File(path);
512 }
513
514 if path.is_dir() || path.extension().is_none() {
518 DirtyTraceDestination::Directory(path)
519 } else {
520 DirtyTraceDestination::File(path)
521 }
522}
523
524fn current_test_name_for_dirty_trace() -> String {
525 let current_thread = std::thread::current();
526 let name = current_thread
527 .name()
528 .map(ToOwned::to_owned)
529 .unwrap_or_else(|| format!("pid-{}", std::process::id()));
530 sanitize_dirty_trace_filename(&name)
531}
532
533fn sanitize_dirty_trace_filename(name: &str) -> String {
534 let sanitized = name
535 .chars()
536 .map(|character| {
537 if character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.') {
538 character
539 } else {
540 '-'
541 }
542 })
543 .collect::<String>();
544
545 let sanitized = sanitized.trim_matches('-');
546 if sanitized.is_empty() {
547 "unknown".to_string()
548 } else {
549 sanitized.to_string()
550 }
551}
552
553#[derive(Clone)]
554struct RuleGroup<'a> {
555 priority: u16,
556 rules: Vec<RuleData<'a>>,
557 rules_by_discriminant: Vec<Option<Vec<RuleData<'a>>>>,
559 universal_rules: Vec<RuleData<'a>>,
560 has_non_discriminant_filters: bool,
561}
562
563enum CandidateRules<'group, 'rules> {
564 Slice(std::slice::Iter<'group, RuleData<'rules>>),
565 Filtered {
566 iter: std::slice::Iter<'group, RuleData<'rules>>,
567 expr: &'group Expr,
568 arena_node: Option<(&'group ExpressionArena, ExpressionNodeId)>,
569 include_universal: bool,
570 },
571}
572
573impl<'group, 'rules> Iterator for CandidateRules<'group, 'rules> {
574 type Item = &'group RuleData<'rules>;
575
576 fn next(&mut self) -> Option<Self::Item> {
577 match self {
578 CandidateRules::Slice(iter) => iter.next(),
579 CandidateRules::Filtered {
580 iter,
581 expr,
582 arena_node,
583 include_universal,
584 } => loop {
585 let rule_data = iter.next()?;
586 if rule_matches_specific_prefilter(rule_data, expr, *arena_node)
587 || (*include_universal && rule_is_universal(rule_data))
588 {
589 return Some(rule_data);
590 }
591 },
592 }
593 }
594}
595
596impl<'a> RuleGroup<'a> {
597 fn new(priority: u16, rules: Vec<RuleData<'a>>) -> Self {
598 let discriminants = rules
599 .iter()
600 .filter_map(|rd| rd.rule.prefilters)
601 .flatten()
602 .filter_map(|prefilter| match prefilter {
603 RulePrefilter::Variant(discriminant) => Some(*discriminant),
604 RulePrefilter::Child { .. }
605 | RulePrefilter::VariantChild { .. }
606 | RulePrefilter::Atom(_)
607 | RulePrefilter::ChildAtom(_) => None,
608 })
609 .collect_vec();
610
611 let mut rules_by_discriminant = Vec::new();
612 if let Some(max_discriminant) = discriminants.iter().copied().max() {
613 rules_by_discriminant.resize_with(max_discriminant + 1, || None);
614 }
615
616 let target_discriminants = discriminants.into_iter().unique().collect_vec();
617
618 for &discriminant in &target_discriminants {
619 rules_by_discriminant[discriminant] = Some(
620 rules
621 .iter()
622 .filter(|rd| {
623 rule_is_universal(rd) || rule_matches_self_discriminant(rd, discriminant)
624 })
625 .cloned()
626 .collect(),
627 );
628 }
629
630 let universal_rules: Vec<RuleData<'a>> = rules
631 .iter()
632 .filter(|rd| rule_is_universal(rd))
633 .cloned()
634 .collect();
635 let has_non_discriminant_filters = rules.iter().any(|rd| {
636 rd.rule.prefilters.is_some_and(|prefilters| {
637 prefilters
638 .iter()
639 .any(|prefilter| !matches!(prefilter, RulePrefilter::Variant(_)))
640 })
641 });
642 Self {
643 priority,
644 rules,
645 rules_by_discriminant,
646 universal_rules,
647 has_non_discriminant_filters,
648 }
649 }
650
651 fn candidates<'group>(
652 &'group self,
653 config: RewriteConfig,
654 expr: &'group Expr,
655 ) -> CandidateRules<'group, 'a> {
656 self.candidates_with_arena(config, expr, None)
657 }
658
659 fn candidates_at_node<'group>(
660 &'group self,
661 config: RewriteConfig,
662 arena: &'group ExpressionArena,
663 node_id: ExpressionNodeId,
664 ) -> CandidateRules<'group, 'a> {
665 self.candidates_with_arena(config, arena.expression(node_id), Some((arena, node_id)))
666 }
667
668 fn candidates_with_arena<'group>(
669 &'group self,
670 config: RewriteConfig,
671 expr: &'group Expr,
672 arena_node: Option<(&'group ExpressionArena, ExpressionNodeId)>,
673 ) -> CandidateRules<'group, 'a> {
674 if !config.prefilter {
675 return CandidateRules::Slice(self.rules.iter());
676 }
677
678 if self.has_non_discriminant_filters {
679 return CandidateRules::Filtered {
685 iter: self.rules.iter(),
686 expr,
687 arena_node,
688 include_universal: true,
689 };
690 }
691
692 let discriminant = discriminant_from_value(expr);
693 CandidateRules::Slice(
694 self.rules_by_discriminant
695 .get(discriminant)
696 .and_then(Option::as_deref)
697 .unwrap_or(&self.universal_rules)
698 .iter(),
699 )
700 }
701
702 #[cfg(test)]
703 fn has_candidates(&self, config: RewriteConfig, expr: &Expr) -> bool {
704 self.has_candidates_with_arena(config, expr, None)
705 }
706
707 fn has_candidates_at_node(
708 &self,
709 config: RewriteConfig,
710 arena: &ExpressionArena,
711 node_id: ExpressionNodeId,
712 ) -> bool {
713 self.has_candidates_with_arena(config, arena.expression(node_id), Some((arena, node_id)))
714 }
715
716 fn has_candidates_with_arena(
717 &self,
718 config: RewriteConfig,
719 expr: &Expr,
720 arena_node: Option<(&ExpressionArena, ExpressionNodeId)>,
721 ) -> bool {
722 if !config.prefilter {
723 return !self.rules.is_empty();
724 }
725
726 if self.has_non_discriminant_filters {
727 return !self.universal_rules.is_empty()
728 || self
729 .rules
730 .iter()
731 .any(|rule_data| rule_matches_specific_prefilter(rule_data, expr, arena_node));
732 }
733
734 let discriminant = discriminant_from_value(expr);
735 !self
736 .rules_by_discriminant
737 .get(discriminant)
738 .and_then(Option::as_deref)
739 .unwrap_or(&self.universal_rules)
740 .is_empty()
741 }
742}
743
744#[derive(Clone, Debug)]
745enum RewriteSurfaceKind {
746 Root,
747 ValueLetting { name: Name },
748}
749
750struct RewriteSurface {
751 kind: RewriteSurfaceKind,
752 arena: ExpressionArena,
753 active: bool,
754}
755
756impl RewriteSurface {
757 fn root(arena: ExpressionArena) -> Self {
758 Self {
759 kind: RewriteSurfaceKind::Root,
760 arena,
761 active: true,
762 }
763 }
764
765 fn value_letting(name: Name, expr: Expr) -> Self {
766 Self {
767 kind: RewriteSurfaceKind::ValueLetting { name },
768 arena: ExpressionArena::from_root(expr),
769 active: true,
770 }
771 }
772}
773
774#[derive(Clone, Copy, Debug)]
775struct ScheduledNode {
776 surface: usize,
777 node_id: ExpressionNodeId,
778 generation: u32,
779 mode: ScheduledMode,
780 depth: usize,
781 sequence: u64,
782}
783
784impl PartialEq for ScheduledNode {
785 fn eq(&self, other: &Self) -> bool {
786 self.depth == other.depth && self.sequence == other.sequence
787 }
788}
789
790impl Eq for ScheduledNode {}
791
792impl PartialOrd for ScheduledNode {
793 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
794 Some(self.cmp(other))
795 }
796}
797
798impl Ord for ScheduledNode {
799 fn cmp(&self, other: &Self) -> Ordering {
800 other
801 .depth
802 .cmp(&self.depth)
803 .then_with(|| other.sequence.cmp(&self.sequence))
804 }
805}
806
807#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
808struct ScheduledKey {
809 level: usize,
810 surface: usize,
811 node_id: ExpressionNodeId,
812 generation: u32,
813}
814
815#[derive(Clone, Copy, Debug, Default)]
820struct NodeSubtreeCandidateCache {
821 generation: u32,
822 generation_valid: bool,
823 known_levels: u128,
824 candidate_levels: u128,
825}
826
827#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
831struct FailedSymbolRuleKey {
832 surface: usize,
833 node_id: ExpressionNodeId,
834 rule: usize,
835}
836
837#[derive(Clone, Copy)]
838struct WorklistSchedulingContext<'arena, 'groups, 'rules> {
839 arena: &'arena ExpressionArena,
840 surface: usize,
841 rule_groups: &'groups [RuleGroup<'rules>],
842 config: RewriteConfig,
843}
844
845impl<'arena, 'groups, 'rules> WorklistSchedulingContext<'arena, 'groups, 'rules> {
846 fn new(
847 arena: &'arena ExpressionArena,
848 surface: usize,
849 rule_groups: &'groups [RuleGroup<'rules>],
850 config: RewriteConfig,
851 ) -> Self {
852 Self {
853 arena,
854 surface,
855 rule_groups,
856 config,
857 }
858 }
859}
860
861struct WorklistScheduler {
862 queues_by_level: Vec<BinaryHeap<ScheduledNode>>,
867 scheduled: HashMap<ScheduledKey, ScheduledMode>,
868 subtree_candidate_cache: Vec<Vec<NodeSubtreeCandidateCache>>,
874 failed_symbol_rules: HashMap<FailedSymbolRuleKey, u64>,
877 reference_nodes_by_name: HashMap<Name, HashSet<(usize, ExpressionNodeId)>>,
883 symbol_revision: u64,
884 next_sequence: u64,
885}
886
887#[derive(Default)]
893struct DeferredEvaluatorWork {
894 nodes: HashSet<(usize, ExpressionNodeId)>,
895}
896
897impl DeferredEvaluatorWork {
898 fn defer_ancestors(
899 &mut self,
900 arena: &ExpressionArena,
901 surface: usize,
902 node_id: ExpressionNodeId,
903 ) {
904 let mut current = arena.parent(node_id);
905 while let Some(current_id) = current {
906 self.nodes.insert((surface, current_id));
907 current = arena.parent(current_id);
908 }
909 }
910
911 fn is_empty(&self) -> bool {
912 self.nodes.is_empty()
913 }
914}
915
916#[derive(Default)]
924struct DeferredAncestorRuleWork {
925 nodes: HashSet<(usize, ExpressionNodeId)>,
926 postponed: Vec<(usize, usize, ExpressionNodeId, ScheduledMode)>,
927 flush_after_level: Option<usize>,
928}
929
930impl DeferredAncestorRuleWork {
931 fn defer_ancestors(
932 &mut self,
933 arena: &ExpressionArena,
934 surface: usize,
935 node_id: ExpressionNodeId,
936 rewrite_level: usize,
937 ) {
938 self.flush_after_level = Some(
939 self.flush_after_level
940 .map_or(rewrite_level, |level| level.min(rewrite_level)),
941 );
942
943 let mut current = arena.parent(node_id);
944 while let Some(current_id) = current {
945 self.nodes.insert((surface, current_id));
946 current = arena.parent(current_id);
947 }
948 }
949
950 fn should_flush_before(&self, next_level: Option<usize>) -> bool {
951 let Some(flush_after_level) = self.flush_after_level else {
952 return false;
953 };
954 next_level.is_none_or(|level| level > flush_after_level)
955 }
956
957 fn contains(&self, surface: usize, node_id: ExpressionNodeId) -> bool {
958 self.nodes.contains(&(surface, node_id))
959 }
960
961 fn postpone(&mut self, item: (usize, usize, ExpressionNodeId, ScheduledMode)) {
962 self.postponed.push(item);
963 }
964
965 fn flush_effect_path_before_root(
972 &mut self,
973 scheduler: &mut WorklistScheduler,
974 surfaces: &mut [RewriteSurface],
975 deferred_evaluators: &mut DeferredEvaluatorWork,
976 surface: usize,
977 node_id: ExpressionNodeId,
978 dirty_trace: &mut DirtyTrace,
979 ) {
980 let Some(rewrite_surface) = surfaces.get(surface) else {
981 return;
982 };
983 let root = rewrite_surface.arena.root();
984 let mut path = Vec::new();
985 let mut current = rewrite_surface.arena.parent(node_id);
986 while let Some(current_id) = current {
987 if current_id == root {
988 break;
989 }
990 path.push(current_id);
991 current = rewrite_surface.arena.parent(current_id);
992 }
993
994 for ancestor in path {
995 self.nodes.remove(&(surface, ancestor));
996 let Some(rewrite_surface) = surfaces.get_mut(surface) else {
997 continue;
998 };
999 if !rewrite_surface.active || !rewrite_surface.arena.is_reachable(ancestor) {
1000 continue;
1001 }
1002
1003 rewrite_surface
1004 .arena
1005 .rebuild_payload_from_children(ancestor);
1006 let evaluator_changed = if matches!(rewrite_surface.kind, RewriteSurfaceKind::Root) {
1007 deferred_evaluators.nodes.remove(&(surface, ancestor));
1008 normalise_evaluator_node_to_fixpoint_without_ancestor_sync(
1009 &mut rewrite_surface.arena,
1010 ancestor,
1011 dirty_trace,
1012 )
1013 } else {
1014 false
1015 };
1016
1017 if evaluator_changed {
1018 scheduler.index_subtree_references(&rewrite_surface.arena, surface, ancestor);
1019 scheduler.enqueue_subtree(
1020 &rewrite_surface.arena,
1021 surface,
1022 ancestor,
1023 Some(dirty_trace),
1024 );
1025 } else {
1026 scheduler.enqueue_node_at_level(
1027 &rewrite_surface.arena,
1028 surface,
1029 ancestor,
1030 0,
1031 ScheduledMode::CheckNode,
1032 Some(dirty_trace),
1033 );
1034 }
1035 }
1036 }
1037
1038 fn flush(
1039 &mut self,
1040 scheduler: &mut WorklistScheduler,
1041 surfaces: &mut [RewriteSurface],
1042 deferred_evaluators: &mut DeferredEvaluatorWork,
1043 dirty_trace: &mut DirtyTrace,
1044 ) {
1045 let nodes = deferred_nodes_in_bottom_up_order(std::mem::take(&mut self.nodes), surfaces);
1046 let postponed = std::mem::take(&mut self.postponed);
1047 self.flush_after_level = None;
1048
1049 let mut evaluator_changed = HashSet::new();
1055 for &(surface, node_id, _) in &nodes {
1056 let Some(rewrite_surface) = surfaces.get_mut(surface) else {
1057 continue;
1058 };
1059 if !rewrite_surface.active || !rewrite_surface.arena.is_reachable(node_id) {
1060 continue;
1061 }
1062 rewrite_surface.arena.rebuild_payload_from_children(node_id);
1063
1064 if matches!(rewrite_surface.kind, RewriteSurfaceKind::Root) {
1065 deferred_evaluators.nodes.remove(&(surface, node_id));
1066 if node_id != rewrite_surface.arena.root()
1067 && normalise_evaluator_node_to_fixpoint_without_ancestor_sync(
1068 &mut rewrite_surface.arena,
1069 node_id,
1070 dirty_trace,
1071 )
1072 {
1073 evaluator_changed.insert((surface, node_id));
1074 }
1075 }
1076 }
1077
1078 for (surface, node_id, _) in nodes {
1079 let Some(rewrite_surface) = surfaces.get(surface) else {
1080 continue;
1081 };
1082 if !rewrite_surface.active || !rewrite_surface.arena.is_reachable(node_id) {
1083 continue;
1084 }
1085 if evaluator_changed.contains(&(surface, node_id)) {
1086 scheduler.index_subtree_references(&rewrite_surface.arena, surface, node_id);
1087 scheduler.enqueue_subtree(
1088 &rewrite_surface.arena,
1089 surface,
1090 node_id,
1091 Some(dirty_trace),
1092 );
1093 } else {
1094 scheduler.enqueue_node_at_level(
1095 &rewrite_surface.arena,
1096 surface,
1097 node_id,
1098 0,
1099 ScheduledMode::CheckNode,
1100 Some(dirty_trace),
1101 );
1102 }
1103 }
1104
1105 for (level, surface, node_id, mode) in postponed {
1109 let Some(rewrite_surface) = surfaces.get(surface) else {
1110 continue;
1111 };
1112 if !rewrite_surface.active || !rewrite_surface.arena.is_reachable(node_id) {
1113 continue;
1114 }
1115 scheduler.enqueue_node_at_level(
1116 &rewrite_surface.arena,
1117 surface,
1118 node_id,
1119 level,
1120 mode,
1121 Some(dirty_trace),
1122 );
1123 }
1124 }
1125}
1126
1127impl WorklistScheduler {
1128 fn empty(rule_groups: &[RuleGroup<'_>]) -> Self {
1129 Self {
1130 queues_by_level: vec![BinaryHeap::new(); rule_groups.len()],
1131 scheduled: HashMap::new(),
1132 subtree_candidate_cache: Vec::new(),
1133 failed_symbol_rules: HashMap::new(),
1134 reference_nodes_by_name: HashMap::new(),
1135 symbol_revision: 0,
1136 next_sequence: 0,
1137 }
1138 }
1139
1140 fn new(
1141 surfaces: &[RewriteSurface],
1142 rule_groups: &[RuleGroup<'_>],
1143 config: RewriteConfig,
1144 ) -> Self {
1145 let mut scheduler = Self::empty(rule_groups);
1146 for surface in 0..surfaces.len() {
1147 scheduler.enqueue_surface(surfaces, surface, rule_groups, config, None);
1148 }
1149 scheduler
1150 }
1151
1152 fn enqueue_surface(
1153 &mut self,
1154 surfaces: &[RewriteSurface],
1155 surface: usize,
1156 _rule_groups: &[RuleGroup<'_>],
1157 _config: RewriteConfig,
1158 dirty_trace: Option<&mut DirtyTrace>,
1159 ) {
1160 let Some(rewrite_surface) = surfaces.get(surface) else {
1161 return;
1162 };
1163 if !rewrite_surface.active {
1164 return;
1165 }
1166
1167 self.index_subtree_references(
1168 &rewrite_surface.arena,
1169 surface,
1170 rewrite_surface.arena.root(),
1171 );
1172 self.enqueue_subtree(
1173 &rewrite_surface.arena,
1174 surface,
1175 rewrite_surface.arena.root(),
1176 dirty_trace,
1177 );
1178 }
1179
1180 fn index_subtree_references(
1181 &mut self,
1182 arena: &ExpressionArena,
1183 surface: usize,
1184 subtree_root: ExpressionNodeId,
1185 ) {
1186 for node_id in rewriter_reachable_subtree_ids(arena, subtree_root) {
1187 let Expr::Atomic(_, Atom::Reference(reference)) = arena.expression(node_id) else {
1188 continue;
1189 };
1190 self.reference_nodes_by_name
1191 .entry(reference.name().clone())
1192 .or_default()
1193 .insert((surface, node_id));
1194 }
1195 }
1196
1197 fn should_attempt_rule(
1198 &self,
1199 surface: usize,
1200 node_id: ExpressionNodeId,
1201 rule_data: &RuleData<'_>,
1202 ) -> bool {
1203 if rule_data.rule.failure_invalidation != RuleFailureInvalidation::SymbolsOnly {
1204 return true;
1205 }
1206
1207 self.failed_symbol_rules.get(&FailedSymbolRuleKey {
1208 surface,
1209 node_id,
1210 rule: rule_data.rule as *const _ as usize,
1211 }) != Some(&self.symbol_revision)
1212 }
1213
1214 fn record_rule_failure(
1215 &mut self,
1216 surface: usize,
1217 node_id: ExpressionNodeId,
1218 rule_data: &RuleData<'_>,
1219 ) {
1220 if rule_data.rule.failure_invalidation == RuleFailureInvalidation::SymbolsOnly {
1221 self.failed_symbol_rules.insert(
1222 FailedSymbolRuleKey {
1223 surface,
1224 node_id,
1225 rule: rule_data.rule as *const _ as usize,
1226 },
1227 self.symbol_revision,
1228 );
1229 }
1230 }
1231
1232 fn invalidate_symbol_rule_failures(&mut self) {
1233 let (next_revision, overflowed) = self.symbol_revision.overflowing_add(1);
1234 self.symbol_revision = next_revision;
1235 if overflowed {
1236 self.failed_symbol_rules.clear();
1237 }
1238 }
1239
1240 fn enqueue_subtree(
1241 &mut self,
1242 arena: &ExpressionArena,
1243 surface: usize,
1244 node_id: ExpressionNodeId,
1245 dirty_trace: Option<&mut DirtyTrace>,
1246 ) {
1247 self.enqueue_node_at_level(
1248 arena,
1249 surface,
1250 node_id,
1251 0,
1252 ScheduledMode::TraverseSubtreeRoot,
1253 dirty_trace,
1254 );
1255 }
1256
1257 #[cfg(test)]
1258 fn enqueue_node_and_ancestors(
1259 &mut self,
1260 arena: &ExpressionArena,
1261 surface: usize,
1262 node_id: ExpressionNodeId,
1263 dirty_trace: &mut DirtyTrace,
1264 ) {
1265 let mut chain = Vec::new();
1266 let mut current = Some(node_id);
1267 while let Some(current_id) = current {
1268 chain.push(current_id);
1269 current = arena.parent(current_id);
1270 }
1271
1272 for current_id in chain.into_iter().rev() {
1273 self.enqueue_node_at_level(
1274 arena,
1275 surface,
1276 current_id,
1277 0,
1278 ScheduledMode::CheckNode,
1279 Some(dirty_trace),
1280 );
1281 }
1282 }
1283
1284 fn enqueue_children_at_level(
1285 &mut self,
1286 context: WorklistSchedulingContext<'_, '_, '_>,
1287 node_id: ExpressionNodeId,
1288 level: usize,
1289 mut dirty_trace: Option<&mut DirtyTrace>,
1290 ) -> usize {
1291 if matches!(context.arena.expression(node_id), Expr::Comprehension(_, _)) {
1292 return 0;
1293 }
1294
1295 let mut child_count = 0;
1296 for &child_id in context.arena.children(node_id) {
1297 if !self.subtree_has_candidates_at_level(
1298 context.arena,
1299 context.surface,
1300 child_id,
1301 level,
1302 context.rule_groups,
1303 context.config,
1304 ) {
1305 continue;
1306 }
1307 child_count += 1;
1308 self.enqueue_node_at_level(
1309 context.arena,
1310 context.surface,
1311 child_id,
1312 level,
1313 ScheduledMode::TraverseSubtreeDescendant,
1314 dirty_trace.as_deref_mut(),
1315 );
1316 }
1317 child_count
1318 }
1319
1320 fn enqueue_node_at_level(
1321 &mut self,
1322 arena: &ExpressionArena,
1323 surface: usize,
1324 node_id: ExpressionNodeId,
1325 level: usize,
1326 mode: ScheduledMode,
1327 mut dirty_trace: Option<&mut DirtyTrace>,
1328 ) {
1329 if level >= self.queues_by_level.len() {
1330 return;
1331 }
1332 if !arena.is_reachable(node_id) {
1333 return;
1334 }
1335
1336 let scheduled = ScheduledNode {
1337 surface,
1338 node_id,
1339 generation: arena.generation(node_id),
1340 mode,
1341 depth: arena.depth(node_id),
1342 sequence: self.next_sequence,
1343 };
1344 self.next_sequence += 1;
1345 let key = ScheduledKey {
1346 level,
1347 surface,
1348 node_id,
1349 generation: scheduled.generation,
1350 };
1351 match self.scheduled.entry(key) {
1352 std::collections::hash_map::Entry::Vacant(entry) => {
1353 entry.insert(mode);
1354 self.queues_by_level[level].push(scheduled);
1355 if let Some(trace) = dirty_trace.as_deref_mut() {
1356 trace.record_worklist_enqueue(mode);
1357 }
1358 }
1359 std::collections::hash_map::Entry::Occupied(mut entry) => {
1360 if !entry.get().includes(mode) {
1361 entry.insert(mode);
1362 self.queues_by_level[level].push(scheduled);
1363 if let Some(trace) = dirty_trace {
1364 trace.record_worklist_enqueue(mode);
1365 }
1366 }
1367 }
1368 }
1369 }
1370
1371 fn enqueue_after_no_rewrite(
1372 &mut self,
1373 context: WorklistSchedulingContext<'_, '_, '_>,
1374 node_id: ExpressionNodeId,
1375 level: usize,
1376 next_self_level: usize,
1377 mode: ScheduledMode,
1378 mut dirty_trace: Option<&mut DirtyTrace>,
1379 ) {
1380 if mode.descends_on_failure() {
1381 let child_count =
1382 self.enqueue_children_at_level(context, node_id, level, dirty_trace.as_deref_mut());
1383 if let Some(trace) = dirty_trace.as_deref_mut() {
1384 trace.record_worklist_child_descent(mode, child_count);
1385 }
1386 }
1387
1388 let Some(next_self_mode) = mode.next_self_mode() else {
1389 return;
1390 };
1391 let next_self_level = if mode.advances_as_subtree() {
1395 self.next_subtree_candidate_level(
1396 context.arena,
1397 context.surface,
1398 node_id,
1399 next_self_level,
1400 context.rule_groups,
1401 context.config,
1402 )
1403 } else {
1404 next_worklist_candidate_level(
1405 context.arena,
1406 node_id,
1407 next_self_level,
1408 context.rule_groups,
1409 context.config,
1410 )
1411 };
1412 self.enqueue_node_at_level(
1413 context.arena,
1414 context.surface,
1415 node_id,
1416 next_self_level,
1417 next_self_mode,
1418 dirty_trace,
1419 );
1420 }
1421
1422 fn pop_next(
1423 &mut self,
1424 surfaces: &[RewriteSurface],
1425 rule_groups: &[RuleGroup<'_>],
1426 config: RewriteConfig,
1427 dirty_trace: &mut DirtyTrace,
1428 ) -> Option<(usize, usize, ExpressionNodeId, ScheduledMode)> {
1429 for level in 0..self.queues_by_level.len() {
1430 while let Some(scheduled) = self.queues_by_level[level].pop() {
1431 let key = ScheduledKey {
1432 level,
1433 surface: scheduled.surface,
1434 node_id: scheduled.node_id,
1435 generation: scheduled.generation,
1436 };
1437 if self.scheduled.get(&key).copied() != Some(scheduled.mode) {
1438 dirty_trace.record_worklist_stale_pop(
1439 scheduled.mode,
1440 WorklistStaleReason::ModeMismatch,
1441 );
1442 continue;
1443 }
1444 self.scheduled.remove(&key);
1445
1446 let Some(surface) = surfaces.get(scheduled.surface) else {
1447 dirty_trace.record_worklist_stale_pop(
1448 scheduled.mode,
1449 WorklistStaleReason::MissingSurface,
1450 );
1451 continue;
1452 };
1453 if !surface.active {
1454 dirty_trace.record_worklist_stale_pop(
1455 scheduled.mode,
1456 WorklistStaleReason::InactiveSurface,
1457 );
1458 continue;
1459 }
1460 let arena = &surface.arena;
1461 if !arena.is_reachable(scheduled.node_id) {
1462 dirty_trace.record_worklist_stale_pop(
1463 scheduled.mode,
1464 WorklistStaleReason::UnreachableNode,
1465 );
1466 continue;
1467 }
1468 if arena.generation(scheduled.node_id) != scheduled.generation {
1469 dirty_trace.record_worklist_stale_pop(
1470 scheduled.mode,
1471 WorklistStaleReason::GenerationMismatch,
1472 );
1473 if scheduled.mode.advances_as_subtree() {
1474 let refresh_level = self.next_subtree_candidate_level(
1478 arena,
1479 scheduled.surface,
1480 scheduled.node_id,
1481 level,
1482 rule_groups,
1483 config,
1484 );
1485 self.enqueue_node_at_level(
1486 arena,
1487 scheduled.surface,
1488 scheduled.node_id,
1489 refresh_level,
1490 scheduled.mode,
1491 Some(dirty_trace),
1492 );
1493 }
1494 continue;
1495 }
1496
1497 dirty_trace.record_worklist_pop(scheduled.mode);
1498 return Some((level, scheduled.surface, scheduled.node_id, scheduled.mode));
1499 }
1500 }
1501
1502 None
1503 }
1504
1505 fn next_subtree_candidate_level(
1506 &mut self,
1507 arena: &ExpressionArena,
1508 surface: usize,
1509 node_id: ExpressionNodeId,
1510 start_level: usize,
1511 rule_groups: &[RuleGroup<'_>],
1512 config: RewriteConfig,
1513 ) -> usize {
1514 if start_level >= rule_groups.len() || !arena.is_reachable(node_id) {
1515 return rule_groups.len();
1516 }
1517
1518 (start_level..rule_groups.len())
1519 .find(|&level| {
1520 self.subtree_has_candidates_at_level(
1521 arena,
1522 surface,
1523 node_id,
1524 level,
1525 rule_groups,
1526 config,
1527 )
1528 })
1529 .unwrap_or(rule_groups.len())
1530 }
1531
1532 fn subtree_has_candidates_at_level(
1533 &mut self,
1534 arena: &ExpressionArena,
1535 surface: usize,
1536 node_id: ExpressionNodeId,
1537 level: usize,
1538 rule_groups: &[RuleGroup<'_>],
1539 config: RewriteConfig,
1540 ) -> bool {
1541 if level >= rule_groups.len() || !arena.is_reachable(node_id) {
1542 return false;
1543 }
1544
1545 let cache_bit = 1u128.checked_shl(level as u32);
1546 if let Some(bit) = cache_bit {
1547 if self.subtree_candidate_cache.len() <= surface {
1548 self.subtree_candidate_cache
1549 .resize_with(surface + 1, Vec::new);
1550 }
1551 let surface_cache = &mut self.subtree_candidate_cache[surface];
1552 if surface_cache.len() <= node_id.index() {
1553 surface_cache.resize(node_id.index() + 1, NodeSubtreeCandidateCache::default());
1554 }
1555 let entry = &mut surface_cache[node_id.index()];
1556 let generation = arena.generation(node_id);
1557 if !entry.generation_valid || entry.generation != generation {
1558 *entry = NodeSubtreeCandidateCache {
1559 generation,
1560 generation_valid: true,
1561 ..NodeSubtreeCandidateCache::default()
1562 };
1563 }
1564 if entry.known_levels & bit != 0 {
1565 return entry.candidate_levels & bit != 0;
1566 }
1567 }
1568
1569 let rule_group = &rule_groups[level];
1570 let has_candidates = rule_group.has_candidates_at_node(config, arena, node_id)
1571 || (!matches!(arena.expression(node_id), Expr::Comprehension(_, _))
1572 && arena.children(node_id).iter().any(|&child_id| {
1573 self.subtree_has_candidates_at_level(
1574 arena,
1575 surface,
1576 child_id,
1577 level,
1578 rule_groups,
1579 config,
1580 )
1581 }));
1582
1583 if let Some(bit) = cache_bit {
1584 let entry = &mut self.subtree_candidate_cache[surface][node_id.index()];
1585 entry.known_levels |= bit;
1586 if has_candidates {
1587 entry.candidate_levels |= bit;
1588 }
1589 }
1590 has_candidates
1591 }
1592}
1593
1594fn next_worklist_candidate_level(
1595 arena: &ExpressionArena,
1596 node_id: ExpressionNodeId,
1597 start_level: usize,
1598 rule_groups: &[RuleGroup<'_>],
1599 config: RewriteConfig,
1600) -> usize {
1601 if start_level >= rule_groups.len() || !arena.is_reachable(node_id) {
1602 return rule_groups.len();
1603 }
1604
1605 rule_groups
1606 .iter()
1607 .enumerate()
1608 .skip(start_level)
1609 .find_map(|(level, rule_group)| {
1610 rule_group
1611 .has_candidates_at_node(config, arena, node_id)
1612 .then_some(level)
1613 })
1614 .unwrap_or(rule_groups.len())
1615}
1616
1617struct RuleEffectImpact {
1618 added_names: Vec<Name>,
1619 changed_names: Vec<Name>,
1620 invalidated_reference_names: Vec<Name>,
1621 has_new_top: bool,
1622 has_new_sat_decisions: bool,
1623}
1624
1625impl RuleEffectImpact {
1626 fn new(
1627 effect: &crate::rule_engine::rule::RuleEffect,
1628 symbols: &crate::ast::SymbolTable,
1629 ) -> Self {
1630 let mut changed_names: Vec<_> = effect
1631 .changed_symbols(symbols)
1632 .into_iter()
1633 .map(|(name, _, _)| name)
1634 .collect();
1635 for name in effect.updated_declaration_names() {
1636 if !changed_names.contains(&name) {
1637 changed_names.push(name);
1638 }
1639 }
1640 let added_names: Vec<_> = effect.added_symbols(symbols).into_iter().collect();
1641 let mut invalidated_reference_names = changed_names.clone();
1644 for added_name in &added_names {
1645 let Some(declaration) = effect.symbols.lookup_local(added_name) else {
1646 continue;
1647 };
1648 let mut seen_sources = HashSet::new();
1649 let mut source = declaration.source().clone();
1650 while let Some(source_declaration) = source.take() {
1651 let source_name = source_declaration.name().clone();
1652 if !seen_sources.insert(source_name.clone()) {
1653 break;
1654 }
1655 if !invalidated_reference_names.contains(&source_name) {
1656 invalidated_reference_names.push(source_name);
1657 }
1658 source.clone_from(&source_declaration.source());
1659 }
1660 }
1661
1662 Self {
1663 added_names,
1664 changed_names,
1665 invalidated_reference_names,
1666 has_new_top: !effect.new_top.is_empty(),
1667 has_new_sat_decisions: !effect.new_sat_decisions.is_empty(),
1668 }
1669 }
1670
1671 fn has_model_side_effects(&self) -> bool {
1672 self.has_new_top || self.has_new_sat_decisions || self.has_symbol_changes()
1673 }
1674
1675 fn has_symbol_changes(&self) -> bool {
1676 !self.added_names.is_empty() || !self.changed_names.is_empty()
1677 }
1678}
1679
1680struct RewritePassContext<'ctx, 'rules, O> {
1681 rules_grouped: &'ctx Vec<(u16, Vec<RuleData<'rules>>)>,
1682 bucketed_rules: &'ctx Vec<RuleGroup<'rules>>,
1683 stats: &'ctx mut RewriterStats,
1684 dirty_trace: &'ctx mut DirtyTrace,
1685 config: RewriteConfig,
1686 attempt_observer: &'ctx mut O,
1687}
1688
1689struct TracingObserver<'run> {
1690 run_start: &'run Instant,
1691 general_enabled: bool,
1692 csv_enabled: bool,
1693}
1694
1695impl RuleAttemptObserver for TracingObserver<'_> {
1696 fn attempted(
1697 &mut self,
1698 priority: u16,
1699 rule: &RuleData<'_>,
1700 expression: &Expr,
1701 status: RuleAttemptStatus,
1702 ) {
1703 let elapsed_s = self.run_start.elapsed().as_secs_f64();
1704
1705 if self.general_enabled {
1706 trace!(
1707 target: "conjure::rule_attempt",
1708 elapsed_s,
1709 rule_level = priority,
1710 rule_name = rule.rule.name,
1711 rule_set = rule.rule_set.name,
1712 status = status.as_str(),
1713 expression = %expression,
1714 "attempted rule"
1715 );
1716 }
1717
1718 if self.csv_enabled {
1719 let expression = expression.to_string();
1720 trace!(
1721 target: "rule_engine_rule_attempt_trace",
1722 "{elapsed_s:.3},{priority},{},{},{},{}",
1723 csv_escape(rule.rule.name),
1724 csv_escape(rule.rule_set.name),
1725 status.as_str(),
1726 csv_escape(&expression),
1727 );
1728 }
1729 }
1730}
1731
1732fn domain_needs_abstract_repr(domain: &crate::ast::DomainPtr) -> bool {
1735 domain_needs_abstract_repr_at(domain, true)
1736}
1737
1738fn domain_needs_abstract_repr_at(domain: &crate::ast::DomainPtr, is_value: bool) -> bool {
1741 use crate::ast::{Domain, GroundDomain, UnresolvedDomain};
1742 let int_is_abstract = is_value && crate::settings::ints_need_representation();
1743 match domain.as_ref() {
1744 Domain::Ground(gd) => match gd.as_ref() {
1745 GroundDomain::Empty(..) | GroundDomain::Bool => false,
1746 GroundDomain::Int(_, _) => int_is_abstract,
1747 GroundDomain::Matrix(..) => true,
1750 _ => true,
1751 },
1752 Domain::Unresolved(ud) => match ud.as_ref() {
1753 UnresolvedDomain::Int(..) => int_is_abstract,
1754 UnresolvedDomain::Matrix(..) => true,
1755 UnresolvedDomain::Reference(re) => re
1756 .domain()
1757 .is_some_and(|d| domain_needs_abstract_repr_at(&d, is_value)),
1758 _ => true,
1759 },
1760 }
1761}
1762
1763fn model_needs_abstract_repr_rules(model: &Model) -> bool {
1768 use crate::ast::{AbstractLiteral, Atom, Expression as Expr, Literal};
1769
1770 for (_, decl) in model.symbols().iter_local() {
1771 if let Some(domain) = decl.domain()
1772 && domain_needs_abstract_repr(&domain)
1773 {
1774 return true;
1775 }
1776 }
1777
1778 model.root().any_expression(|expr| match expr {
1781 Expr::AbstractLiteral(_, abs) => !matches!(abs, AbstractLiteral::Matrix(..)),
1782 Expr::Atomic(_, Atom::Literal(Literal::AbstractLiteral(abs))) => {
1783 !matches!(abs, AbstractLiteral::Matrix(..))
1784 }
1785 _ => false,
1786 })
1787}
1788
1789pub fn rewrite_model<'a>(
1792 model: &Model,
1793 rule_sets: &Vec<&'a RuleSet<'a>>,
1794 config: RewriteConfig,
1795) -> Result<Model, RewriteError> {
1796 let _uniform_scope = crate::representation::uniform::Scope::new();
1797 if crate::settings::channelling() == crate::settings::Channelling::Uniform {
1798 for (_, decl) in model.symbols().iter_local() {
1799 if let Some(dom) = decl.domain() {
1800 for (_, state) in decl.reprs().iter() {
1801 crate::representation::uniform::check_choice(&dom, state.rule())
1802 .map_err(|e| RewriteError::UniformRepresentation(e.to_string()))?;
1803 crate::representation::uniform::record_choice(&dom, state.rule());
1804 }
1805 }
1806 }
1807 }
1808 set_current_rewriter(Rewriter::Rewrite(config));
1809
1810 let needs_abstract_repr = model_needs_abstract_repr_rules(model);
1811 let filtered_rule_sets: Vec<&'a RuleSet<'a>> = if needs_abstract_repr {
1812 rule_sets.clone()
1813 } else {
1814 rule_sets
1815 .iter()
1816 .copied()
1817 .filter(|rs| rs.name != "ReprGeneral" && rs.name != "ReprTuplePacked")
1818 .collect()
1819 };
1820
1821 let rules_grouped = get_rules_grouped(&filtered_rule_sets)
1822 .unwrap_or_else(|_| bug!("get_rule_priorities() failed!"))
1823 .into_iter()
1824 .collect_vec();
1825 let bucketed_rules = rules_grouped
1826 .iter()
1827 .map(|(priority, rules)| RuleGroup::new(*priority, rules.clone()))
1828 .collect_vec();
1829
1830 let mut model = introduce_objective_auxiliary(model.clone());
1831 if rule_sets.iter().any(|rules| rules.name == "SAT")
1832 && let Some(objective) = &model.objective
1833 && !model
1834 .sat_decisions()
1835 .iter()
1836 .any(|decision| matches!(decision, crate::ast::SatEncodingDecision::Objective { .. }))
1837 && !model
1838 .root()
1839 .any_expression(|expression| matches!(expression, Expr::SatObjective(..)))
1840 {
1841 model.add_constraint(Expr::SatObjective(
1842 crate::ast::Metadata::new(),
1843 matches!(
1844 objective.direction,
1845 crate::ast::OptimiseDirection::Minimising
1846 ),
1847 crate::ast::Moo::new(objective.expression.clone()),
1848 ));
1849 }
1850 tighten_domains_from_constraints(&mut model);
1851 let mut rewriter_stats = RewriterStats::new();
1852 rewriter_stats.is_optimisation_enabled = Some(!config.is_baseline());
1853 let mut dirty_trace = DirtyTrace::from_env();
1854 let run_start = Instant::now();
1855
1856 if rule_trace_enabled() && default_rule_trace_enabled() {
1857 trace!(
1858 target: "rule_engine_rule_trace",
1859 "Model before rewriting:\n\n{}\n--\n",
1860 model
1861 );
1862 }
1863 if rule_trace_enabled() && rule_attempt_trace_enabled() {
1864 trace!(
1865 target: "rule_engine_rule_attempt_trace",
1866 "elapsed_s,rule_level,rule_name,rule_set,status,expression"
1867 );
1868 }
1869
1870 let general_attempt_trace_enabled = tracing::enabled!(
1871 target: "conjure::rule_attempt",
1872 tracing::Level::TRACE
1873 );
1874 let csv_attempt_trace_enabled = rule_trace_enabled() && rule_attempt_trace_enabled();
1875
1876 if general_attempt_trace_enabled || csv_attempt_trace_enabled {
1877 let mut observer = TracingObserver {
1878 run_start: &run_start,
1879 general_enabled: general_attempt_trace_enabled,
1880 csv_enabled: csv_attempt_trace_enabled,
1881 };
1882 rewrite_to_fixpoint(
1883 &mut model,
1884 &rules_grouped,
1885 &bucketed_rules,
1886 &mut rewriter_stats,
1887 &mut dirty_trace,
1888 config,
1889 &mut observer,
1890 );
1891 } else {
1892 let mut observer = NoopObserver;
1893 rewrite_to_fixpoint(
1894 &mut model,
1895 &rules_grouped,
1896 &bucketed_rules,
1897 &mut rewriter_stats,
1898 &mut dirty_trace,
1899 config,
1900 &mut observer,
1901 );
1902 }
1903
1904 let run_end = Instant::now();
1905 rewriter_stats.rewriter_run_time = Some(run_end - run_start);
1906 model
1907 .context
1908 .write()
1909 .unwrap()
1910 .stats
1911 .add_rewriter_run(rewriter_stats);
1912 dirty_trace.finish(
1913 model
1914 .context
1915 .read()
1916 .unwrap()
1917 .stats
1918 .rewriter_runs
1919 .last()
1920 .expect("rewriter stats were just added"),
1921 );
1922
1923 super::encoding_selection::resolve_encoding_choices(model.sat_decisions_mut());
1924
1925 if rule_trace_enabled() && default_rule_trace_enabled() {
1926 trace!(
1927 target: "rule_engine_rule_trace",
1928 "Final model:\n\n{}",
1929 model
1930 );
1931 }
1932
1933 if let Some(normalised_root) = finish_root_evaluator_normalisation(model.root()) {
1938 model.replace_root(normalised_root);
1939 }
1940
1941 if crate::settings::channelling() == crate::settings::Channelling::Uniform {
1942 for (_, decl) in model.symbols().iter_local() {
1943 if decl.as_find().is_some()
1944 && decl.reprs().is_empty()
1945 && let Some(dom) = decl.domain()
1946 && let Some(selected) = crate::representation::uniform::selected(&dom, false)
1947 && selected.probe_for(decl).is_err()
1948 {
1949 return Err(RewriteError::UniformRepresentation(format!(
1950 "uniform representation `{}` does not support `{}` with domain `{}`",
1951 selected.name(),
1952 decl.name(),
1953 dom,
1954 )));
1955 }
1956 }
1957 }
1958 Ok(model)
1959}
1960
1961#[allow(clippy::too_many_arguments)]
1962fn rewrite_to_fixpoint<'rules, O: RuleAttemptObserver>(
1963 model: &mut Model,
1964 rules_grouped: &Vec<(u16, Vec<RuleData<'rules>>)>,
1965 bucketed_rules: &Vec<RuleGroup<'rules>>,
1966 stats: &mut RewriterStats,
1967 dirty_trace: &mut DirtyTrace,
1968 config: RewriteConfig,
1969 attempt_observer: &mut O,
1970) {
1971 let mut pass_ctx = RewritePassContext {
1972 rules_grouped,
1973 bucketed_rules,
1974 stats,
1975 dirty_trace,
1976 config,
1977 attempt_observer,
1978 };
1979 if config.worklist {
1980 let _ = try_rewrite_model(model, &mut pass_ctx);
1981 } else {
1982 let mut done_something = true;
1983 while done_something {
1984 done_something = try_rewrite_model(model, &mut pass_ctx).is_some();
1985 }
1986 }
1987}
1988
1989fn try_rewrite_model<'ctx, 'rules, O: RuleAttemptObserver>(
1993 submodel: &mut Model,
1994 ctx: &mut RewritePassContext<'ctx, 'rules, O>,
1995) -> Option<()> {
1996 ctx.dirty_trace.passes += 1;
1997 if !ctx.config.worklist
1998 && try_rewrite_value_letting_once(submodel, ctx.rules_grouped, ctx.attempt_observer)
1999 .is_some()
2000 {
2001 ctx.dirty_trace.value_letting_rewrites += 1;
2002 increment_counter(&mut ctx.stats.rewriter_value_letting_rewrites);
2003 return Some(());
2004 }
2005
2006 let mut did_rewrite = false;
2007 let mut arena = ExpressionArena::from_root(take_model_root(submodel));
2008 normalise_evaluators_bottom_up(&mut arena, ctx.dirty_trace);
2009
2010 if ctx.config.worklist {
2011 return try_rewrite_model_with_worklist(submodel, ctx, arena);
2012 }
2013
2014 'rewrite_loop: loop {
2015 let mut results: Vec<ApplicableRule<'_, ExpressionNodeId>> = vec![];
2016 let preorder_ids = rewriter_preorder_ids(&arena);
2017 'top: for (level, rule_group) in ctx.bucketed_rules.iter().enumerate() {
2019 ctx.dirty_trace.priority_scans += 1;
2020 for &node_id in &preorder_ids {
2021 ctx.dirty_trace.expression_visits += 1;
2022 let mut attempted_rule = false;
2023 {
2024 let expr = arena.expression(node_id);
2025 for rd in rule_group.candidates(ctx.config, expr) {
2026 attempted_rule = true;
2027 ctx.dirty_trace
2028 .record_rule_attempt(rule_group.priority, rd.rule.name);
2029 ctx.stats.rewriter_rule_application_attempts =
2031 Some(ctx.stats.rewriter_rule_application_attempts.unwrap_or(0) + 1);
2032
2033 match rd.rule.apply(expr, &submodel.symbols()) {
2034 Ok(red) => {
2035 ctx.attempt_observer.attempted(
2036 rule_group.priority,
2037 rd,
2038 expr,
2039 RuleAttemptStatus::Success,
2040 );
2041
2042 ctx.stats.rewriter_rule_applications =
2044 Some(ctx.stats.rewriter_rule_applications.unwrap_or(0) + 1);
2045
2046 results.push((
2048 RuleResult {
2049 rule_data: rd.clone(),
2050 effect: red,
2051 },
2052 level,
2053 expr.clone(),
2054 node_id,
2055 root_variable_snapshot_for_default_trace(
2056 expr,
2057 &submodel.symbols(),
2058 ),
2059 ));
2060 }
2061 Err(_) => {
2062 ctx.attempt_observer.attempted(
2063 rule_group.priority,
2064 rd,
2065 expr,
2066 RuleAttemptStatus::Failure,
2067 );
2068 }
2069 }
2070 }
2071 }
2072 if attempted_rule {
2073 ctx.dirty_trace.attempted_expressions += 1;
2074 }
2075 if !results.is_empty() {
2078 break 'top;
2079 }
2080 }
2081 }
2082
2083 if !results.is_empty() {
2084 let selected =
2085 choose_rule_result_index(results.iter().map(|(result, _, _, _, _)| result));
2086 results.swap(0, selected);
2087 }
2088
2089 match results.into_iter().next() {
2090 None => {
2091 submodel.replace_root(arena.into_synced_root_expression());
2092 break;
2093 }
2094 Some((result, _level, expr, node_id, variable_snapshot_before)) => {
2095 let effect = result.effect.materialise(&submodel.symbols());
2096 let variable_snapshots = variable_snapshot_before.map(|before| {
2097 let after = snapshot_symbols_after_effect(&submodel.symbols(), &effect);
2098 (before, after)
2099 });
2100 let result = RuleResult {
2101 rule_data: result.rule_data,
2102 effect,
2103 };
2104
2105 log_rule_application(
2107 &result,
2108 &expr,
2109 &submodel.symbols(),
2110 variable_snapshots
2111 .as_ref()
2112 .map(|(before, after)| (before, after)),
2113 );
2114
2115 let effect_impact = RuleEffectImpact::new(&result.effect, &submodel.symbols());
2116 let has_model_side_effects = effect_impact.has_model_side_effects();
2117 let rule_name = result.rule_data.rule.name;
2118 let RuleResult { effect, .. } = result;
2119 let crate::rule_engine::rule::RuleEffect {
2120 new_expression,
2121 new_top,
2122 symbols,
2123 new_sat_decisions,
2124 declaration_updates,
2125 ..
2126 } = effect;
2127 replace_focus_and_sync_ancestors(&mut arena, node_id, new_expression);
2129
2130 ctx.dirty_trace
2132 .record_rewrite(rule_name, has_model_side_effects);
2133 for update in declaration_updates {
2134 update.apply();
2135 }
2136 submodel.symbols_mut().extend(symbols);
2137 if effect_impact.has_new_top {
2138 arena.add_root_children(new_top);
2139 }
2140 submodel.add_sat_decisions(new_sat_decisions);
2141 let _ =
2142 normalise_evaluators_from_node_to_root(&mut arena, node_id, ctx.dirty_trace);
2143 if has_model_side_effects {
2144 ctx.dirty_trace.record_side_effect_kept_in_arena();
2145 }
2146
2147 #[cfg(debug_assertions)]
2148 {
2149 submodel.replace_root(arena.expression(arena.root()).clone());
2152 let assertion_context = format!("rewriter after applying rule '{rule_name}'");
2153 debug_assert_model_well_formed(submodel, &assertion_context);
2154 }
2155
2156 did_rewrite = true;
2157 continue 'rewrite_loop;
2158 }
2159 }
2160 }
2161
2162 did_rewrite.then_some(())
2163}
2164
2165fn try_rewrite_model_with_worklist<'ctx, 'rules, O: RuleAttemptObserver>(
2166 submodel: &mut Model,
2167 ctx: &mut RewritePassContext<'ctx, 'rules, O>,
2168 arena: ExpressionArena,
2169) -> Option<()> {
2170 let mut did_rewrite = false;
2171 let root_surface = 0usize;
2172 let (mut surfaces, mut value_letting_surfaces) = build_worklist_surfaces(submodel, arena);
2173 for surface in &mut surfaces {
2174 if surface.active {
2175 normalise_evaluators_bottom_up(&mut surface.arena, ctx.dirty_trace);
2176 }
2177 }
2178 write_worklist_surfaces_to_model(submodel, &surfaces);
2179 let mut scheduler = WorklistScheduler::new(&surfaces, ctx.bucketed_rules, ctx.config);
2180 let mut deferred_evaluators = DeferredEvaluatorWork::default();
2181 let mut deferred_ancestor_rules = DeferredAncestorRuleWork::default();
2182
2183 loop {
2184 let next = loop {
2185 let candidate =
2186 scheduler.pop_next(&surfaces, ctx.bucketed_rules, ctx.config, ctx.dirty_trace);
2187 if let Some((_, surface, node_id, _)) = candidate
2188 && deferred_ancestor_rules.contains(surface, node_id)
2189 {
2190 deferred_ancestor_rules.postpone(candidate.unwrap());
2191 continue;
2192 }
2193 break candidate;
2194 };
2195 let crosses_priority_boundary =
2196 deferred_ancestor_rules.should_flush_before(next.as_ref().map(|item| item.0));
2197 if crosses_priority_boundary {
2198 if let Some((level, surface, node_id, mode)) = next {
2201 scheduler.enqueue_node_at_level(
2202 &surfaces[surface].arena,
2203 surface,
2204 node_id,
2205 level,
2206 mode,
2207 Some(ctx.dirty_trace),
2208 );
2209 }
2210 deferred_ancestor_rules.flush(
2211 &mut scheduler,
2212 &mut surfaces,
2213 &mut deferred_evaluators,
2214 ctx.dirty_trace,
2215 );
2216 continue;
2217 }
2218
2219 let Some((level, surface_index, node_id, scheduled_mode)) = next else {
2220 if normalise_deferred_evaluators(
2221 &mut deferred_evaluators,
2222 submodel,
2223 &mut surfaces,
2224 &mut scheduler,
2225 ctx.dirty_trace,
2226 ) {
2227 did_rewrite = true;
2228 continue;
2229 }
2230 break;
2231 };
2232 ctx.dirty_trace.priority_scans += 1;
2233 ctx.dirty_trace.expression_visits += 1;
2234
2235 let rule_group = &ctx.bucketed_rules[level];
2236 if !rule_group.has_candidates_at_node(ctx.config, &surfaces[surface_index].arena, node_id) {
2237 ctx.dirty_trace
2238 .record_worklist_no_candidate_pop(scheduled_mode);
2239 scheduler.enqueue_after_no_rewrite(
2240 WorklistSchedulingContext::new(
2241 &surfaces[surface_index].arena,
2242 surface_index,
2243 ctx.bucketed_rules,
2244 ctx.config,
2245 ),
2246 node_id,
2247 level,
2248 level + 1,
2249 scheduled_mode,
2250 Some(ctx.dirty_trace),
2251 );
2252 continue;
2253 }
2254
2255 let mut results: Vec<ApplicableRule<'_, ExpressionNodeId>> = vec![];
2256 let mut attempted_rule = false;
2257 {
2258 let arena = &surfaces[surface_index].arena;
2259 let expr = arena.expression(node_id);
2260 for rd in rule_group.candidates_at_node(ctx.config, arena, node_id) {
2261 if !scheduler.should_attempt_rule(surface_index, node_id, rd) {
2262 continue;
2263 }
2264 attempted_rule = true;
2265 ctx.dirty_trace
2266 .record_rule_attempt(rule_group.priority, rd.rule.name);
2267 ctx.stats.rewriter_rule_application_attempts =
2268 Some(ctx.stats.rewriter_rule_application_attempts.unwrap_or(0) + 1);
2269
2270 match rd.rule.apply(expr, &submodel.symbols()) {
2271 Ok(red) => {
2272 ctx.attempt_observer.attempted(
2273 rule_group.priority,
2274 rd,
2275 expr,
2276 RuleAttemptStatus::Success,
2277 );
2278
2279 ctx.stats.rewriter_rule_applications =
2280 Some(ctx.stats.rewriter_rule_applications.unwrap_or(0) + 1);
2281
2282 results.push((
2283 RuleResult {
2284 rule_data: rd.clone(),
2285 effect: red,
2286 },
2287 level,
2288 expr.clone(),
2289 node_id,
2290 root_variable_snapshot_for_default_trace(expr, &submodel.symbols()),
2291 ));
2292 }
2293 Err(_) => {
2294 scheduler.record_rule_failure(surface_index, node_id, rd);
2295 ctx.attempt_observer.attempted(
2296 rule_group.priority,
2297 rd,
2298 expr,
2299 RuleAttemptStatus::Failure,
2300 );
2301 }
2302 }
2303 }
2304 }
2305
2306 if attempted_rule {
2307 ctx.dirty_trace.attempted_expressions += 1;
2308 ctx.dirty_trace
2309 .record_worklist_rule_attempt_pop(scheduled_mode);
2310 }
2311
2312 if results.is_empty() {
2313 scheduler.enqueue_after_no_rewrite(
2314 WorklistSchedulingContext::new(
2315 &surfaces[surface_index].arena,
2316 surface_index,
2317 ctx.bucketed_rules,
2318 ctx.config,
2319 ),
2320 node_id,
2321 level,
2322 level + 1,
2323 scheduled_mode,
2324 Some(ctx.dirty_trace),
2325 );
2326 continue;
2327 }
2328
2329 let selected = choose_rule_result_index(results.iter().map(|(result, _, _, _, _)| result));
2330 let (result, _level, expr, node_id, variable_snapshot_before) =
2331 results.swap_remove(selected);
2332
2333 let effect = result.effect.materialise(&submodel.symbols());
2334 let variable_snapshots = variable_snapshot_before.map(|before| {
2335 let after = snapshot_symbols_after_effect(&submodel.symbols(), &effect);
2336 (before, after)
2337 });
2338 let result = RuleResult {
2339 rule_data: result.rule_data,
2340 effect,
2341 };
2342
2343 log_rule_application(
2344 &result,
2345 &expr,
2346 &submodel.symbols(),
2347 variable_snapshots
2348 .as_ref()
2349 .map(|(before, after)| (before, after)),
2350 );
2351
2352 let effect_impact = RuleEffectImpact::new(&result.effect, &submodel.symbols());
2353 let has_model_side_effects = effect_impact.has_model_side_effects();
2354 let rewritten_value_letting_name =
2355 value_letting_surface_name(&surfaces[surface_index].kind).cloned();
2356 let invalidates_symbol_rules =
2357 effect_impact.has_symbol_changes() || rewritten_value_letting_name.is_some();
2358 let rule_name = result.rule_data.rule.name;
2359 let RuleResult { effect, .. } = result;
2360 let crate::rule_engine::rule::RuleEffect {
2361 new_expression,
2362 new_top,
2363 symbols,
2364 new_sat_decisions,
2365 declaration_updates,
2366 ..
2367 } = effect;
2368 {
2369 let arena = &mut surfaces[surface_index].arena;
2370 arena.replace_subtree(node_id, new_expression);
2371 }
2372
2373 ctx.dirty_trace
2374 .record_rewrite(rule_name, has_model_side_effects);
2375 for update in declaration_updates {
2376 update.apply();
2377 }
2378 submodel.symbols_mut().extend(symbols);
2379 let new_top_node_ids = if effect_impact.has_new_top {
2380 surfaces[root_surface].arena.add_root_children(new_top)
2381 } else {
2382 Vec::new()
2383 };
2384 submodel.add_sat_decisions(new_sat_decisions);
2385 let rewrite_impact_node_id = {
2386 let arena = &mut surfaces[surface_index].arena;
2387 normalise_evaluators_subtree_bottom_up(arena, node_id, ctx.dirty_trace);
2388 deferred_evaluators.defer_ancestors(arena, surface_index, node_id);
2389 deferred_ancestor_rules.defer_ancestors(arena, surface_index, node_id, level);
2390 node_id
2391 };
2392 scheduler.index_subtree_references(
2393 &surfaces[surface_index].arena,
2394 surface_index,
2395 rewrite_impact_node_id,
2396 );
2397 for &new_top_node_id in &new_top_node_ids {
2398 if surfaces[root_surface].arena.is_reachable(new_top_node_id) {
2399 normalise_evaluators_subtree_bottom_up(
2400 &mut surfaces[root_surface].arena,
2401 new_top_node_id,
2402 ctx.dirty_trace,
2403 );
2404 deferred_evaluators.defer_ancestors(
2405 &surfaces[root_surface].arena,
2406 root_surface,
2407 new_top_node_id,
2408 );
2409 deferred_ancestor_rules.defer_ancestors(
2410 &surfaces[root_surface].arena,
2411 root_surface,
2412 new_top_node_id,
2413 level,
2414 );
2415 scheduler.index_subtree_references(
2416 &surfaces[root_surface].arena,
2417 root_surface,
2418 new_top_node_id,
2419 );
2420 }
2421 }
2422 if let Some(name) = rewritten_value_letting_name.as_ref() {
2423 write_value_letting_surface_to_model(submodel, name, &surfaces[surface_index].arena);
2424 ctx.dirty_trace.value_letting_rewrites += 1;
2425 increment_counter(&mut ctx.stats.rewriter_value_letting_rewrites);
2426 }
2427 let mut affected_names = effect_impact.invalidated_reference_names.clone();
2428 if let Some(name) = rewritten_value_letting_name.as_ref()
2429 && !affected_names.contains(name)
2430 {
2431 affected_names.push(name.clone());
2432 }
2433 let mut symbol_surface_names = effect_impact.added_names.clone();
2434 symbol_surface_names.extend(effect_impact.changed_names.iter().cloned());
2435 if let Some(name) = rewritten_value_letting_name.as_ref() {
2436 symbol_surface_names.retain(|candidate| candidate != name);
2437 }
2438 let synced_surfaces = sync_value_letting_surfaces(
2439 submodel,
2440 &mut surfaces,
2441 &mut value_letting_surfaces,
2442 &symbol_surface_names,
2443 );
2444
2445 if has_model_side_effects {
2451 deferred_ancestor_rules.flush_effect_path_before_root(
2452 &mut scheduler,
2453 &mut surfaces,
2454 &mut deferred_evaluators,
2455 surface_index,
2456 rewrite_impact_node_id,
2457 ctx.dirty_trace,
2458 );
2459 }
2460 {
2461 if has_model_side_effects {
2462 ctx.dirty_trace.record_side_effect_kept_in_arena();
2463 }
2464 if invalidates_symbol_rules {
2465 scheduler.invalidate_symbol_rule_failures();
2466 scheduler.enqueue_node_at_level(
2467 &surfaces[root_surface].arena,
2468 root_surface,
2469 surfaces[root_surface].arena.root(),
2470 0,
2471 ScheduledMode::CheckNode,
2472 Some(ctx.dirty_trace),
2473 );
2474 }
2475 scheduler.enqueue_subtree(
2476 &surfaces[surface_index].arena,
2477 surface_index,
2478 rewrite_impact_node_id,
2479 Some(ctx.dirty_trace),
2480 );
2481 for new_top_node_id in new_top_node_ids {
2482 scheduler.enqueue_subtree(
2483 &surfaces[root_surface].arena,
2484 root_surface,
2485 new_top_node_id,
2486 Some(ctx.dirty_trace),
2487 );
2488 }
2489 for synced_surface in synced_surfaces {
2490 scheduler.enqueue_surface(
2491 &surfaces,
2492 synced_surface,
2493 ctx.bucketed_rules,
2494 ctx.config,
2495 Some(ctx.dirty_trace),
2496 );
2497 }
2498 enqueue_worklist_nodes_referencing_names(
2499 &mut scheduler,
2500 &surfaces,
2501 &affected_names,
2502 ctx.dirty_trace,
2503 );
2504 }
2505
2506 did_rewrite = true;
2507 }
2508
2509 move_worklist_surfaces_to_model(submodel, surfaces);
2510 #[cfg(debug_assertions)]
2511 debug_assert_model_well_formed(submodel, "rewriter after a settled worklist pass");
2512 did_rewrite.then_some(())
2513}
2514
2515#[cfg(test)]
2516fn enqueue_worklist_rewrite_impact(
2517 scheduler: &mut WorklistScheduler,
2518 arena: &ExpressionArena,
2519 surface: usize,
2520 node_id: ExpressionNodeId,
2521 dirty_trace: &mut DirtyTrace,
2522) {
2523 scheduler.enqueue_node_and_ancestors(arena, surface, node_id, dirty_trace);
2524 scheduler.enqueue_subtree(arena, surface, node_id, Some(dirty_trace));
2525}
2526
2527fn normalise_deferred_evaluators(
2534 work: &mut DeferredEvaluatorWork,
2535 submodel: &mut Model,
2536 surfaces: &mut [RewriteSurface],
2537 scheduler: &mut WorklistScheduler,
2538 dirty_trace: &mut DirtyTrace,
2539) -> bool {
2540 if work.is_empty() {
2541 return false;
2542 }
2543
2544 let nodes = deferred_nodes_in_bottom_up_order(std::mem::take(&mut work.nodes), surfaces);
2545
2546 let mut affected_surfaces = HashSet::new();
2547 let mut changed_value_lettings = HashSet::new();
2548 for (surface, node_id, _) in nodes {
2549 let Some(rewrite_surface) = surfaces.get_mut(surface) else {
2550 continue;
2551 };
2552 if !rewrite_surface.active || !rewrite_surface.arena.is_reachable(node_id) {
2553 continue;
2554 }
2555 affected_surfaces.insert(surface);
2556 rewrite_surface.arena.rebuild_payload_from_children(node_id);
2557
2558 if node_id != rewrite_surface.arena.root()
2560 && normalise_evaluator_node_to_fixpoint_without_ancestor_sync(
2561 &mut rewrite_surface.arena,
2562 node_id,
2563 dirty_trace,
2564 )
2565 {
2566 scheduler.index_subtree_references(&rewrite_surface.arena, surface, node_id);
2567 if let Some(name) = value_letting_surface_name(&rewrite_surface.kind) {
2568 changed_value_lettings.insert(name.clone());
2569 }
2570 }
2571 }
2572
2573 let mut wrote_value_letting = false;
2574 let mut changed_value_lettings: Vec<_> = changed_value_lettings.into_iter().collect();
2575 changed_value_lettings.sort();
2576 for name in &changed_value_lettings {
2577 let Some(surface) = surfaces.iter().find(|surface| {
2578 surface.active && value_letting_surface_name(&surface.kind) == Some(name)
2579 }) else {
2580 continue;
2581 };
2582 wrote_value_letting |=
2583 write_value_letting_surface_to_model_without_refresh(submodel, name, &surface.arena);
2584 }
2585 if wrote_value_letting {
2586 submodel.symbols_mut().refresh_local_binding_hashes();
2587 scheduler.invalidate_symbol_rule_failures();
2588 scheduler.enqueue_node_at_level(
2589 &surfaces[0].arena,
2590 0,
2591 surfaces[0].arena.root(),
2592 0,
2593 ScheduledMode::CheckNode,
2594 Some(dirty_trace),
2595 );
2596 enqueue_worklist_nodes_referencing_names(
2597 scheduler,
2598 surfaces,
2599 &changed_value_lettings,
2600 dirty_trace,
2601 );
2602 }
2603
2604 let mut affected_surfaces: Vec<_> = affected_surfaces.into_iter().collect();
2605 affected_surfaces.sort_unstable();
2606 for surface in affected_surfaces.iter().copied() {
2607 let arena = &surfaces[surface].arena;
2608 scheduler.enqueue_subtree(arena, surface, arena.root(), Some(dirty_trace));
2609 }
2610
2611 !affected_surfaces.is_empty()
2612}
2613
2614fn deferred_nodes_in_bottom_up_order(
2615 nodes: impl IntoIterator<Item = (usize, ExpressionNodeId)>,
2616 surfaces: &[RewriteSurface],
2617) -> Vec<(usize, ExpressionNodeId, Option<Vec<usize>>)> {
2618 let mut nodes: Vec<_> = nodes
2621 .into_iter()
2622 .map(|(surface, node_id)| {
2623 let path = surfaces
2624 .get(surface)
2625 .filter(|rewrite_surface| rewrite_surface.active)
2626 .filter(|rewrite_surface| rewrite_surface.arena.is_reachable(node_id))
2627 .map(|rewrite_surface| rewrite_surface.arena.preorder_path(node_id));
2628 (surface, node_id, path)
2629 })
2630 .collect();
2631 nodes.sort_by(
2632 |(left_surface, left_node, left_path), (right_surface, right_node, right_path)| {
2633 right_path
2634 .as_ref()
2635 .map_or(0, Vec::len)
2636 .cmp(&left_path.as_ref().map_or(0, Vec::len))
2637 .then_with(|| left_surface.cmp(right_surface))
2638 .then_with(|| match (left_path, right_path) {
2639 (Some(left_path), Some(right_path)) => left_path.cmp(right_path),
2640 _ => left_node.cmp(right_node),
2641 })
2642 },
2643 );
2644 nodes
2645}
2646
2647fn normalise_evaluators_bottom_up(arena: &mut ExpressionArena, dirty_trace: &mut DirtyTrace) {
2654 normalise_evaluators_subtree_bottom_up(arena, arena.root(), dirty_trace);
2655}
2656
2657fn normalise_evaluators_subtree_bottom_up(
2658 arena: &mut ExpressionArena,
2659 subtree_root: ExpressionNodeId,
2660 dirty_trace: &mut DirtyTrace,
2661) -> bool {
2662 let nodes = rewriter_reachable_subtree_ids(arena, subtree_root);
2663 let mut changed = false;
2664
2665 for node_id in nodes.into_iter().rev() {
2666 if !arena.is_reachable(node_id) {
2667 continue;
2668 }
2669 changed |=
2670 normalise_evaluator_node_to_fixpoint_without_ancestor_sync(arena, node_id, dirty_trace);
2671
2672 if node_id != subtree_root
2677 && let Some(parent_id) = arena.parent(node_id)
2678 && arena.is_reachable(parent_id)
2679 {
2680 arena.sync_payload_for_changed_child(parent_id, node_id);
2681 }
2682 }
2683
2684 changed
2685}
2686
2687fn normalise_evaluators_from_node_to_root(
2695 arena: &mut ExpressionArena,
2696 node_id: ExpressionNodeId,
2697 dirty_trace: &mut DirtyTrace,
2698) -> (ExpressionNodeId, bool) {
2699 let subtree_changed = normalise_evaluators_subtree_bottom_up(arena, node_id, dirty_trace);
2703 let mut came_from = node_id;
2704 let mut current = arena.parent(node_id);
2705 let mut highest_rewritten = node_id;
2706 let mut changed = subtree_changed;
2707
2708 while let Some(current_id) = current {
2709 if !arena.is_reachable(current_id) {
2710 break;
2711 }
2712
2713 let rewritten = if current_id == arena.root() {
2714 normalise_root_evaluator_for_child(arena, came_from, dirty_trace)
2718 } else {
2719 normalise_evaluator_node_to_fixpoint(arena, current_id, dirty_trace)
2720 };
2721 if rewritten {
2722 highest_rewritten = current_id;
2723 changed = true;
2724 }
2725 came_from = current_id;
2726 current = arena.parent(current_id);
2727 }
2728
2729 (highest_rewritten, changed)
2730}
2731
2732fn normalise_root_evaluator_for_child(
2740 arena: &mut ExpressionArena,
2741 child_id: ExpressionNodeId,
2742 dirty_trace: &mut DirtyTrace,
2743) -> bool {
2744 let root_id = arena.root();
2745 if !arena.children(root_id).contains(&child_id) {
2746 return normalise_evaluator_node_to_fixpoint(arena, root_id, dirty_trace);
2747 }
2748
2749 let Some(replacement) = normalise_root_constraint_deep(arena.expression(child_id)) else {
2750 return false;
2751 };
2752
2753 dirty_trace.replacement_subtree_clears += 1;
2754 arena.replace_subtree(child_id, replacement);
2755 dirty_trace.record_rewrite("evaluator_normalisation_hook", false);
2756 sync_ancestor_payloads(arena, child_id);
2757 true
2758}
2759
2760fn normalise_evaluator_node_to_fixpoint(
2761 arena: &mut ExpressionArena,
2762 node_id: ExpressionNodeId,
2763 dirty_trace: &mut DirtyTrace,
2764) -> bool {
2765 let changed =
2766 normalise_evaluator_node_to_fixpoint_without_ancestor_sync(arena, node_id, dirty_trace);
2767
2768 if changed {
2769 sync_ancestor_payloads(arena, node_id);
2770 }
2771
2772 changed
2773}
2774
2775fn normalise_evaluator_node_to_fixpoint_without_ancestor_sync(
2776 arena: &mut ExpressionArena,
2777 node_id: ExpressionNodeId,
2778 dirty_trace: &mut DirtyTrace,
2779) -> bool {
2780 let mut changed = false;
2781
2782 while arena.is_reachable(node_id) {
2783 let expr = arena.expression(node_id);
2784 let Some(replacement) = normalise_evaluator_local(expr) else {
2785 break;
2786 };
2787
2788 dirty_trace.replacement_subtree_clears += 1;
2789 arena.replace_subtree(node_id, replacement);
2790 dirty_trace.record_rewrite("evaluator_normalisation_hook", false);
2791 changed = true;
2792 }
2793
2794 changed
2795}
2796
2797fn build_worklist_surfaces(
2798 submodel: &Model,
2799 root_arena: ExpressionArena,
2800) -> (Vec<RewriteSurface>, HashMap<Name, usize>) {
2801 let mut surfaces = vec![RewriteSurface::root(root_arena)];
2802 let mut value_letting_surfaces = HashMap::new();
2803
2804 let symbols = submodel.symbols();
2805 for (name, decl) in symbols.iter_local() {
2806 let letting_expr = decl.as_value_letting().map(|expr| expr.clone());
2807 if let Some(expr) = letting_expr {
2808 let surface = surfaces.len();
2809 surfaces.push(RewriteSurface::value_letting(name.clone(), expr));
2810 value_letting_surfaces.insert(name.clone(), surface);
2811 }
2812 }
2813
2814 (surfaces, value_letting_surfaces)
2815}
2816
2817fn value_letting_surface_name(kind: &RewriteSurfaceKind) -> Option<&Name> {
2818 match kind {
2819 RewriteSurfaceKind::Root => None,
2820 RewriteSurfaceKind::ValueLetting { name } => Some(name),
2821 }
2822}
2823
2824fn current_value_letting_expression(submodel: &Model, name: &Name) -> Option<Expr> {
2825 let declaration = {
2826 let symbols = submodel.symbols();
2827 symbols.lookup_local(name)
2828 }?;
2829 declaration.as_value_letting().map(|expr| expr.clone())
2830}
2831
2832fn sync_value_letting_surfaces(
2833 submodel: &Model,
2834 surfaces: &mut Vec<RewriteSurface>,
2835 value_letting_surfaces: &mut HashMap<Name, usize>,
2836 names: &[Name],
2837) -> Vec<usize> {
2838 let mut synced_surfaces = Vec::new();
2839 let mut seen = HashSet::new();
2840
2841 for name in names {
2842 if !seen.insert(name.clone()) {
2843 continue;
2844 }
2845
2846 if let Some(expr) = current_value_letting_expression(submodel, name) {
2847 if let Some(old_surface) = value_letting_surfaces.get(name).copied()
2848 && let Some(surface) = surfaces.get_mut(old_surface)
2849 {
2850 surface.active = false;
2851 }
2852 let new_surface = surfaces.len();
2853 surfaces.push(RewriteSurface::value_letting(name.clone(), expr));
2854 value_letting_surfaces.insert(name.clone(), new_surface);
2855 synced_surfaces.push(new_surface);
2856 } else if let Some(old_surface) = value_letting_surfaces.remove(name)
2857 && let Some(surface) = surfaces.get_mut(old_surface)
2858 {
2859 surface.active = false;
2860 }
2861 }
2862
2863 synced_surfaces
2864}
2865
2866fn write_worklist_surfaces_to_model(submodel: &mut Model, surfaces: &[RewriteSurface]) {
2867 let root = surfaces[0]
2868 .arena
2869 .expression(surfaces[0].arena.root())
2870 .clone();
2871 submodel.replace_root(root);
2872
2873 let mut wrote_value_letting = false;
2874 for surface in surfaces.iter().skip(1) {
2875 if !surface.active {
2876 continue;
2877 }
2878 let Some(name) = value_letting_surface_name(&surface.kind) else {
2879 continue;
2880 };
2881 let expression = surface.arena.expression(surface.arena.root()).clone();
2882 wrote_value_letting |=
2883 write_value_letting_expression_to_model_without_refresh(submodel, name, expression);
2884 }
2885 if wrote_value_letting {
2886 submodel.symbols_mut().refresh_local_binding_hashes();
2887 }
2888}
2889
2890fn move_worklist_surfaces_to_model(submodel: &mut Model, surfaces: Vec<RewriteSurface>) {
2891 let mut surfaces = surfaces.into_iter();
2892 let root = surfaces.next().expect("worklist must have a root surface");
2893 submodel.replace_root(root.arena.into_synced_root_expression());
2894
2895 let mut wrote_value_letting = false;
2896 for surface in surfaces {
2897 if !surface.active {
2898 continue;
2899 }
2900 let RewriteSurfaceKind::ValueLetting { name } = surface.kind else {
2901 continue;
2902 };
2903 wrote_value_letting |= write_value_letting_expression_to_model_without_refresh(
2904 submodel,
2905 &name,
2906 surface.arena.into_synced_root_expression(),
2907 );
2908 }
2909 if wrote_value_letting {
2910 submodel.symbols_mut().refresh_local_binding_hashes();
2911 }
2912}
2913
2914fn write_value_letting_surface_to_model(
2915 submodel: &mut Model,
2916 name: &Name,
2917 arena: &ExpressionArena,
2918) -> bool {
2919 let written = write_value_letting_surface_to_model_without_refresh(submodel, name, arena);
2920 if written {
2921 submodel.symbols_mut().refresh_local_binding_hashes();
2922 }
2923 written
2924}
2925
2926fn write_value_letting_surface_to_model_without_refresh(
2927 submodel: &mut Model,
2928 name: &Name,
2929 arena: &ExpressionArena,
2930) -> bool {
2931 write_value_letting_expression_to_model_without_refresh(
2932 submodel,
2933 name,
2934 arena.expression_from(arena.root()),
2935 )
2936}
2937
2938fn write_value_letting_expression_to_model_without_refresh(
2939 submodel: &mut Model,
2940 name: &Name,
2941 expression: Expr,
2942) -> bool {
2943 let declaration = {
2944 let symbols = submodel.symbols();
2945 symbols.lookup_local(name)
2946 };
2947 let Some(mut declaration) = declaration else {
2948 return false;
2949 };
2950 {
2951 let Some(mut letting) = declaration.as_value_letting_mut() else {
2952 return false;
2953 };
2954
2955 *letting = expression;
2956 }
2957 true
2958}
2959
2960fn enqueue_worklist_nodes_referencing_names(
2961 scheduler: &mut WorklistScheduler,
2962 surfaces: &[RewriteSurface],
2963 names: &[Name],
2964 dirty_trace: &mut DirtyTrace,
2965) {
2966 if names.is_empty() {
2967 return;
2968 }
2969
2970 let mut affected_nodes = BTreeSet::new();
2973 for name in names {
2974 let Some(reference_nodes) = scheduler.reference_nodes_by_name.get(name) else {
2975 continue;
2976 };
2977 for &(surface_index, reference_node) in reference_nodes {
2978 let Some(surface) = surfaces.get(surface_index) else {
2979 continue;
2980 };
2981 if !surface.active
2982 || !surface.arena.is_reachable(reference_node)
2983 || !expression_directly_references_any(
2984 surface.arena.expression(reference_node),
2985 std::slice::from_ref(name),
2986 )
2987 {
2988 continue;
2989 }
2990
2991 let mut current = Some(reference_node);
2992 while let Some(node_id) = current {
2993 affected_nodes.insert((surface_index, node_id));
2994 current = surface.arena.parent(node_id);
2995 }
2996 }
2997 }
2998
2999 for (surface_index, node_id) in affected_nodes {
3000 let surface = &surfaces[surface_index];
3001 scheduler.enqueue_node_at_level(
3002 &surface.arena,
3003 surface_index,
3004 node_id,
3005 0,
3006 ScheduledMode::CheckNode,
3007 Some(dirty_trace),
3008 );
3009 }
3010}
3011
3012#[cfg(test)]
3013fn collect_worklist_nodes_referencing_names(
3014 arena: &ExpressionArena,
3015 node_id: ExpressionNodeId,
3016 names: &[Name],
3017 affected_nodes: &mut Vec<ExpressionNodeId>,
3018) -> bool {
3019 if !arena.is_reachable(node_id) {
3020 return false;
3021 }
3022
3023 let mut references_changed_name =
3024 expression_directly_references_any(arena.expression(node_id), names);
3025 if !matches!(arena.expression(node_id), Expr::Comprehension(_, _)) {
3026 for &child_id in arena.children(node_id) {
3027 references_changed_name |=
3028 collect_worklist_nodes_referencing_names(arena, child_id, names, affected_nodes);
3029 }
3030 }
3031
3032 if references_changed_name {
3033 affected_nodes.push(node_id);
3034 }
3035 references_changed_name
3036}
3037
3038fn expression_directly_references_any(expr: &Expr, names: &[Name]) -> bool {
3039 let Expr::Atomic(_, Atom::Reference(reference)) = expr else {
3040 return false;
3041 };
3042 names.iter().any(|name| &*reference.name() == name)
3043}
3044
3045fn increment_counter(counter: &mut Option<usize>) {
3046 *counter = Some(counter.unwrap_or(0) + 1);
3047}
3048
3049fn rewriter_preorder_ids(arena: &ExpressionArena) -> Vec<ExpressionNodeId> {
3051 rewriter_reachable_subtree_ids(arena, arena.root())
3052}
3053
3054fn rewriter_reachable_subtree_ids(
3059 arena: &ExpressionArena,
3060 node_id: ExpressionNodeId,
3061) -> Vec<ExpressionNodeId> {
3062 fn collect(
3063 arena: &ExpressionArena,
3064 node_id: ExpressionNodeId,
3065 nodes: &mut Vec<ExpressionNodeId>,
3066 ) {
3067 if !arena.is_reachable(node_id) {
3068 return;
3069 }
3070 nodes.push(node_id);
3071 if matches!(arena.expression(node_id), Expr::Comprehension(_, _)) {
3072 return;
3073 }
3074
3075 for child in arena.children(node_id) {
3076 collect(arena, *child, nodes);
3077 }
3078 }
3079
3080 let mut nodes = Vec::new();
3081 collect(arena, node_id, &mut nodes);
3082 nodes
3083}
3084
3085fn replace_focus_and_sync_ancestors(
3087 arena: &mut ExpressionArena,
3088 node_id: ExpressionNodeId,
3089 new_focus: Expr,
3090) {
3091 arena.replace_subtree(node_id, new_focus);
3092 sync_ancestor_payloads(arena, node_id);
3093}
3094
3095fn sync_ancestor_payloads(arena: &mut ExpressionArena, node_id: ExpressionNodeId) {
3096 let mut child_id = node_id;
3097 let mut ancestor = arena.parent(node_id);
3098 while let Some(ancestor_id) = ancestor {
3099 arena.sync_payload_for_changed_child(ancestor_id, child_id);
3101 child_id = ancestor_id;
3102 ancestor = arena.parent(ancestor_id);
3103 }
3104}
3105
3106fn take_model_root(model: &mut Model) -> Expr {
3107 model.replace_root(Expr::Root(Metadata::new(), Vec::new()))
3108}
3109
3110fn rule_is_universal(rule_data: &RuleData<'_>) -> bool {
3111 rule_data.rule.prefilters.is_none()
3112}
3113
3114fn rule_matches_self_discriminant(rule_data: &RuleData<'_>, expr_discriminant: usize) -> bool {
3115 rule_data
3116 .rule
3117 .prefilters
3118 .is_some_and(|prefilters| {
3119 prefilters.iter().any(|prefilter| {
3120 matches!(prefilter, RulePrefilter::Variant(discriminant) if *discriminant == expr_discriminant)
3121 })
3122 })
3123}
3124
3125fn rule_matches_specific_prefilter(
3126 rule_data: &RuleData<'_>,
3127 expr: &Expr,
3128 arena_node: Option<(&ExpressionArena, ExpressionNodeId)>,
3129) -> bool {
3130 if rule_is_universal(rule_data) {
3131 return false;
3132 }
3133
3134 let expr_discriminant = discriminant_from_value(expr);
3135 rule_data.rule.prefilters.is_some_and(|prefilters| {
3136 prefilters.iter().any(|prefilter| match prefilter {
3137 RulePrefilter::Variant(discriminant) => *discriminant == expr_discriminant,
3138 RulePrefilter::Child { child } => {
3139 has_direct_child_discriminant(expr, arena_node, *child)
3140 }
3141 RulePrefilter::VariantChild { variant, child } => {
3142 *variant == expr_discriminant
3143 && has_direct_child_discriminant(expr, arena_node, *child)
3144 }
3145 RulePrefilter::Atom(atom_kind) => expr_atom_kind(expr) == Some(*atom_kind),
3146 RulePrefilter::ChildAtom(atom_kind) => {
3147 expr_has_direct_child_atom_kind(expr, *atom_kind)
3148 }
3149 })
3150 })
3151}
3152
3153fn has_direct_child_discriminant(
3154 expr: &Expr,
3155 arena_node: Option<(&ExpressionArena, ExpressionNodeId)>,
3156 target_discriminant: usize,
3157) -> bool {
3158 if let Some((arena, node_id)) = arena_node {
3159 arena.has_direct_child_discriminant(node_id, target_discriminant)
3160 } else {
3161 expr_has_direct_child_discriminant(expr, &[target_discriminant])
3162 }
3163}
3164
3165fn expr_atom_kind(expr: &Expr) -> Option<AtomKind> {
3166 match expr {
3167 Expr::Atomic(_, Atom::Literal(_)) => Some(AtomKind::Literal),
3168 Expr::Atomic(_, Atom::Reference(_)) => Some(AtomKind::Reference),
3169 _ => None,
3170 }
3171}
3172
3173fn expr_has_direct_child_atom_kind(expr: &Expr, target: AtomKind) -> bool {
3178 let mut found = false;
3179 expr.for_each_expr_child(&mut |child| {
3180 if !found && expr_atom_kind(child) == Some(target) {
3181 found = true;
3182 }
3183 });
3184 found
3185}
3186
3187fn expr_has_direct_child_discriminant(expr: &Expr, target_discriminants: &[usize]) -> bool {
3188 if target_discriminants.is_empty() {
3189 return false;
3190 }
3191
3192 let mut found = false;
3193 expr.for_each_expr_child(&mut |child| {
3194 if !found && target_discriminants.contains(&discriminant_from_value(child)) {
3195 found = true;
3196 }
3197 });
3198 found
3199}
3200
3201fn csv_escape(field: &str) -> String {
3202 if field.contains([',', '"', '\n', '\r']) {
3203 format!("\"{}\"", field.replace('"', "\"\""))
3204 } else {
3205 field.to_string()
3206 }
3207}
3208
3209#[cfg(test)]
3210mod tests {
3211 use crate::ast::comprehension::ComprehensionBuilder;
3212 use crate::ast::{Atom, DeclarationPtr, ExpressionArena, Literal, Moo, SymbolTablePtr};
3213 use crate::matrix_expr;
3214
3215 use super::*;
3216
3217 fn int_lit(value: i32) -> Expr {
3218 Expr::Atomic(Metadata::new(), Atom::Literal(Literal::Int(value)))
3219 }
3220
3221 fn bool_lit(value: bool) -> Expr {
3222 Expr::Atomic(Metadata::new(), Atom::Literal(Literal::Bool(value)))
3223 }
3224
3225 fn root(exprs: Vec<Expr>) -> Expr {
3226 Expr::Root(Metadata::new(), exprs)
3227 }
3228
3229 fn comprehension(return_expression: Expr, guards: Vec<Expr>) -> Expr {
3230 let mut builder = ComprehensionBuilder::new(SymbolTablePtr::new());
3231 for guard in guards {
3232 builder = builder.guard(guard);
3233 }
3234 Expr::Comprehension(
3235 Metadata::new(),
3236 Moo::new(builder.with_return_value(return_expression)),
3237 )
3238 }
3239
3240 #[test]
3241 fn dirty_trace_filename_replaces_path_separators_and_module_delimiters() {
3242 assert_eq!(
3243 sanitize_dirty_trace_filename("generated_tests::savilerow/quasiGroup6"),
3244 "generated_tests--savilerow-quasiGroup6"
3245 );
3246 }
3247
3248 #[test]
3249 fn dirty_trace_bare_path_is_directory_destination() {
3250 assert_eq!(
3251 dirty_trace_destination_from_env_value("trace-dir".into()),
3252 DirtyTraceDestination::Directory(PathBuf::from("trace-dir"))
3253 );
3254 }
3255
3256 #[test]
3257 fn dirty_trace_txt_path_is_file_destination() {
3258 assert_eq!(
3259 dirty_trace_destination_from_env_value("trace.txt".into()),
3260 DirtyTraceDestination::File(PathBuf::from("trace.txt"))
3261 );
3262 }
3263
3264 fn reference_expr(name: &Name) -> Expr {
3265 use crate::ast::{Domain, Range, Reference};
3266
3267 Expr::Atomic(
3268 Metadata::new(),
3269 Atom::Reference(Reference::new(DeclarationPtr::new_find(
3270 name.clone(),
3271 Domain::int(vec![Range::Bounded(1, 3)]),
3272 ))),
3273 )
3274 }
3275
3276 fn test_rule_set_applies(_: &crate::settings::SolverFamily) -> bool {
3277 true
3278 }
3279
3280 fn never_apply_test_rule(
3281 _: &Expr,
3282 _: &crate::ast::SymbolTable,
3283 ) -> crate::rule_engine::ApplicationResult {
3284 Err(crate::rule_engine::ApplicationError::RuleNotApplicable)
3285 }
3286
3287 static TEST_RULE_SET: RuleSet<'static> =
3288 RuleSet::new("test-rule-set", &[], test_rule_set_applies);
3289 static TEST_RULE: crate::rule_engine::Rule<'static> = crate::rule_engine::Rule::new(
3290 "never-apply-test-rule",
3291 never_apply_test_rule,
3292 &[("test-rule-set", 1)],
3293 );
3294 static TEST_NO_TARGET_RULE: crate::rule_engine::Rule<'static> = crate::rule_engine::Rule {
3295 name: "no-target-test-rule",
3296 application: never_apply_test_rule,
3297 rule_sets: &[("test-rule-set", 1)],
3298 prefilters: Some(&[]),
3299 failure_invalidation: crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3300 };
3301 static TEST_SYMBOLS_ONLY_RULE: crate::rule_engine::Rule<'static> = crate::rule_engine::Rule {
3302 name: "symbols-only-test-rule",
3303 application: never_apply_test_rule,
3304 rule_sets: &[("test-rule-set", 1)],
3305 prefilters: None,
3306 failure_invalidation: crate::rule_engine::RuleFailureInvalidation::SymbolsOnly,
3307 };
3308 fn test_rule_groups_at_priorities(priorities: &[u16]) -> Vec<RuleGroup<'static>> {
3309 priorities
3310 .iter()
3311 .map(|&priority| {
3312 RuleGroup::new(
3313 priority,
3314 vec![crate::rule_engine::RuleData {
3315 rule: &TEST_RULE,
3316 priority,
3317 rule_set: &TEST_RULE_SET,
3318 }],
3319 )
3320 })
3321 .collect()
3322 }
3323
3324 fn test_rule_groups() -> Vec<RuleGroup<'static>> {
3325 test_rule_groups_at_priorities(&[1])
3326 }
3327
3328 fn test_rule_groups_with_two_levels() -> Vec<RuleGroup<'static>> {
3329 test_rule_groups_at_priorities(&[1, 2])
3330 }
3331
3332 fn test_rule_groups_with_no_candidate_middle_level() -> Vec<RuleGroup<'static>> {
3333 vec![
3334 RuleGroup::new(
3335 1,
3336 vec![crate::rule_engine::RuleData {
3337 rule: &TEST_RULE,
3338 priority: 1,
3339 rule_set: &TEST_RULE_SET,
3340 }],
3341 ),
3342 RuleGroup::new(
3343 2,
3344 vec![crate::rule_engine::RuleData {
3345 rule: &TEST_NO_TARGET_RULE,
3346 priority: 2,
3347 rule_set: &TEST_RULE_SET,
3348 }],
3349 ),
3350 RuleGroup::new(
3351 3,
3352 vec![crate::rule_engine::RuleData {
3353 rule: &TEST_RULE,
3354 priority: 3,
3355 rule_set: &TEST_RULE_SET,
3356 }],
3357 ),
3358 ]
3359 }
3360
3361 fn test_rule_groups_targeting_expr(expr: &Expr) -> Vec<RuleGroup<'static>> {
3362 let discriminant = discriminant_from_value(expr);
3363 let mut rules_by_discriminant = Vec::new();
3364 rules_by_discriminant.resize_with(discriminant + 1, || None);
3365 rules_by_discriminant[discriminant] = Some(vec![crate::rule_engine::RuleData {
3366 rule: &TEST_RULE,
3367 priority: 1,
3368 rule_set: &TEST_RULE_SET,
3369 }]);
3370
3371 vec![RuleGroup {
3372 priority: 1,
3373 rules: vec![crate::rule_engine::RuleData {
3374 rule: &TEST_RULE,
3375 priority: 1,
3376 rule_set: &TEST_RULE_SET,
3377 }],
3378 rules_by_discriminant,
3379 universal_rules: Vec::new(),
3380 has_non_discriminant_filters: false,
3381 }]
3382 }
3383
3384 #[test]
3385 fn rule_group_child_filter_matches_immediate_child_kind() {
3386 let bubble_discriminant = discriminant_from_value(&Expr::Bubble(
3387 Metadata::new(),
3388 Moo::new(int_lit(0)),
3389 Moo::new(bool_lit(true)),
3390 ));
3391 let child_prefilters: &'static [RulePrefilter] = Box::leak(
3392 vec![RulePrefilter::Child {
3393 child: bubble_discriminant,
3394 }]
3395 .into_boxed_slice(),
3396 );
3397 let child_bubble_rule: &'static crate::rule_engine::Rule<'static> =
3398 Box::leak(Box::new(crate::rule_engine::Rule {
3399 name: "child-bubble-test-rule",
3400 application: never_apply_test_rule,
3401 rule_sets: &[("test-rule-set", 1)],
3402 prefilters: Some(child_prefilters),
3403 failure_invalidation:
3404 crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3405 }));
3406 let rule_group = RuleGroup::new(
3407 1,
3408 vec![crate::rule_engine::RuleData {
3409 rule: child_bubble_rule,
3410 priority: 1,
3411 rule_set: &TEST_RULE_SET,
3412 }],
3413 );
3414 let config = RewriteConfig::optimised();
3415
3416 let expr_with_bubble_child = Expr::Eq(
3417 Metadata::new(),
3418 Moo::new(Expr::Bubble(
3419 Metadata::new(),
3420 Moo::new(int_lit(1)),
3421 Moo::new(bool_lit(true)),
3422 )),
3423 Moo::new(int_lit(2)),
3424 );
3425 let expr_without_bubble_child =
3426 Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
3427
3428 assert!(rule_group.has_candidates(config, &expr_with_bubble_child));
3429 assert_eq!(
3430 rule_group
3431 .candidates(config, &expr_with_bubble_child)
3432 .map(|rule_data| rule_data.rule.name)
3433 .collect_vec(),
3434 vec!["child-bubble-test-rule"]
3435 );
3436 assert!(!rule_group.has_candidates(config, &expr_without_bubble_child));
3437 }
3438
3439 #[test]
3440 fn rule_group_atom_filter_matches_atomic_reference() {
3441 let atom_reference_rule: &'static crate::rule_engine::Rule<'static> =
3442 Box::leak(Box::new(crate::rule_engine::Rule {
3443 name: "atom-reference-test-rule",
3444 application: never_apply_test_rule,
3445 rule_sets: &[("test-rule-set", 1)],
3446 prefilters: Some(&[RulePrefilter::Atom(AtomKind::Reference)]),
3447 failure_invalidation:
3448 crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3449 }));
3450 let rule_group = RuleGroup::new(
3451 1,
3452 vec![crate::rule_engine::RuleData {
3453 rule: atom_reference_rule,
3454 priority: 1,
3455 rule_set: &TEST_RULE_SET,
3456 }],
3457 );
3458 let config = RewriteConfig::optimised();
3459 let reference = reference_expr(&Name::user("x"));
3460 let literal = int_lit(1);
3461 let composite = Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
3462
3463 assert!(rule_group.has_candidates(config, &reference));
3464 assert_eq!(
3465 rule_group
3466 .candidates(config, &reference)
3467 .map(|rule_data| rule_data.rule.name)
3468 .collect_vec(),
3469 vec!["atom-reference-test-rule"]
3470 );
3471 assert!(!rule_group.has_candidates(config, &literal));
3472 assert!(!rule_group.has_candidates(config, &composite));
3473 }
3474
3475 #[test]
3476 fn rule_group_child_atom_filter_matches_direct_atomic_reference_child() {
3477 let child_atom_rule: &'static crate::rule_engine::Rule<'static> =
3478 Box::leak(Box::new(crate::rule_engine::Rule {
3479 name: "child-atom-reference-test-rule",
3480 application: never_apply_test_rule,
3481 rule_sets: &[("test-rule-set", 1)],
3482 prefilters: Some(&[RulePrefilter::ChildAtom(AtomKind::Reference)]),
3483 failure_invalidation:
3484 crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3485 }));
3486 let rule_group = RuleGroup::new(
3487 1,
3488 vec![crate::rule_engine::RuleData {
3489 rule: child_atom_rule,
3490 priority: 1,
3491 rule_set: &TEST_RULE_SET,
3492 }],
3493 );
3494 let config = RewriteConfig::optimised();
3495 let with_reference_child = Expr::Eq(
3496 Metadata::new(),
3497 Moo::new(reference_expr(&Name::user("x"))),
3498 Moo::new(int_lit(2)),
3499 );
3500 let with_literal_children =
3502 Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
3503 let bare_reference = reference_expr(&Name::user("x"));
3505
3506 assert!(rule_group.has_candidates(config, &with_reference_child));
3507 assert_eq!(
3508 rule_group
3509 .candidates(config, &with_reference_child)
3510 .map(|rule_data| rule_data.rule.name)
3511 .collect_vec(),
3512 vec!["child-atom-reference-test-rule"]
3513 );
3514 assert!(!rule_group.has_candidates(config, &with_literal_children));
3515 assert!(!rule_group.has_candidates(config, &bare_reference));
3516 }
3517
3518 #[test]
3519 fn rule_group_includes_universal_rules_in_variant_buckets() {
3520 let lex_discriminant = discriminant_from_value(&Expr::LexLt(
3521 Metadata::new(),
3522 Moo::new(int_lit(1)),
3523 Moo::new(int_lit(2)),
3524 ));
3525 let lex_prefilters: &'static [RulePrefilter] =
3526 Box::leak(Box::new([RulePrefilter::Variant(lex_discriminant)]));
3527 let variant_rule: &'static crate::rule_engine::Rule<'static> =
3528 Box::leak(Box::new(crate::rule_engine::Rule {
3529 name: "variant-specific-test-rule",
3530 application: never_apply_test_rule,
3531 rule_sets: &[("test-rule-set", 1)],
3532 prefilters: Some(lex_prefilters),
3533 failure_invalidation:
3534 crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3535 }));
3536 let universal_rule: &'static crate::rule_engine::Rule<'static> =
3537 Box::leak(Box::new(crate::rule_engine::Rule {
3538 name: "universal-test-rule",
3539 application: never_apply_test_rule,
3540 rule_sets: &[("test-rule-set", 1)],
3541 prefilters: None,
3542 failure_invalidation:
3543 crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3544 }));
3545 let rule_group = RuleGroup::new(
3546 1,
3547 vec![
3548 crate::rule_engine::RuleData {
3549 rule: variant_rule,
3550 priority: 1,
3551 rule_set: &TEST_RULE_SET,
3552 },
3553 crate::rule_engine::RuleData {
3554 rule: universal_rule,
3555 priority: 1,
3556 rule_set: &TEST_RULE_SET,
3557 },
3558 ],
3559 );
3560 let config = RewriteConfig::optimised();
3561 let lex = Expr::LexLt(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
3562
3563 assert!(rule_group.has_candidates(config, &lex));
3564 assert_eq!(
3565 rule_group
3566 .candidates(config, &lex)
3567 .map(|rule_data| rule_data.rule.name)
3568 .collect_vec(),
3569 vec!["variant-specific-test-rule", "universal-test-rule"]
3570 );
3571 }
3572
3573 #[test]
3574 fn rule_group_variant_child_filter_does_not_cross_product_alternatives() {
3575 let and_discriminant = discriminant_from_value(&Expr::And(
3576 Metadata::new(),
3577 Moo::new(matrix_expr![bool_lit(true)]),
3578 ));
3579 let or_discriminant = discriminant_from_value(&Expr::Or(
3580 Metadata::new(),
3581 Moo::new(matrix_expr![bool_lit(true)]),
3582 ));
3583 let comprehension_discriminant =
3584 discriminant_from_value(&comprehension(bool_lit(true), vec![]));
3585 let atomic_discriminant = discriminant_from_value(&bool_lit(true));
3586 let paired_prefilters: &'static [RulePrefilter] = Box::leak(
3587 vec![
3588 RulePrefilter::VariantChild {
3589 variant: and_discriminant,
3590 child: comprehension_discriminant,
3591 },
3592 RulePrefilter::VariantChild {
3593 variant: or_discriminant,
3594 child: atomic_discriminant,
3595 },
3596 ]
3597 .into_boxed_slice(),
3598 );
3599 let paired_rule: &'static crate::rule_engine::Rule<'static> =
3600 Box::leak(Box::new(crate::rule_engine::Rule {
3601 name: "paired-prefilter-test-rule",
3602 application: never_apply_test_rule,
3603 rule_sets: &[("test-rule-set", 1)],
3604 prefilters: Some(paired_prefilters),
3605 failure_invalidation:
3606 crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3607 }));
3608 let rule_group = RuleGroup::new(
3609 1,
3610 vec![crate::rule_engine::RuleData {
3611 rule: paired_rule,
3612 priority: 1,
3613 rule_set: &TEST_RULE_SET,
3614 }],
3615 );
3616 let config = RewriteConfig::optimised();
3617 let and_with_comprehension = Expr::And(
3618 Metadata::new(),
3619 Moo::new(comprehension(bool_lit(true), vec![])),
3620 );
3621 let or_with_atomic = Expr::Or(Metadata::new(), Moo::new(bool_lit(true)));
3622 let and_with_atomic = Expr::And(Metadata::new(), Moo::new(bool_lit(true)));
3623 let or_with_comprehension = Expr::Or(
3624 Metadata::new(),
3625 Moo::new(comprehension(bool_lit(true), vec![])),
3626 );
3627
3628 assert!(rule_group.has_candidates(config, &and_with_comprehension));
3629 assert!(rule_group.has_candidates(config, &or_with_atomic));
3630 assert!(!rule_group.has_candidates(config, &and_with_atomic));
3631 assert!(!rule_group.has_candidates(config, &or_with_comprehension));
3632 }
3633
3634 #[test]
3635 fn rewriter_subtree_preorder_does_not_enter_comprehensions() {
3636 let tree = root(vec![
3637 comprehension(int_lit(1), vec![int_lit(2)]),
3638 int_lit(3),
3639 ]);
3640 let arena = ExpressionArena::from_root(tree);
3641 let root_ids = rewriter_preorder_ids(&arena);
3642 let comp_id = arena.children(arena.root())[0];
3643
3644 assert_eq!(root_ids.len(), 3);
3645 assert_eq!(
3646 rewriter_reachable_subtree_ids(&arena, comp_id),
3647 vec![comp_id]
3648 );
3649 }
3650
3651 #[test]
3652 fn worklist_ancestor_enqueue_checks_enclosing_nodes_first() {
3653 let tree = root(vec![Expr::Eq(
3654 Metadata::new(),
3655 Moo::new(int_lit(1)),
3656 Moo::new(int_lit(2)),
3657 )]);
3658 let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
3659 let arena = &surfaces[0].arena;
3660 let ids = rewriter_preorder_ids(arena);
3661 let root_id = ids[0];
3662 let eq_id = ids[1];
3663 let left_leaf_id = ids[2];
3664
3665 let rule_groups = test_rule_groups();
3666 let mut scheduler = WorklistScheduler::empty(&rule_groups);
3667 let mut dirty_trace = DirtyTrace::default();
3668
3669 scheduler.enqueue_node_and_ancestors(arena, 0, left_leaf_id, &mut dirty_trace);
3670
3671 assert_eq!(
3672 scheduler.pop_next(
3673 &surfaces,
3674 &rule_groups,
3675 RewriteConfig::optimised(),
3676 &mut dirty_trace
3677 ),
3678 Some((0, 0, root_id, ScheduledMode::CheckNode))
3679 );
3680 assert_eq!(
3681 scheduler.pop_next(
3682 &surfaces,
3683 &rule_groups,
3684 RewriteConfig::optimised(),
3685 &mut dirty_trace
3686 ),
3687 Some((0, 0, eq_id, ScheduledMode::CheckNode))
3688 );
3689 assert_eq!(
3690 scheduler.pop_next(
3691 &surfaces,
3692 &rule_groups,
3693 RewriteConfig::optimised(),
3694 &mut dirty_trace
3695 ),
3696 Some((0, 0, left_leaf_id, ScheduledMode::CheckNode))
3697 );
3698 assert_eq!(
3699 scheduler.pop_next(
3700 &surfaces,
3701 &rule_groups,
3702 RewriteConfig::optimised(),
3703 &mut dirty_trace
3704 ),
3705 None
3706 );
3707 }
3708
3709 #[test]
3710 fn worklist_level_order_prefers_ancestors_over_earlier_descendants() {
3711 let tree = root(vec![Expr::Eq(
3712 Metadata::new(),
3713 Moo::new(int_lit(1)),
3714 Moo::new(int_lit(2)),
3715 )]);
3716 let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
3717 let arena = &surfaces[0].arena;
3718 let ids = rewriter_preorder_ids(arena);
3719 let eq_id = ids[1];
3720 let left_leaf_id = ids[2];
3721
3722 let rule_groups = test_rule_groups();
3723 let mut scheduler = WorklistScheduler::empty(&rule_groups);
3724 let mut dirty_trace = DirtyTrace::default();
3725
3726 scheduler.enqueue_node_at_level(
3727 arena,
3728 0,
3729 left_leaf_id,
3730 0,
3731 ScheduledMode::CheckNode,
3732 Some(&mut dirty_trace),
3733 );
3734 scheduler.enqueue_node_at_level(
3735 arena,
3736 0,
3737 eq_id,
3738 0,
3739 ScheduledMode::CheckNode,
3740 Some(&mut dirty_trace),
3741 );
3742
3743 assert_eq!(
3744 scheduler.pop_next(
3745 &surfaces,
3746 &rule_groups,
3747 RewriteConfig::optimised(),
3748 &mut dirty_trace
3749 ),
3750 Some((0, 0, eq_id, ScheduledMode::CheckNode))
3751 );
3752 assert_eq!(
3753 scheduler.pop_next(
3754 &surfaces,
3755 &rule_groups,
3756 RewriteConfig::optimised(),
3757 &mut dirty_trace
3758 ),
3759 Some((0, 0, left_leaf_id, ScheduledMode::CheckNode))
3760 );
3761 }
3762
3763 #[test]
3764 fn worklist_subtree_descends_lazily_in_breadth_first_order() {
3765 let tree = root(vec![
3766 Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2))),
3767 int_lit(3),
3768 ]);
3769 let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
3770 let arena = &surfaces[0].arena;
3771 let ids = rewriter_preorder_ids(arena);
3772 let root_id = ids[0];
3773 let eq_id = ids[1];
3774 let left_leaf_id = ids[2];
3775 let right_leaf_id = ids[3];
3776 let root_sibling_id = ids[4];
3777
3778 let rule_groups = test_rule_groups_with_two_levels();
3779 let mut scheduler =
3780 WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
3781 let mut dirty_trace = DirtyTrace::default();
3782
3783 let root_work = scheduler.pop_next(
3784 &surfaces,
3785 &rule_groups,
3786 RewriteConfig::optimised(),
3787 &mut dirty_trace,
3788 );
3789 assert_eq!(
3790 root_work,
3791 Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
3792 );
3793 assert_eq!(
3794 scheduler.pop_next(
3795 &surfaces,
3796 &rule_groups,
3797 RewriteConfig::optimised(),
3798 &mut dirty_trace
3799 ),
3800 None
3801 );
3802
3803 scheduler.enqueue_after_no_rewrite(
3804 WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
3805 root_id,
3806 0,
3807 1,
3808 ScheduledMode::TraverseSubtreeRoot,
3809 Some(&mut dirty_trace),
3810 );
3811 let eq_work = scheduler.pop_next(
3812 &surfaces,
3813 &rule_groups,
3814 RewriteConfig::optimised(),
3815 &mut dirty_trace,
3816 );
3817 assert_eq!(
3818 eq_work,
3819 Some((0, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
3820 );
3821 scheduler.enqueue_after_no_rewrite(
3822 WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
3823 eq_id,
3824 0,
3825 1,
3826 ScheduledMode::TraverseSubtreeDescendant,
3827 Some(&mut dirty_trace),
3828 );
3829 assert_eq!(
3830 scheduler.pop_next(
3831 &surfaces,
3832 &rule_groups,
3833 RewriteConfig::optimised(),
3834 &mut dirty_trace
3835 ),
3836 Some((
3837 0,
3838 0,
3839 root_sibling_id,
3840 ScheduledMode::TraverseSubtreeDescendant
3841 ))
3842 );
3843 assert_eq!(
3844 scheduler.pop_next(
3845 &surfaces,
3846 &rule_groups,
3847 RewriteConfig::optimised(),
3848 &mut dirty_trace
3849 ),
3850 Some((0, 0, left_leaf_id, ScheduledMode::TraverseSubtreeDescendant))
3851 );
3852 assert_eq!(
3853 scheduler.pop_next(
3854 &surfaces,
3855 &rule_groups,
3856 RewriteConfig::optimised(),
3857 &mut dirty_trace
3858 ),
3859 Some((
3860 0,
3861 0,
3862 right_leaf_id,
3863 ScheduledMode::TraverseSubtreeDescendant
3864 ))
3865 );
3866 assert_eq!(
3867 scheduler.pop_next(
3868 &surfaces,
3869 &rule_groups,
3870 RewriteConfig::optimised(),
3871 &mut dirty_trace
3872 ),
3873 Some((1, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
3874 );
3875 assert_eq!(
3876 scheduler.pop_next(
3877 &surfaces,
3878 &rule_groups,
3879 RewriteConfig::optimised(),
3880 &mut dirty_trace
3881 ),
3882 None
3883 );
3884
3885 scheduler.enqueue_after_no_rewrite(
3886 WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
3887 root_id,
3888 1,
3889 2,
3890 ScheduledMode::TraverseSubtreeRoot,
3891 Some(&mut dirty_trace),
3892 );
3893 assert_eq!(
3894 scheduler.pop_next(
3895 &surfaces,
3896 &rule_groups,
3897 RewriteConfig::optimised(),
3898 &mut dirty_trace
3899 ),
3900 Some((1, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
3901 );
3902 }
3903
3904 #[test]
3905 fn worklist_refreshes_stale_subtree_carrier_after_descendant_rewrite() {
3906 let tree = root(vec![
3907 Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2))),
3908 int_lit(3),
3909 ]);
3910 let mut surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
3911 let ids = rewriter_preorder_ids(&surfaces[0].arena);
3912 let root_id = ids[0];
3913 let eq_id = ids[1];
3914 let root_sibling_id = ids[4];
3915
3916 let rule_groups = test_rule_groups_with_two_levels();
3917 let mut scheduler =
3918 WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
3919 let mut dirty_trace = DirtyTrace::default();
3920
3921 assert_eq!(
3922 scheduler.pop_next(
3923 &surfaces,
3924 &rule_groups,
3925 RewriteConfig::optimised(),
3926 &mut dirty_trace
3927 ),
3928 Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
3929 );
3930 scheduler.enqueue_after_no_rewrite(
3931 WorklistSchedulingContext::new(
3932 &surfaces[0].arena,
3933 0,
3934 &rule_groups,
3935 RewriteConfig::optimised(),
3936 ),
3937 root_id,
3938 0,
3939 1,
3940 ScheduledMode::TraverseSubtreeRoot,
3941 Some(&mut dirty_trace),
3942 );
3943
3944 assert_eq!(
3945 scheduler.pop_next(
3946 &surfaces,
3947 &rule_groups,
3948 RewriteConfig::optimised(),
3949 &mut dirty_trace
3950 ),
3951 Some((0, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
3952 );
3953 replace_focus_and_sync_ancestors(&mut surfaces[0].arena, eq_id, int_lit(10));
3954 enqueue_worklist_rewrite_impact(
3955 &mut scheduler,
3956 &surfaces[0].arena,
3957 0,
3958 eq_id,
3959 &mut dirty_trace,
3960 );
3961
3962 let mut found_refreshed_root = false;
3963 for _ in 0..32 {
3964 let Some((level, surface, node_id, mode)) = scheduler.pop_next(
3965 &surfaces,
3966 &rule_groups,
3967 RewriteConfig::optimised(),
3968 &mut dirty_trace,
3969 ) else {
3970 break;
3971 };
3972 if (level, surface, node_id, mode)
3973 == (1, 0, root_id, ScheduledMode::TraverseSubtreeRoot)
3974 {
3975 found_refreshed_root = true;
3976 scheduler.enqueue_after_no_rewrite(
3977 WorklistSchedulingContext::new(
3978 &surfaces[0].arena,
3979 0,
3980 &rule_groups,
3981 RewriteConfig::optimised(),
3982 ),
3983 root_id,
3984 1,
3985 2,
3986 ScheduledMode::TraverseSubtreeRoot,
3987 Some(&mut dirty_trace),
3988 );
3989 break;
3990 }
3991 scheduler.enqueue_after_no_rewrite(
3992 WorklistSchedulingContext::new(
3993 &surfaces[surface].arena,
3994 surface,
3995 &rule_groups,
3996 RewriteConfig::optimised(),
3997 ),
3998 node_id,
3999 level,
4000 level + 1,
4001 mode,
4002 Some(&mut dirty_trace),
4003 );
4004 }
4005 assert!(found_refreshed_root);
4006
4007 let mut found_sibling_at_next_level = false;
4008 for _ in 0..32 {
4009 let Some((level, _surface, node_id, mode)) = scheduler.pop_next(
4010 &surfaces,
4011 &rule_groups,
4012 RewriteConfig::optimised(),
4013 &mut dirty_trace,
4014 ) else {
4015 break;
4016 };
4017 if (level, node_id, mode)
4018 == (1, root_sibling_id, ScheduledMode::TraverseSubtreeDescendant)
4019 {
4020 found_sibling_at_next_level = true;
4021 break;
4022 }
4023 }
4024 assert!(found_sibling_at_next_level);
4025 }
4026
4027 #[test]
4028 fn worklist_prunes_child_subtrees_without_candidates_at_level() {
4029 let eq = Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
4030 let tree = root(vec![eq.clone(), int_lit(3)]);
4031 let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
4032 let arena = &surfaces[0].arena;
4033 let ids = rewriter_preorder_ids(arena);
4034 let root_id = ids[0];
4035 let eq_id = ids[1];
4036
4037 let rule_groups = test_rule_groups_targeting_expr(&eq);
4038 let mut scheduler =
4039 WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
4040 let mut dirty_trace = DirtyTrace::default();
4041
4042 assert_eq!(
4043 scheduler.pop_next(
4044 &surfaces,
4045 &rule_groups,
4046 RewriteConfig::optimised(),
4047 &mut dirty_trace
4048 ),
4049 Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
4050 );
4051 scheduler.enqueue_after_no_rewrite(
4052 WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
4053 root_id,
4054 0,
4055 1,
4056 ScheduledMode::TraverseSubtreeRoot,
4057 Some(&mut dirty_trace),
4058 );
4059
4060 assert_eq!(
4061 scheduler.pop_next(
4062 &surfaces,
4063 &rule_groups,
4064 RewriteConfig::optimised(),
4065 &mut dirty_trace
4066 ),
4067 Some((0, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
4068 );
4069 assert_eq!(
4070 scheduler.pop_next(
4071 &surfaces,
4072 &rule_groups,
4073 RewriteConfig::optimised(),
4074 &mut dirty_trace
4075 ),
4076 None
4077 );
4078 }
4079
4080 #[test]
4081 fn worklist_no_rewrite_skips_levels_without_candidates_for_subtree() {
4082 let tree = root(vec![int_lit(1)]);
4083 let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
4084 let arena = &surfaces[0].arena;
4085 let ids = rewriter_preorder_ids(arena);
4086 let root_id = ids[0];
4087 let child_id = ids[1];
4088
4089 let rule_groups = test_rule_groups_with_no_candidate_middle_level();
4090 let mut scheduler =
4091 WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
4092 let mut dirty_trace = DirtyTrace::default();
4093
4094 assert_eq!(
4095 scheduler.pop_next(
4096 &surfaces,
4097 &rule_groups,
4098 RewriteConfig::optimised(),
4099 &mut dirty_trace
4100 ),
4101 Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
4102 );
4103
4104 scheduler.enqueue_after_no_rewrite(
4105 WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
4106 root_id,
4107 0,
4108 1,
4109 ScheduledMode::TraverseSubtreeRoot,
4110 Some(&mut dirty_trace),
4111 );
4112
4113 assert_eq!(
4114 scheduler.pop_next(
4115 &surfaces,
4116 &rule_groups,
4117 RewriteConfig::optimised(),
4118 &mut dirty_trace
4119 ),
4120 Some((0, 0, child_id, ScheduledMode::TraverseSubtreeDescendant))
4121 );
4122 assert_eq!(
4123 scheduler.pop_next(
4124 &surfaces,
4125 &rule_groups,
4126 RewriteConfig::optimised(),
4127 &mut dirty_trace
4128 ),
4129 Some((2, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
4130 );
4131 }
4132
4133 #[test]
4134 fn worklist_reference_invalidation_schedules_affected_path_once() {
4135 let x = Name::user("x");
4136 let tree = root(vec![
4137 Expr::Eq(
4138 Metadata::new(),
4139 Moo::new(reference_expr(&x)),
4140 Moo::new(int_lit(1)),
4141 ),
4142 int_lit(2),
4143 ]);
4144 let arena = ExpressionArena::from_root(tree);
4145 let ids = rewriter_preorder_ids(&arena);
4146 let root_id = ids[0];
4147 let eq_id = ids[1];
4148 let reference_id = ids[2];
4149
4150 let mut affected_nodes = Vec::new();
4151 collect_worklist_nodes_referencing_names(
4152 &arena,
4153 arena.root(),
4154 std::slice::from_ref(&x),
4155 &mut affected_nodes,
4156 );
4157
4158 assert_eq!(
4159 affected_nodes.into_iter().rev().collect_vec(),
4160 vec![root_id, eq_id, reference_id]
4161 );
4162 }
4163
4164 #[test]
4165 fn worklist_caches_symbols_only_failures_until_symbols_change() {
4166 let tree = root(vec![int_lit(1)]);
4167 let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
4168 let node_id = surfaces[0].arena.root();
4169 let rule_groups = vec![RuleGroup::new(
4170 1,
4171 vec![crate::rule_engine::RuleData {
4172 rule: &TEST_SYMBOLS_ONLY_RULE,
4173 priority: 1,
4174 rule_set: &TEST_RULE_SET,
4175 }],
4176 )];
4177 let rule_data = &rule_groups[0].rules[0];
4178 let mut scheduler = WorklistScheduler::empty(&rule_groups);
4179
4180 assert!(scheduler.should_attempt_rule(0, node_id, rule_data));
4181 scheduler.record_rule_failure(0, node_id, rule_data);
4182 assert!(!scheduler.should_attempt_rule(0, node_id, rule_data));
4183
4184 scheduler.invalidate_symbol_rule_failures();
4185 assert!(scheduler.should_attempt_rule(0, node_id, rule_data));
4186 }
4187
4188 #[test]
4189 fn evaluator_batches_ancestor_work_until_the_worklist_is_empty() {
4190 let x = Name::user("x");
4191 let and = Expr::And(
4192 Metadata::new(),
4193 Moo::new(matrix_expr![bool_lit(true), reference_expr(&x)]),
4194 );
4195 let mut surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(root(
4196 vec![and],
4197 )))];
4198 let ids = rewriter_preorder_ids(&surfaces[0].arena);
4199 let and_id = ids
4200 .iter()
4201 .copied()
4202 .find(|&node_id| matches!(surfaces[0].arena.expression(node_id), Expr::And(..)))
4203 .expect("and node");
4204 let true_id = ids
4205 .iter()
4206 .copied()
4207 .find(|&node_id| surfaces[0].arena.expression(node_id) == &bool_lit(true))
4208 .expect("true literal");
4209
4210 replace_focus_and_sync_ancestors(&mut surfaces[0].arena, true_id, bool_lit(false));
4211 let mut work = DeferredEvaluatorWork::default();
4212 work.defer_ancestors(&mut surfaces[0].arena, 0, true_id);
4213 work.defer_ancestors(&mut surfaces[0].arena, 0, true_id);
4214 assert_eq!(
4215 work.nodes.len(),
4216 3,
4217 "matrix, `and`, and root ancestors are unique"
4218 );
4219 assert!(matches!(
4220 surfaces[0].arena.expression(and_id),
4221 Expr::And(..)
4222 ));
4223
4224 let rule_groups = Vec::new();
4225 let mut scheduler = WorklistScheduler::empty(&rule_groups);
4226 let mut dirty_trace = DirtyTrace::default();
4227 assert!(normalise_deferred_evaluators(
4228 &mut work,
4229 &mut Model::new(Default::default()),
4230 &mut surfaces,
4231 &mut scheduler,
4232 &mut dirty_trace,
4233 ));
4234 assert_eq!(surfaces[0].arena.expression(and_id), &bool_lit(false));
4235 assert!(work.is_empty());
4236 }
4237}