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_clauses: 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_clauses: !effect.new_clauses.is_empty(),
1668 }
1669 }
1670
1671 fn has_model_side_effects(&self) -> bool {
1672 self.has_new_top || self.has_new_clauses || 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 set_current_rewriter(Rewriter::Rewrite(config));
1797
1798 let needs_abstract_repr = model_needs_abstract_repr_rules(model);
1799 let filtered_rule_sets: Vec<&'a RuleSet<'a>> = if needs_abstract_repr {
1800 rule_sets.clone()
1801 } else {
1802 rule_sets
1803 .iter()
1804 .copied()
1805 .filter(|rs| rs.name != "ReprGeneral" && rs.name != "ReprTuplePacked")
1806 .collect()
1807 };
1808
1809 let rules_grouped = get_rules_grouped(&filtered_rule_sets)
1810 .unwrap_or_else(|_| bug!("get_rule_priorities() failed!"))
1811 .into_iter()
1812 .collect_vec();
1813 let bucketed_rules = rules_grouped
1814 .iter()
1815 .map(|(priority, rules)| RuleGroup::new(*priority, rules.clone()))
1816 .collect_vec();
1817
1818 let mut model = introduce_objective_auxiliary(model.clone());
1819 tighten_domains_from_constraints(&mut model);
1820 let mut rewriter_stats = RewriterStats::new();
1821 rewriter_stats.is_optimisation_enabled = Some(!config.is_baseline());
1822 let mut dirty_trace = DirtyTrace::from_env();
1823 let run_start = Instant::now();
1824
1825 if rule_trace_enabled() && default_rule_trace_enabled() {
1826 trace!(
1827 target: "rule_engine_rule_trace",
1828 "Model before rewriting:\n\n{}\n--\n",
1829 model
1830 );
1831 }
1832 if rule_trace_enabled() && rule_attempt_trace_enabled() {
1833 trace!(
1834 target: "rule_engine_rule_attempt_trace",
1835 "elapsed_s,rule_level,rule_name,rule_set,status,expression"
1836 );
1837 }
1838
1839 let general_attempt_trace_enabled = tracing::enabled!(
1840 target: "conjure::rule_attempt",
1841 tracing::Level::TRACE
1842 );
1843 let csv_attempt_trace_enabled = rule_trace_enabled() && rule_attempt_trace_enabled();
1844
1845 if general_attempt_trace_enabled || csv_attempt_trace_enabled {
1846 let mut observer = TracingObserver {
1847 run_start: &run_start,
1848 general_enabled: general_attempt_trace_enabled,
1849 csv_enabled: csv_attempt_trace_enabled,
1850 };
1851 rewrite_to_fixpoint(
1852 &mut model,
1853 &rules_grouped,
1854 &bucketed_rules,
1855 &mut rewriter_stats,
1856 &mut dirty_trace,
1857 config,
1858 &mut observer,
1859 );
1860 } else {
1861 let mut observer = NoopObserver;
1862 rewrite_to_fixpoint(
1863 &mut model,
1864 &rules_grouped,
1865 &bucketed_rules,
1866 &mut rewriter_stats,
1867 &mut dirty_trace,
1868 config,
1869 &mut observer,
1870 );
1871 }
1872
1873 let run_end = Instant::now();
1874 rewriter_stats.rewriter_run_time = Some(run_end - run_start);
1875 model
1876 .context
1877 .write()
1878 .unwrap()
1879 .stats
1880 .add_rewriter_run(rewriter_stats);
1881 dirty_trace.finish(
1882 model
1883 .context
1884 .read()
1885 .unwrap()
1886 .stats
1887 .rewriter_runs
1888 .last()
1889 .expect("rewriter stats were just added"),
1890 );
1891
1892 if rule_trace_enabled() && default_rule_trace_enabled() {
1893 trace!(
1894 target: "rule_engine_rule_trace",
1895 "Final model:\n\n{}",
1896 model
1897 );
1898 }
1899
1900 if let Some(normalised_root) = finish_root_evaluator_normalisation(model.root()) {
1905 model.replace_root(normalised_root);
1906 }
1907
1908 Ok(model)
1909}
1910
1911#[allow(clippy::too_many_arguments)]
1912fn rewrite_to_fixpoint<'rules, O: RuleAttemptObserver>(
1913 model: &mut Model,
1914 rules_grouped: &Vec<(u16, Vec<RuleData<'rules>>)>,
1915 bucketed_rules: &Vec<RuleGroup<'rules>>,
1916 stats: &mut RewriterStats,
1917 dirty_trace: &mut DirtyTrace,
1918 config: RewriteConfig,
1919 attempt_observer: &mut O,
1920) {
1921 let mut pass_ctx = RewritePassContext {
1922 rules_grouped,
1923 bucketed_rules,
1924 stats,
1925 dirty_trace,
1926 config,
1927 attempt_observer,
1928 };
1929 if config.worklist {
1930 let _ = try_rewrite_model(model, &mut pass_ctx);
1931 } else {
1932 let mut done_something = true;
1933 while done_something {
1934 done_something = try_rewrite_model(model, &mut pass_ctx).is_some();
1935 }
1936 }
1937}
1938
1939fn try_rewrite_model<'ctx, 'rules, O: RuleAttemptObserver>(
1943 submodel: &mut Model,
1944 ctx: &mut RewritePassContext<'ctx, 'rules, O>,
1945) -> Option<()> {
1946 ctx.dirty_trace.passes += 1;
1947 if !ctx.config.worklist
1948 && try_rewrite_value_letting_once(submodel, ctx.rules_grouped, ctx.attempt_observer)
1949 .is_some()
1950 {
1951 ctx.dirty_trace.value_letting_rewrites += 1;
1952 increment_counter(&mut ctx.stats.rewriter_value_letting_rewrites);
1953 return Some(());
1954 }
1955
1956 let mut did_rewrite = false;
1957 let mut arena = ExpressionArena::from_root(take_model_root(submodel));
1958 normalise_evaluators_bottom_up(&mut arena, ctx.dirty_trace);
1959
1960 if ctx.config.worklist {
1961 return try_rewrite_model_with_worklist(submodel, ctx, arena);
1962 }
1963
1964 'rewrite_loop: loop {
1965 let mut results: Vec<ApplicableRule<'_, ExpressionNodeId>> = vec![];
1966 let preorder_ids = rewriter_preorder_ids(&arena);
1967 'top: for (level, rule_group) in ctx.bucketed_rules.iter().enumerate() {
1969 ctx.dirty_trace.priority_scans += 1;
1970 for &node_id in &preorder_ids {
1971 ctx.dirty_trace.expression_visits += 1;
1972 let mut attempted_rule = false;
1973 {
1974 let expr = arena.expression(node_id);
1975 for rd in rule_group.candidates(ctx.config, expr) {
1976 attempted_rule = true;
1977 ctx.dirty_trace
1978 .record_rule_attempt(rule_group.priority, rd.rule.name);
1979 ctx.stats.rewriter_rule_application_attempts =
1981 Some(ctx.stats.rewriter_rule_application_attempts.unwrap_or(0) + 1);
1982
1983 match (rd.rule.application)(expr, &submodel.symbols()) {
1984 Ok(red) => {
1985 ctx.attempt_observer.attempted(
1986 rule_group.priority,
1987 rd,
1988 expr,
1989 RuleAttemptStatus::Success,
1990 );
1991
1992 ctx.stats.rewriter_rule_applications =
1994 Some(ctx.stats.rewriter_rule_applications.unwrap_or(0) + 1);
1995
1996 results.push((
1998 RuleResult {
1999 rule_data: rd.clone(),
2000 effect: red,
2001 },
2002 level,
2003 expr.clone(),
2004 node_id,
2005 root_variable_snapshot_for_default_trace(
2006 expr,
2007 &submodel.symbols(),
2008 ),
2009 ));
2010 }
2011 Err(_) => {
2012 ctx.attempt_observer.attempted(
2013 rule_group.priority,
2014 rd,
2015 expr,
2016 RuleAttemptStatus::Failure,
2017 );
2018 }
2019 }
2020 }
2021 }
2022 if attempted_rule {
2023 ctx.dirty_trace.attempted_expressions += 1;
2024 }
2025 if !results.is_empty() {
2028 break 'top;
2029 }
2030 }
2031 }
2032
2033 if !results.is_empty() {
2034 let selected =
2035 choose_rule_result_index(results.iter().map(|(result, _, _, _, _)| result));
2036 results.swap(0, selected);
2037 }
2038
2039 match results.into_iter().next() {
2040 None => {
2041 submodel.replace_root(arena.into_synced_root_expression());
2042 break;
2043 }
2044 Some((result, _level, expr, node_id, variable_snapshot_before)) => {
2045 let effect = result.effect.materialise(&submodel.symbols());
2046 let variable_snapshots = variable_snapshot_before.map(|before| {
2047 let after = snapshot_symbols_after_effect(&submodel.symbols(), &effect);
2048 (before, after)
2049 });
2050 let result = RuleResult {
2051 rule_data: result.rule_data,
2052 effect,
2053 };
2054
2055 log_rule_application(
2057 &result,
2058 &expr,
2059 &submodel.symbols(),
2060 variable_snapshots
2061 .as_ref()
2062 .map(|(before, after)| (before, after)),
2063 );
2064
2065 let effect_impact = RuleEffectImpact::new(&result.effect, &submodel.symbols());
2066 let has_model_side_effects = effect_impact.has_model_side_effects();
2067 let rule_name = result.rule_data.rule.name;
2068 let RuleResult { effect, .. } = result;
2069 let crate::rule_engine::rule::RuleEffect {
2070 new_expression,
2071 new_top,
2072 symbols,
2073 new_clauses,
2074 declaration_updates,
2075 ..
2076 } = effect;
2077 replace_focus_and_sync_ancestors(&mut arena, node_id, new_expression);
2079
2080 ctx.dirty_trace
2082 .record_rewrite(rule_name, has_model_side_effects);
2083 for update in declaration_updates {
2084 update.apply();
2085 }
2086 submodel.symbols_mut().extend(symbols);
2087 if effect_impact.has_new_top {
2088 arena.add_root_children(new_top);
2089 }
2090 submodel.add_clauses(new_clauses);
2091 let _ =
2092 normalise_evaluators_from_node_to_root(&mut arena, node_id, ctx.dirty_trace);
2093 if has_model_side_effects {
2094 ctx.dirty_trace.record_side_effect_kept_in_arena();
2095 }
2096
2097 #[cfg(debug_assertions)]
2098 {
2099 submodel.replace_root(arena.expression(arena.root()).clone());
2102 let assertion_context = format!("rewriter after applying rule '{rule_name}'");
2103 debug_assert_model_well_formed(submodel, &assertion_context);
2104 }
2105
2106 did_rewrite = true;
2107 continue 'rewrite_loop;
2108 }
2109 }
2110 }
2111
2112 did_rewrite.then_some(())
2113}
2114
2115fn try_rewrite_model_with_worklist<'ctx, 'rules, O: RuleAttemptObserver>(
2116 submodel: &mut Model,
2117 ctx: &mut RewritePassContext<'ctx, 'rules, O>,
2118 arena: ExpressionArena,
2119) -> Option<()> {
2120 let mut did_rewrite = false;
2121 let root_surface = 0usize;
2122 let (mut surfaces, mut value_letting_surfaces) = build_worklist_surfaces(submodel, arena);
2123 for surface in &mut surfaces {
2124 if surface.active {
2125 normalise_evaluators_bottom_up(&mut surface.arena, ctx.dirty_trace);
2126 }
2127 }
2128 write_worklist_surfaces_to_model(submodel, &surfaces);
2129 let mut scheduler = WorklistScheduler::new(&surfaces, ctx.bucketed_rules, ctx.config);
2130 let mut deferred_evaluators = DeferredEvaluatorWork::default();
2131 let mut deferred_ancestor_rules = DeferredAncestorRuleWork::default();
2132
2133 loop {
2134 let next = loop {
2135 let candidate =
2136 scheduler.pop_next(&surfaces, ctx.bucketed_rules, ctx.config, ctx.dirty_trace);
2137 if let Some((_, surface, node_id, _)) = candidate
2138 && deferred_ancestor_rules.contains(surface, node_id)
2139 {
2140 deferred_ancestor_rules.postpone(candidate.unwrap());
2141 continue;
2142 }
2143 break candidate;
2144 };
2145 let crosses_priority_boundary =
2146 deferred_ancestor_rules.should_flush_before(next.as_ref().map(|item| item.0));
2147 if crosses_priority_boundary {
2148 if let Some((level, surface, node_id, mode)) = next {
2151 scheduler.enqueue_node_at_level(
2152 &surfaces[surface].arena,
2153 surface,
2154 node_id,
2155 level,
2156 mode,
2157 Some(ctx.dirty_trace),
2158 );
2159 }
2160 deferred_ancestor_rules.flush(
2161 &mut scheduler,
2162 &mut surfaces,
2163 &mut deferred_evaluators,
2164 ctx.dirty_trace,
2165 );
2166 continue;
2167 }
2168
2169 let Some((level, surface_index, node_id, scheduled_mode)) = next else {
2170 if normalise_deferred_evaluators(
2171 &mut deferred_evaluators,
2172 submodel,
2173 &mut surfaces,
2174 &mut scheduler,
2175 ctx.dirty_trace,
2176 ) {
2177 did_rewrite = true;
2178 continue;
2179 }
2180 break;
2181 };
2182 ctx.dirty_trace.priority_scans += 1;
2183 ctx.dirty_trace.expression_visits += 1;
2184
2185 let rule_group = &ctx.bucketed_rules[level];
2186 if !rule_group.has_candidates_at_node(ctx.config, &surfaces[surface_index].arena, node_id) {
2187 ctx.dirty_trace
2188 .record_worklist_no_candidate_pop(scheduled_mode);
2189 scheduler.enqueue_after_no_rewrite(
2190 WorklistSchedulingContext::new(
2191 &surfaces[surface_index].arena,
2192 surface_index,
2193 ctx.bucketed_rules,
2194 ctx.config,
2195 ),
2196 node_id,
2197 level,
2198 level + 1,
2199 scheduled_mode,
2200 Some(ctx.dirty_trace),
2201 );
2202 continue;
2203 }
2204
2205 let mut results: Vec<ApplicableRule<'_, ExpressionNodeId>> = vec![];
2206 let mut attempted_rule = false;
2207 {
2208 let arena = &surfaces[surface_index].arena;
2209 let expr = arena.expression(node_id);
2210 for rd in rule_group.candidates_at_node(ctx.config, arena, node_id) {
2211 if !scheduler.should_attempt_rule(surface_index, node_id, rd) {
2212 continue;
2213 }
2214 attempted_rule = true;
2215 ctx.dirty_trace
2216 .record_rule_attempt(rule_group.priority, rd.rule.name);
2217 ctx.stats.rewriter_rule_application_attempts =
2218 Some(ctx.stats.rewriter_rule_application_attempts.unwrap_or(0) + 1);
2219
2220 match (rd.rule.application)(expr, &submodel.symbols()) {
2221 Ok(red) => {
2222 ctx.attempt_observer.attempted(
2223 rule_group.priority,
2224 rd,
2225 expr,
2226 RuleAttemptStatus::Success,
2227 );
2228
2229 ctx.stats.rewriter_rule_applications =
2230 Some(ctx.stats.rewriter_rule_applications.unwrap_or(0) + 1);
2231
2232 results.push((
2233 RuleResult {
2234 rule_data: rd.clone(),
2235 effect: red,
2236 },
2237 level,
2238 expr.clone(),
2239 node_id,
2240 root_variable_snapshot_for_default_trace(expr, &submodel.symbols()),
2241 ));
2242 }
2243 Err(_) => {
2244 scheduler.record_rule_failure(surface_index, node_id, rd);
2245 ctx.attempt_observer.attempted(
2246 rule_group.priority,
2247 rd,
2248 expr,
2249 RuleAttemptStatus::Failure,
2250 );
2251 }
2252 }
2253 }
2254 }
2255
2256 if attempted_rule {
2257 ctx.dirty_trace.attempted_expressions += 1;
2258 ctx.dirty_trace
2259 .record_worklist_rule_attempt_pop(scheduled_mode);
2260 }
2261
2262 if results.is_empty() {
2263 scheduler.enqueue_after_no_rewrite(
2264 WorklistSchedulingContext::new(
2265 &surfaces[surface_index].arena,
2266 surface_index,
2267 ctx.bucketed_rules,
2268 ctx.config,
2269 ),
2270 node_id,
2271 level,
2272 level + 1,
2273 scheduled_mode,
2274 Some(ctx.dirty_trace),
2275 );
2276 continue;
2277 }
2278
2279 let selected = choose_rule_result_index(results.iter().map(|(result, _, _, _, _)| result));
2280 let (result, _level, expr, node_id, variable_snapshot_before) =
2281 results.swap_remove(selected);
2282
2283 let effect = result.effect.materialise(&submodel.symbols());
2284 let variable_snapshots = variable_snapshot_before.map(|before| {
2285 let after = snapshot_symbols_after_effect(&submodel.symbols(), &effect);
2286 (before, after)
2287 });
2288 let result = RuleResult {
2289 rule_data: result.rule_data,
2290 effect,
2291 };
2292
2293 log_rule_application(
2294 &result,
2295 &expr,
2296 &submodel.symbols(),
2297 variable_snapshots
2298 .as_ref()
2299 .map(|(before, after)| (before, after)),
2300 );
2301
2302 let effect_impact = RuleEffectImpact::new(&result.effect, &submodel.symbols());
2303 let has_model_side_effects = effect_impact.has_model_side_effects();
2304 let rewritten_value_letting_name =
2305 value_letting_surface_name(&surfaces[surface_index].kind).cloned();
2306 let invalidates_symbol_rules =
2307 effect_impact.has_symbol_changes() || rewritten_value_letting_name.is_some();
2308 let rule_name = result.rule_data.rule.name;
2309 let RuleResult { effect, .. } = result;
2310 let crate::rule_engine::rule::RuleEffect {
2311 new_expression,
2312 new_top,
2313 symbols,
2314 new_clauses,
2315 declaration_updates,
2316 ..
2317 } = effect;
2318 {
2319 let arena = &mut surfaces[surface_index].arena;
2320 arena.replace_subtree(node_id, new_expression);
2321 }
2322
2323 ctx.dirty_trace
2324 .record_rewrite(rule_name, has_model_side_effects);
2325 for update in declaration_updates {
2326 update.apply();
2327 }
2328 submodel.symbols_mut().extend(symbols);
2329 let new_top_node_ids = if effect_impact.has_new_top {
2330 surfaces[root_surface].arena.add_root_children(new_top)
2331 } else {
2332 Vec::new()
2333 };
2334 submodel.add_clauses(new_clauses);
2335 let rewrite_impact_node_id = {
2336 let arena = &mut surfaces[surface_index].arena;
2337 normalise_evaluators_subtree_bottom_up(arena, node_id, ctx.dirty_trace);
2338 deferred_evaluators.defer_ancestors(arena, surface_index, node_id);
2339 deferred_ancestor_rules.defer_ancestors(arena, surface_index, node_id, level);
2340 node_id
2341 };
2342 scheduler.index_subtree_references(
2343 &surfaces[surface_index].arena,
2344 surface_index,
2345 rewrite_impact_node_id,
2346 );
2347 for &new_top_node_id in &new_top_node_ids {
2348 if surfaces[root_surface].arena.is_reachable(new_top_node_id) {
2349 normalise_evaluators_subtree_bottom_up(
2350 &mut surfaces[root_surface].arena,
2351 new_top_node_id,
2352 ctx.dirty_trace,
2353 );
2354 deferred_evaluators.defer_ancestors(
2355 &surfaces[root_surface].arena,
2356 root_surface,
2357 new_top_node_id,
2358 );
2359 deferred_ancestor_rules.defer_ancestors(
2360 &surfaces[root_surface].arena,
2361 root_surface,
2362 new_top_node_id,
2363 level,
2364 );
2365 scheduler.index_subtree_references(
2366 &surfaces[root_surface].arena,
2367 root_surface,
2368 new_top_node_id,
2369 );
2370 }
2371 }
2372 if let Some(name) = rewritten_value_letting_name.as_ref() {
2373 write_value_letting_surface_to_model(submodel, name, &surfaces[surface_index].arena);
2374 ctx.dirty_trace.value_letting_rewrites += 1;
2375 increment_counter(&mut ctx.stats.rewriter_value_letting_rewrites);
2376 }
2377 let mut affected_names = effect_impact.invalidated_reference_names.clone();
2378 if let Some(name) = rewritten_value_letting_name.as_ref()
2379 && !affected_names.contains(name)
2380 {
2381 affected_names.push(name.clone());
2382 }
2383 let mut symbol_surface_names = effect_impact.added_names.clone();
2384 symbol_surface_names.extend(effect_impact.changed_names.iter().cloned());
2385 if let Some(name) = rewritten_value_letting_name.as_ref() {
2386 symbol_surface_names.retain(|candidate| candidate != name);
2387 }
2388 let synced_surfaces = sync_value_letting_surfaces(
2389 submodel,
2390 &mut surfaces,
2391 &mut value_letting_surfaces,
2392 &symbol_surface_names,
2393 );
2394
2395 if has_model_side_effects {
2401 deferred_ancestor_rules.flush_effect_path_before_root(
2402 &mut scheduler,
2403 &mut surfaces,
2404 &mut deferred_evaluators,
2405 surface_index,
2406 rewrite_impact_node_id,
2407 ctx.dirty_trace,
2408 );
2409 }
2410 {
2411 if has_model_side_effects {
2412 ctx.dirty_trace.record_side_effect_kept_in_arena();
2413 }
2414 if invalidates_symbol_rules {
2415 scheduler.invalidate_symbol_rule_failures();
2416 scheduler.enqueue_node_at_level(
2417 &surfaces[root_surface].arena,
2418 root_surface,
2419 surfaces[root_surface].arena.root(),
2420 0,
2421 ScheduledMode::CheckNode,
2422 Some(ctx.dirty_trace),
2423 );
2424 }
2425 scheduler.enqueue_subtree(
2426 &surfaces[surface_index].arena,
2427 surface_index,
2428 rewrite_impact_node_id,
2429 Some(ctx.dirty_trace),
2430 );
2431 for new_top_node_id in new_top_node_ids {
2432 scheduler.enqueue_subtree(
2433 &surfaces[root_surface].arena,
2434 root_surface,
2435 new_top_node_id,
2436 Some(ctx.dirty_trace),
2437 );
2438 }
2439 for synced_surface in synced_surfaces {
2440 scheduler.enqueue_surface(
2441 &surfaces,
2442 synced_surface,
2443 ctx.bucketed_rules,
2444 ctx.config,
2445 Some(ctx.dirty_trace),
2446 );
2447 }
2448 enqueue_worklist_nodes_referencing_names(
2449 &mut scheduler,
2450 &surfaces,
2451 &affected_names,
2452 ctx.dirty_trace,
2453 );
2454 }
2455
2456 did_rewrite = true;
2457 }
2458
2459 move_worklist_surfaces_to_model(submodel, surfaces);
2460 #[cfg(debug_assertions)]
2461 debug_assert_model_well_formed(submodel, "rewriter after a settled worklist pass");
2462 did_rewrite.then_some(())
2463}
2464
2465#[cfg(test)]
2466fn enqueue_worklist_rewrite_impact(
2467 scheduler: &mut WorklistScheduler,
2468 arena: &ExpressionArena,
2469 surface: usize,
2470 node_id: ExpressionNodeId,
2471 dirty_trace: &mut DirtyTrace,
2472) {
2473 scheduler.enqueue_node_and_ancestors(arena, surface, node_id, dirty_trace);
2474 scheduler.enqueue_subtree(arena, surface, node_id, Some(dirty_trace));
2475}
2476
2477fn normalise_deferred_evaluators(
2484 work: &mut DeferredEvaluatorWork,
2485 submodel: &mut Model,
2486 surfaces: &mut [RewriteSurface],
2487 scheduler: &mut WorklistScheduler,
2488 dirty_trace: &mut DirtyTrace,
2489) -> bool {
2490 if work.is_empty() {
2491 return false;
2492 }
2493
2494 let nodes = deferred_nodes_in_bottom_up_order(std::mem::take(&mut work.nodes), surfaces);
2495
2496 let mut affected_surfaces = HashSet::new();
2497 let mut changed_value_lettings = HashSet::new();
2498 for (surface, node_id, _) in nodes {
2499 let Some(rewrite_surface) = surfaces.get_mut(surface) else {
2500 continue;
2501 };
2502 if !rewrite_surface.active || !rewrite_surface.arena.is_reachable(node_id) {
2503 continue;
2504 }
2505 affected_surfaces.insert(surface);
2506 rewrite_surface.arena.rebuild_payload_from_children(node_id);
2507
2508 if node_id != rewrite_surface.arena.root()
2510 && normalise_evaluator_node_to_fixpoint_without_ancestor_sync(
2511 &mut rewrite_surface.arena,
2512 node_id,
2513 dirty_trace,
2514 )
2515 {
2516 scheduler.index_subtree_references(&rewrite_surface.arena, surface, node_id);
2517 if let Some(name) = value_letting_surface_name(&rewrite_surface.kind) {
2518 changed_value_lettings.insert(name.clone());
2519 }
2520 }
2521 }
2522
2523 let mut wrote_value_letting = false;
2524 let mut changed_value_lettings: Vec<_> = changed_value_lettings.into_iter().collect();
2525 changed_value_lettings.sort();
2526 for name in &changed_value_lettings {
2527 let Some(surface) = surfaces.iter().find(|surface| {
2528 surface.active && value_letting_surface_name(&surface.kind) == Some(name)
2529 }) else {
2530 continue;
2531 };
2532 wrote_value_letting |=
2533 write_value_letting_surface_to_model_without_refresh(submodel, name, &surface.arena);
2534 }
2535 if wrote_value_letting {
2536 submodel.symbols_mut().refresh_local_binding_hashes();
2537 scheduler.invalidate_symbol_rule_failures();
2538 scheduler.enqueue_node_at_level(
2539 &surfaces[0].arena,
2540 0,
2541 surfaces[0].arena.root(),
2542 0,
2543 ScheduledMode::CheckNode,
2544 Some(dirty_trace),
2545 );
2546 enqueue_worklist_nodes_referencing_names(
2547 scheduler,
2548 surfaces,
2549 &changed_value_lettings,
2550 dirty_trace,
2551 );
2552 }
2553
2554 let mut affected_surfaces: Vec<_> = affected_surfaces.into_iter().collect();
2555 affected_surfaces.sort_unstable();
2556 for surface in affected_surfaces.iter().copied() {
2557 let arena = &surfaces[surface].arena;
2558 scheduler.enqueue_subtree(arena, surface, arena.root(), Some(dirty_trace));
2559 }
2560
2561 !affected_surfaces.is_empty()
2562}
2563
2564fn deferred_nodes_in_bottom_up_order(
2565 nodes: impl IntoIterator<Item = (usize, ExpressionNodeId)>,
2566 surfaces: &[RewriteSurface],
2567) -> Vec<(usize, ExpressionNodeId, Option<Vec<usize>>)> {
2568 let mut nodes: Vec<_> = nodes
2571 .into_iter()
2572 .map(|(surface, node_id)| {
2573 let path = surfaces
2574 .get(surface)
2575 .filter(|rewrite_surface| rewrite_surface.active)
2576 .filter(|rewrite_surface| rewrite_surface.arena.is_reachable(node_id))
2577 .map(|rewrite_surface| rewrite_surface.arena.preorder_path(node_id));
2578 (surface, node_id, path)
2579 })
2580 .collect();
2581 nodes.sort_by(
2582 |(left_surface, left_node, left_path), (right_surface, right_node, right_path)| {
2583 right_path
2584 .as_ref()
2585 .map_or(0, Vec::len)
2586 .cmp(&left_path.as_ref().map_or(0, Vec::len))
2587 .then_with(|| left_surface.cmp(right_surface))
2588 .then_with(|| match (left_path, right_path) {
2589 (Some(left_path), Some(right_path)) => left_path.cmp(right_path),
2590 _ => left_node.cmp(right_node),
2591 })
2592 },
2593 );
2594 nodes
2595}
2596
2597fn normalise_evaluators_bottom_up(arena: &mut ExpressionArena, dirty_trace: &mut DirtyTrace) {
2604 normalise_evaluators_subtree_bottom_up(arena, arena.root(), dirty_trace);
2605}
2606
2607fn normalise_evaluators_subtree_bottom_up(
2608 arena: &mut ExpressionArena,
2609 subtree_root: ExpressionNodeId,
2610 dirty_trace: &mut DirtyTrace,
2611) -> bool {
2612 let nodes = rewriter_reachable_subtree_ids(arena, subtree_root);
2613 let mut changed = false;
2614
2615 for node_id in nodes.into_iter().rev() {
2616 if !arena.is_reachable(node_id) {
2617 continue;
2618 }
2619 changed |=
2620 normalise_evaluator_node_to_fixpoint_without_ancestor_sync(arena, node_id, dirty_trace);
2621
2622 if node_id != subtree_root
2627 && let Some(parent_id) = arena.parent(node_id)
2628 && arena.is_reachable(parent_id)
2629 {
2630 arena.sync_payload_for_changed_child(parent_id, node_id);
2631 }
2632 }
2633
2634 changed
2635}
2636
2637fn normalise_evaluators_from_node_to_root(
2645 arena: &mut ExpressionArena,
2646 node_id: ExpressionNodeId,
2647 dirty_trace: &mut DirtyTrace,
2648) -> (ExpressionNodeId, bool) {
2649 let subtree_changed = normalise_evaluators_subtree_bottom_up(arena, node_id, dirty_trace);
2653 let mut came_from = node_id;
2654 let mut current = arena.parent(node_id);
2655 let mut highest_rewritten = node_id;
2656 let mut changed = subtree_changed;
2657
2658 while let Some(current_id) = current {
2659 if !arena.is_reachable(current_id) {
2660 break;
2661 }
2662
2663 let rewritten = if current_id == arena.root() {
2664 normalise_root_evaluator_for_child(arena, came_from, dirty_trace)
2668 } else {
2669 normalise_evaluator_node_to_fixpoint(arena, current_id, dirty_trace)
2670 };
2671 if rewritten {
2672 highest_rewritten = current_id;
2673 changed = true;
2674 }
2675 came_from = current_id;
2676 current = arena.parent(current_id);
2677 }
2678
2679 (highest_rewritten, changed)
2680}
2681
2682fn normalise_root_evaluator_for_child(
2690 arena: &mut ExpressionArena,
2691 child_id: ExpressionNodeId,
2692 dirty_trace: &mut DirtyTrace,
2693) -> bool {
2694 let root_id = arena.root();
2695 if !arena.children(root_id).contains(&child_id) {
2696 return normalise_evaluator_node_to_fixpoint(arena, root_id, dirty_trace);
2697 }
2698
2699 let Some(replacement) = normalise_root_constraint_deep(arena.expression(child_id)) else {
2700 return false;
2701 };
2702
2703 dirty_trace.replacement_subtree_clears += 1;
2704 arena.replace_subtree(child_id, replacement);
2705 dirty_trace.record_rewrite("evaluator_normalisation_hook", false);
2706 sync_ancestor_payloads(arena, child_id);
2707 true
2708}
2709
2710fn normalise_evaluator_node_to_fixpoint(
2711 arena: &mut ExpressionArena,
2712 node_id: ExpressionNodeId,
2713 dirty_trace: &mut DirtyTrace,
2714) -> bool {
2715 let changed =
2716 normalise_evaluator_node_to_fixpoint_without_ancestor_sync(arena, node_id, dirty_trace);
2717
2718 if changed {
2719 sync_ancestor_payloads(arena, node_id);
2720 }
2721
2722 changed
2723}
2724
2725fn normalise_evaluator_node_to_fixpoint_without_ancestor_sync(
2726 arena: &mut ExpressionArena,
2727 node_id: ExpressionNodeId,
2728 dirty_trace: &mut DirtyTrace,
2729) -> bool {
2730 let mut changed = false;
2731
2732 while arena.is_reachable(node_id) {
2733 let expr = arena.expression(node_id);
2734 let Some(replacement) = normalise_evaluator_local(expr) else {
2735 break;
2736 };
2737
2738 dirty_trace.replacement_subtree_clears += 1;
2739 arena.replace_subtree(node_id, replacement);
2740 dirty_trace.record_rewrite("evaluator_normalisation_hook", false);
2741 changed = true;
2742 }
2743
2744 changed
2745}
2746
2747fn build_worklist_surfaces(
2748 submodel: &Model,
2749 root_arena: ExpressionArena,
2750) -> (Vec<RewriteSurface>, HashMap<Name, usize>) {
2751 let mut surfaces = vec![RewriteSurface::root(root_arena)];
2752 let mut value_letting_surfaces = HashMap::new();
2753
2754 let symbols = submodel.symbols();
2755 for (name, decl) in symbols.iter_local() {
2756 let letting_expr = decl.as_value_letting().map(|expr| expr.clone());
2757 if let Some(expr) = letting_expr {
2758 let surface = surfaces.len();
2759 surfaces.push(RewriteSurface::value_letting(name.clone(), expr));
2760 value_letting_surfaces.insert(name.clone(), surface);
2761 }
2762 }
2763
2764 (surfaces, value_letting_surfaces)
2765}
2766
2767fn value_letting_surface_name(kind: &RewriteSurfaceKind) -> Option<&Name> {
2768 match kind {
2769 RewriteSurfaceKind::Root => None,
2770 RewriteSurfaceKind::ValueLetting { name } => Some(name),
2771 }
2772}
2773
2774fn current_value_letting_expression(submodel: &Model, name: &Name) -> Option<Expr> {
2775 let declaration = {
2776 let symbols = submodel.symbols();
2777 symbols.lookup_local(name)
2778 }?;
2779 declaration.as_value_letting().map(|expr| expr.clone())
2780}
2781
2782fn sync_value_letting_surfaces(
2783 submodel: &Model,
2784 surfaces: &mut Vec<RewriteSurface>,
2785 value_letting_surfaces: &mut HashMap<Name, usize>,
2786 names: &[Name],
2787) -> Vec<usize> {
2788 let mut synced_surfaces = Vec::new();
2789 let mut seen = HashSet::new();
2790
2791 for name in names {
2792 if !seen.insert(name.clone()) {
2793 continue;
2794 }
2795
2796 if let Some(expr) = current_value_letting_expression(submodel, name) {
2797 if let Some(old_surface) = value_letting_surfaces.get(name).copied()
2798 && let Some(surface) = surfaces.get_mut(old_surface)
2799 {
2800 surface.active = false;
2801 }
2802 let new_surface = surfaces.len();
2803 surfaces.push(RewriteSurface::value_letting(name.clone(), expr));
2804 value_letting_surfaces.insert(name.clone(), new_surface);
2805 synced_surfaces.push(new_surface);
2806 } else if let Some(old_surface) = value_letting_surfaces.remove(name)
2807 && let Some(surface) = surfaces.get_mut(old_surface)
2808 {
2809 surface.active = false;
2810 }
2811 }
2812
2813 synced_surfaces
2814}
2815
2816fn write_worklist_surfaces_to_model(submodel: &mut Model, surfaces: &[RewriteSurface]) {
2817 let root = surfaces[0]
2818 .arena
2819 .expression(surfaces[0].arena.root())
2820 .clone();
2821 submodel.replace_root(root);
2822
2823 let mut wrote_value_letting = false;
2824 for surface in surfaces.iter().skip(1) {
2825 if !surface.active {
2826 continue;
2827 }
2828 let Some(name) = value_letting_surface_name(&surface.kind) else {
2829 continue;
2830 };
2831 let expression = surface.arena.expression(surface.arena.root()).clone();
2832 wrote_value_letting |=
2833 write_value_letting_expression_to_model_without_refresh(submodel, name, expression);
2834 }
2835 if wrote_value_letting {
2836 submodel.symbols_mut().refresh_local_binding_hashes();
2837 }
2838}
2839
2840fn move_worklist_surfaces_to_model(submodel: &mut Model, surfaces: Vec<RewriteSurface>) {
2841 let mut surfaces = surfaces.into_iter();
2842 let root = surfaces.next().expect("worklist must have a root surface");
2843 submodel.replace_root(root.arena.into_synced_root_expression());
2844
2845 let mut wrote_value_letting = false;
2846 for surface in surfaces {
2847 if !surface.active {
2848 continue;
2849 }
2850 let RewriteSurfaceKind::ValueLetting { name } = surface.kind else {
2851 continue;
2852 };
2853 wrote_value_letting |= write_value_letting_expression_to_model_without_refresh(
2854 submodel,
2855 &name,
2856 surface.arena.into_synced_root_expression(),
2857 );
2858 }
2859 if wrote_value_letting {
2860 submodel.symbols_mut().refresh_local_binding_hashes();
2861 }
2862}
2863
2864fn write_value_letting_surface_to_model(
2865 submodel: &mut Model,
2866 name: &Name,
2867 arena: &ExpressionArena,
2868) -> bool {
2869 let written = write_value_letting_surface_to_model_without_refresh(submodel, name, arena);
2870 if written {
2871 submodel.symbols_mut().refresh_local_binding_hashes();
2872 }
2873 written
2874}
2875
2876fn write_value_letting_surface_to_model_without_refresh(
2877 submodel: &mut Model,
2878 name: &Name,
2879 arena: &ExpressionArena,
2880) -> bool {
2881 write_value_letting_expression_to_model_without_refresh(
2882 submodel,
2883 name,
2884 arena.expression_from(arena.root()),
2885 )
2886}
2887
2888fn write_value_letting_expression_to_model_without_refresh(
2889 submodel: &mut Model,
2890 name: &Name,
2891 expression: Expr,
2892) -> bool {
2893 let declaration = {
2894 let symbols = submodel.symbols();
2895 symbols.lookup_local(name)
2896 };
2897 let Some(mut declaration) = declaration else {
2898 return false;
2899 };
2900 {
2901 let Some(mut letting) = declaration.as_value_letting_mut() else {
2902 return false;
2903 };
2904
2905 *letting = expression;
2906 }
2907 true
2908}
2909
2910fn enqueue_worklist_nodes_referencing_names(
2911 scheduler: &mut WorklistScheduler,
2912 surfaces: &[RewriteSurface],
2913 names: &[Name],
2914 dirty_trace: &mut DirtyTrace,
2915) {
2916 if names.is_empty() {
2917 return;
2918 }
2919
2920 let mut affected_nodes = BTreeSet::new();
2923 for name in names {
2924 let Some(reference_nodes) = scheduler.reference_nodes_by_name.get(name) else {
2925 continue;
2926 };
2927 for &(surface_index, reference_node) in reference_nodes {
2928 let Some(surface) = surfaces.get(surface_index) else {
2929 continue;
2930 };
2931 if !surface.active
2932 || !surface.arena.is_reachable(reference_node)
2933 || !expression_directly_references_any(
2934 surface.arena.expression(reference_node),
2935 std::slice::from_ref(name),
2936 )
2937 {
2938 continue;
2939 }
2940
2941 let mut current = Some(reference_node);
2942 while let Some(node_id) = current {
2943 affected_nodes.insert((surface_index, node_id));
2944 current = surface.arena.parent(node_id);
2945 }
2946 }
2947 }
2948
2949 for (surface_index, node_id) in affected_nodes {
2950 let surface = &surfaces[surface_index];
2951 scheduler.enqueue_node_at_level(
2952 &surface.arena,
2953 surface_index,
2954 node_id,
2955 0,
2956 ScheduledMode::CheckNode,
2957 Some(dirty_trace),
2958 );
2959 }
2960}
2961
2962#[cfg(test)]
2963fn collect_worklist_nodes_referencing_names(
2964 arena: &ExpressionArena,
2965 node_id: ExpressionNodeId,
2966 names: &[Name],
2967 affected_nodes: &mut Vec<ExpressionNodeId>,
2968) -> bool {
2969 if !arena.is_reachable(node_id) {
2970 return false;
2971 }
2972
2973 let mut references_changed_name =
2974 expression_directly_references_any(arena.expression(node_id), names);
2975 if !matches!(arena.expression(node_id), Expr::Comprehension(_, _)) {
2976 for &child_id in arena.children(node_id) {
2977 references_changed_name |=
2978 collect_worklist_nodes_referencing_names(arena, child_id, names, affected_nodes);
2979 }
2980 }
2981
2982 if references_changed_name {
2983 affected_nodes.push(node_id);
2984 }
2985 references_changed_name
2986}
2987
2988fn expression_directly_references_any(expr: &Expr, names: &[Name]) -> bool {
2989 let Expr::Atomic(_, Atom::Reference(reference)) = expr else {
2990 return false;
2991 };
2992 names.iter().any(|name| &*reference.name() == name)
2993}
2994
2995fn increment_counter(counter: &mut Option<usize>) {
2996 *counter = Some(counter.unwrap_or(0) + 1);
2997}
2998
2999fn rewriter_preorder_ids(arena: &ExpressionArena) -> Vec<ExpressionNodeId> {
3001 rewriter_reachable_subtree_ids(arena, arena.root())
3002}
3003
3004fn rewriter_reachable_subtree_ids(
3009 arena: &ExpressionArena,
3010 node_id: ExpressionNodeId,
3011) -> Vec<ExpressionNodeId> {
3012 fn collect(
3013 arena: &ExpressionArena,
3014 node_id: ExpressionNodeId,
3015 nodes: &mut Vec<ExpressionNodeId>,
3016 ) {
3017 if !arena.is_reachable(node_id) {
3018 return;
3019 }
3020 nodes.push(node_id);
3021 if matches!(arena.expression(node_id), Expr::Comprehension(_, _)) {
3022 return;
3023 }
3024
3025 for child in arena.children(node_id) {
3026 collect(arena, *child, nodes);
3027 }
3028 }
3029
3030 let mut nodes = Vec::new();
3031 collect(arena, node_id, &mut nodes);
3032 nodes
3033}
3034
3035fn replace_focus_and_sync_ancestors(
3037 arena: &mut ExpressionArena,
3038 node_id: ExpressionNodeId,
3039 new_focus: Expr,
3040) {
3041 arena.replace_subtree(node_id, new_focus);
3042 sync_ancestor_payloads(arena, node_id);
3043}
3044
3045fn sync_ancestor_payloads(arena: &mut ExpressionArena, node_id: ExpressionNodeId) {
3046 let mut child_id = node_id;
3047 let mut ancestor = arena.parent(node_id);
3048 while let Some(ancestor_id) = ancestor {
3049 arena.sync_payload_for_changed_child(ancestor_id, child_id);
3051 child_id = ancestor_id;
3052 ancestor = arena.parent(ancestor_id);
3053 }
3054}
3055
3056fn take_model_root(model: &mut Model) -> Expr {
3057 model.replace_root(Expr::Root(Metadata::new(), Vec::new()))
3058}
3059
3060fn rule_is_universal(rule_data: &RuleData<'_>) -> bool {
3061 rule_data.rule.prefilters.is_none()
3062}
3063
3064fn rule_matches_self_discriminant(rule_data: &RuleData<'_>, expr_discriminant: usize) -> bool {
3065 rule_data
3066 .rule
3067 .prefilters
3068 .is_some_and(|prefilters| {
3069 prefilters.iter().any(|prefilter| {
3070 matches!(prefilter, RulePrefilter::Variant(discriminant) if *discriminant == expr_discriminant)
3071 })
3072 })
3073}
3074
3075fn rule_matches_specific_prefilter(
3076 rule_data: &RuleData<'_>,
3077 expr: &Expr,
3078 arena_node: Option<(&ExpressionArena, ExpressionNodeId)>,
3079) -> bool {
3080 if rule_is_universal(rule_data) {
3081 return false;
3082 }
3083
3084 let expr_discriminant = discriminant_from_value(expr);
3085 rule_data.rule.prefilters.is_some_and(|prefilters| {
3086 prefilters.iter().any(|prefilter| match prefilter {
3087 RulePrefilter::Variant(discriminant) => *discriminant == expr_discriminant,
3088 RulePrefilter::Child { child } => {
3089 has_direct_child_discriminant(expr, arena_node, *child)
3090 }
3091 RulePrefilter::VariantChild { variant, child } => {
3092 *variant == expr_discriminant
3093 && has_direct_child_discriminant(expr, arena_node, *child)
3094 }
3095 RulePrefilter::Atom(atom_kind) => expr_atom_kind(expr) == Some(*atom_kind),
3096 RulePrefilter::ChildAtom(atom_kind) => {
3097 expr_has_direct_child_atom_kind(expr, *atom_kind)
3098 }
3099 })
3100 })
3101}
3102
3103fn has_direct_child_discriminant(
3104 expr: &Expr,
3105 arena_node: Option<(&ExpressionArena, ExpressionNodeId)>,
3106 target_discriminant: usize,
3107) -> bool {
3108 if let Some((arena, node_id)) = arena_node {
3109 arena.has_direct_child_discriminant(node_id, target_discriminant)
3110 } else {
3111 expr_has_direct_child_discriminant(expr, &[target_discriminant])
3112 }
3113}
3114
3115fn expr_atom_kind(expr: &Expr) -> Option<AtomKind> {
3116 match expr {
3117 Expr::Atomic(_, Atom::Literal(_)) => Some(AtomKind::Literal),
3118 Expr::Atomic(_, Atom::Reference(_)) => Some(AtomKind::Reference),
3119 _ => None,
3120 }
3121}
3122
3123fn expr_has_direct_child_atom_kind(expr: &Expr, target: AtomKind) -> bool {
3128 let mut found = false;
3129 expr.for_each_expr_child(&mut |child| {
3130 if !found && expr_atom_kind(child) == Some(target) {
3131 found = true;
3132 }
3133 });
3134 found
3135}
3136
3137fn expr_has_direct_child_discriminant(expr: &Expr, target_discriminants: &[usize]) -> bool {
3138 if target_discriminants.is_empty() {
3139 return false;
3140 }
3141
3142 let mut found = false;
3143 expr.for_each_expr_child(&mut |child| {
3144 if !found && target_discriminants.contains(&discriminant_from_value(child)) {
3145 found = true;
3146 }
3147 });
3148 found
3149}
3150
3151fn csv_escape(field: &str) -> String {
3152 if field.contains([',', '"', '\n', '\r']) {
3153 format!("\"{}\"", field.replace('"', "\"\""))
3154 } else {
3155 field.to_string()
3156 }
3157}
3158
3159#[cfg(test)]
3160mod tests {
3161 use crate::ast::comprehension::ComprehensionBuilder;
3162 use crate::ast::{Atom, DeclarationPtr, ExpressionArena, Literal, Moo, SymbolTablePtr};
3163 use crate::matrix_expr;
3164
3165 use super::*;
3166
3167 fn int_lit(value: i32) -> Expr {
3168 Expr::Atomic(Metadata::new(), Atom::Literal(Literal::Int(value)))
3169 }
3170
3171 fn bool_lit(value: bool) -> Expr {
3172 Expr::Atomic(Metadata::new(), Atom::Literal(Literal::Bool(value)))
3173 }
3174
3175 fn root(exprs: Vec<Expr>) -> Expr {
3176 Expr::Root(Metadata::new(), exprs)
3177 }
3178
3179 fn comprehension(return_expression: Expr, guards: Vec<Expr>) -> Expr {
3180 let mut builder = ComprehensionBuilder::new(SymbolTablePtr::new());
3181 for guard in guards {
3182 builder = builder.guard(guard);
3183 }
3184 Expr::Comprehension(
3185 Metadata::new(),
3186 Moo::new(builder.with_return_value(return_expression)),
3187 )
3188 }
3189
3190 #[test]
3191 fn dirty_trace_filename_replaces_path_separators_and_module_delimiters() {
3192 assert_eq!(
3193 sanitize_dirty_trace_filename("generated_tests::savilerow/quasiGroup6"),
3194 "generated_tests--savilerow-quasiGroup6"
3195 );
3196 }
3197
3198 #[test]
3199 fn dirty_trace_bare_path_is_directory_destination() {
3200 assert_eq!(
3201 dirty_trace_destination_from_env_value("trace-dir".into()),
3202 DirtyTraceDestination::Directory(PathBuf::from("trace-dir"))
3203 );
3204 }
3205
3206 #[test]
3207 fn dirty_trace_txt_path_is_file_destination() {
3208 assert_eq!(
3209 dirty_trace_destination_from_env_value("trace.txt".into()),
3210 DirtyTraceDestination::File(PathBuf::from("trace.txt"))
3211 );
3212 }
3213
3214 fn reference_expr(name: &Name) -> Expr {
3215 use crate::ast::{Domain, Range, Reference};
3216
3217 Expr::Atomic(
3218 Metadata::new(),
3219 Atom::Reference(Reference::new(DeclarationPtr::new_find(
3220 name.clone(),
3221 Domain::int(vec![Range::Bounded(1, 3)]),
3222 ))),
3223 )
3224 }
3225
3226 fn test_rule_set_applies(_: &crate::settings::SolverFamily) -> bool {
3227 true
3228 }
3229
3230 fn never_apply_test_rule(
3231 _: &Expr,
3232 _: &crate::ast::SymbolTable,
3233 ) -> crate::rule_engine::ApplicationResult {
3234 Err(crate::rule_engine::ApplicationError::RuleNotApplicable)
3235 }
3236
3237 static TEST_RULE_SET: RuleSet<'static> =
3238 RuleSet::new("test-rule-set", &[], test_rule_set_applies);
3239 static TEST_RULE: crate::rule_engine::Rule<'static> = crate::rule_engine::Rule::new(
3240 "never-apply-test-rule",
3241 never_apply_test_rule,
3242 &[("test-rule-set", 1)],
3243 );
3244 static TEST_NO_TARGET_RULE: crate::rule_engine::Rule<'static> = crate::rule_engine::Rule {
3245 name: "no-target-test-rule",
3246 application: never_apply_test_rule,
3247 rule_sets: &[("test-rule-set", 1)],
3248 prefilters: Some(&[]),
3249 failure_invalidation: crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3250 };
3251 static TEST_SYMBOLS_ONLY_RULE: crate::rule_engine::Rule<'static> = crate::rule_engine::Rule {
3252 name: "symbols-only-test-rule",
3253 application: never_apply_test_rule,
3254 rule_sets: &[("test-rule-set", 1)],
3255 prefilters: None,
3256 failure_invalidation: crate::rule_engine::RuleFailureInvalidation::SymbolsOnly,
3257 };
3258 fn test_rule_groups_at_priorities(priorities: &[u16]) -> Vec<RuleGroup<'static>> {
3259 priorities
3260 .iter()
3261 .map(|&priority| {
3262 RuleGroup::new(
3263 priority,
3264 vec![crate::rule_engine::RuleData {
3265 rule: &TEST_RULE,
3266 priority,
3267 rule_set: &TEST_RULE_SET,
3268 }],
3269 )
3270 })
3271 .collect()
3272 }
3273
3274 fn test_rule_groups() -> Vec<RuleGroup<'static>> {
3275 test_rule_groups_at_priorities(&[1])
3276 }
3277
3278 fn test_rule_groups_with_two_levels() -> Vec<RuleGroup<'static>> {
3279 test_rule_groups_at_priorities(&[1, 2])
3280 }
3281
3282 fn test_rule_groups_with_no_candidate_middle_level() -> Vec<RuleGroup<'static>> {
3283 vec![
3284 RuleGroup::new(
3285 1,
3286 vec![crate::rule_engine::RuleData {
3287 rule: &TEST_RULE,
3288 priority: 1,
3289 rule_set: &TEST_RULE_SET,
3290 }],
3291 ),
3292 RuleGroup::new(
3293 2,
3294 vec![crate::rule_engine::RuleData {
3295 rule: &TEST_NO_TARGET_RULE,
3296 priority: 2,
3297 rule_set: &TEST_RULE_SET,
3298 }],
3299 ),
3300 RuleGroup::new(
3301 3,
3302 vec![crate::rule_engine::RuleData {
3303 rule: &TEST_RULE,
3304 priority: 3,
3305 rule_set: &TEST_RULE_SET,
3306 }],
3307 ),
3308 ]
3309 }
3310
3311 fn test_rule_groups_targeting_expr(expr: &Expr) -> Vec<RuleGroup<'static>> {
3312 let discriminant = discriminant_from_value(expr);
3313 let mut rules_by_discriminant = Vec::new();
3314 rules_by_discriminant.resize_with(discriminant + 1, || None);
3315 rules_by_discriminant[discriminant] = Some(vec![crate::rule_engine::RuleData {
3316 rule: &TEST_RULE,
3317 priority: 1,
3318 rule_set: &TEST_RULE_SET,
3319 }]);
3320
3321 vec![RuleGroup {
3322 priority: 1,
3323 rules: vec![crate::rule_engine::RuleData {
3324 rule: &TEST_RULE,
3325 priority: 1,
3326 rule_set: &TEST_RULE_SET,
3327 }],
3328 rules_by_discriminant,
3329 universal_rules: Vec::new(),
3330 has_non_discriminant_filters: false,
3331 }]
3332 }
3333
3334 #[test]
3335 fn rule_group_child_filter_matches_immediate_child_kind() {
3336 let bubble_discriminant = discriminant_from_value(&Expr::Bubble(
3337 Metadata::new(),
3338 Moo::new(int_lit(0)),
3339 Moo::new(bool_lit(true)),
3340 ));
3341 let child_prefilters: &'static [RulePrefilter] = Box::leak(
3342 vec![RulePrefilter::Child {
3343 child: bubble_discriminant,
3344 }]
3345 .into_boxed_slice(),
3346 );
3347 let child_bubble_rule: &'static crate::rule_engine::Rule<'static> =
3348 Box::leak(Box::new(crate::rule_engine::Rule {
3349 name: "child-bubble-test-rule",
3350 application: never_apply_test_rule,
3351 rule_sets: &[("test-rule-set", 1)],
3352 prefilters: Some(child_prefilters),
3353 failure_invalidation:
3354 crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3355 }));
3356 let rule_group = RuleGroup::new(
3357 1,
3358 vec![crate::rule_engine::RuleData {
3359 rule: child_bubble_rule,
3360 priority: 1,
3361 rule_set: &TEST_RULE_SET,
3362 }],
3363 );
3364 let config = RewriteConfig::optimised();
3365
3366 let expr_with_bubble_child = Expr::Eq(
3367 Metadata::new(),
3368 Moo::new(Expr::Bubble(
3369 Metadata::new(),
3370 Moo::new(int_lit(1)),
3371 Moo::new(bool_lit(true)),
3372 )),
3373 Moo::new(int_lit(2)),
3374 );
3375 let expr_without_bubble_child =
3376 Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
3377
3378 assert!(rule_group.has_candidates(config, &expr_with_bubble_child));
3379 assert_eq!(
3380 rule_group
3381 .candidates(config, &expr_with_bubble_child)
3382 .map(|rule_data| rule_data.rule.name)
3383 .collect_vec(),
3384 vec!["child-bubble-test-rule"]
3385 );
3386 assert!(!rule_group.has_candidates(config, &expr_without_bubble_child));
3387 }
3388
3389 #[test]
3390 fn rule_group_atom_filter_matches_atomic_reference() {
3391 let atom_reference_rule: &'static crate::rule_engine::Rule<'static> =
3392 Box::leak(Box::new(crate::rule_engine::Rule {
3393 name: "atom-reference-test-rule",
3394 application: never_apply_test_rule,
3395 rule_sets: &[("test-rule-set", 1)],
3396 prefilters: Some(&[RulePrefilter::Atom(AtomKind::Reference)]),
3397 failure_invalidation:
3398 crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3399 }));
3400 let rule_group = RuleGroup::new(
3401 1,
3402 vec![crate::rule_engine::RuleData {
3403 rule: atom_reference_rule,
3404 priority: 1,
3405 rule_set: &TEST_RULE_SET,
3406 }],
3407 );
3408 let config = RewriteConfig::optimised();
3409 let reference = reference_expr(&Name::user("x"));
3410 let literal = int_lit(1);
3411 let composite = Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
3412
3413 assert!(rule_group.has_candidates(config, &reference));
3414 assert_eq!(
3415 rule_group
3416 .candidates(config, &reference)
3417 .map(|rule_data| rule_data.rule.name)
3418 .collect_vec(),
3419 vec!["atom-reference-test-rule"]
3420 );
3421 assert!(!rule_group.has_candidates(config, &literal));
3422 assert!(!rule_group.has_candidates(config, &composite));
3423 }
3424
3425 #[test]
3426 fn rule_group_child_atom_filter_matches_direct_atomic_reference_child() {
3427 let child_atom_rule: &'static crate::rule_engine::Rule<'static> =
3428 Box::leak(Box::new(crate::rule_engine::Rule {
3429 name: "child-atom-reference-test-rule",
3430 application: never_apply_test_rule,
3431 rule_sets: &[("test-rule-set", 1)],
3432 prefilters: Some(&[RulePrefilter::ChildAtom(AtomKind::Reference)]),
3433 failure_invalidation:
3434 crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3435 }));
3436 let rule_group = RuleGroup::new(
3437 1,
3438 vec![crate::rule_engine::RuleData {
3439 rule: child_atom_rule,
3440 priority: 1,
3441 rule_set: &TEST_RULE_SET,
3442 }],
3443 );
3444 let config = RewriteConfig::optimised();
3445 let with_reference_child = Expr::Eq(
3446 Metadata::new(),
3447 Moo::new(reference_expr(&Name::user("x"))),
3448 Moo::new(int_lit(2)),
3449 );
3450 let with_literal_children =
3452 Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
3453 let bare_reference = reference_expr(&Name::user("x"));
3455
3456 assert!(rule_group.has_candidates(config, &with_reference_child));
3457 assert_eq!(
3458 rule_group
3459 .candidates(config, &with_reference_child)
3460 .map(|rule_data| rule_data.rule.name)
3461 .collect_vec(),
3462 vec!["child-atom-reference-test-rule"]
3463 );
3464 assert!(!rule_group.has_candidates(config, &with_literal_children));
3465 assert!(!rule_group.has_candidates(config, &bare_reference));
3466 }
3467
3468 #[test]
3469 fn rule_group_includes_universal_rules_in_variant_buckets() {
3470 let lex_discriminant = discriminant_from_value(&Expr::LexLt(
3471 Metadata::new(),
3472 Moo::new(int_lit(1)),
3473 Moo::new(int_lit(2)),
3474 ));
3475 let lex_prefilters: &'static [RulePrefilter] =
3476 Box::leak(Box::new([RulePrefilter::Variant(lex_discriminant)]));
3477 let variant_rule: &'static crate::rule_engine::Rule<'static> =
3478 Box::leak(Box::new(crate::rule_engine::Rule {
3479 name: "variant-specific-test-rule",
3480 application: never_apply_test_rule,
3481 rule_sets: &[("test-rule-set", 1)],
3482 prefilters: Some(lex_prefilters),
3483 failure_invalidation:
3484 crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3485 }));
3486 let universal_rule: &'static crate::rule_engine::Rule<'static> =
3487 Box::leak(Box::new(crate::rule_engine::Rule {
3488 name: "universal-test-rule",
3489 application: never_apply_test_rule,
3490 rule_sets: &[("test-rule-set", 1)],
3491 prefilters: None,
3492 failure_invalidation:
3493 crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3494 }));
3495 let rule_group = RuleGroup::new(
3496 1,
3497 vec![
3498 crate::rule_engine::RuleData {
3499 rule: variant_rule,
3500 priority: 1,
3501 rule_set: &TEST_RULE_SET,
3502 },
3503 crate::rule_engine::RuleData {
3504 rule: universal_rule,
3505 priority: 1,
3506 rule_set: &TEST_RULE_SET,
3507 },
3508 ],
3509 );
3510 let config = RewriteConfig::optimised();
3511 let lex = Expr::LexLt(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
3512
3513 assert!(rule_group.has_candidates(config, &lex));
3514 assert_eq!(
3515 rule_group
3516 .candidates(config, &lex)
3517 .map(|rule_data| rule_data.rule.name)
3518 .collect_vec(),
3519 vec!["variant-specific-test-rule", "universal-test-rule"]
3520 );
3521 }
3522
3523 #[test]
3524 fn rule_group_variant_child_filter_does_not_cross_product_alternatives() {
3525 let and_discriminant = discriminant_from_value(&Expr::And(
3526 Metadata::new(),
3527 Moo::new(matrix_expr![bool_lit(true)]),
3528 ));
3529 let or_discriminant = discriminant_from_value(&Expr::Or(
3530 Metadata::new(),
3531 Moo::new(matrix_expr![bool_lit(true)]),
3532 ));
3533 let comprehension_discriminant =
3534 discriminant_from_value(&comprehension(bool_lit(true), vec![]));
3535 let atomic_discriminant = discriminant_from_value(&bool_lit(true));
3536 let paired_prefilters: &'static [RulePrefilter] = Box::leak(
3537 vec![
3538 RulePrefilter::VariantChild {
3539 variant: and_discriminant,
3540 child: comprehension_discriminant,
3541 },
3542 RulePrefilter::VariantChild {
3543 variant: or_discriminant,
3544 child: atomic_discriminant,
3545 },
3546 ]
3547 .into_boxed_slice(),
3548 );
3549 let paired_rule: &'static crate::rule_engine::Rule<'static> =
3550 Box::leak(Box::new(crate::rule_engine::Rule {
3551 name: "paired-prefilter-test-rule",
3552 application: never_apply_test_rule,
3553 rule_sets: &[("test-rule-set", 1)],
3554 prefilters: Some(paired_prefilters),
3555 failure_invalidation:
3556 crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3557 }));
3558 let rule_group = RuleGroup::new(
3559 1,
3560 vec![crate::rule_engine::RuleData {
3561 rule: paired_rule,
3562 priority: 1,
3563 rule_set: &TEST_RULE_SET,
3564 }],
3565 );
3566 let config = RewriteConfig::optimised();
3567 let and_with_comprehension = Expr::And(
3568 Metadata::new(),
3569 Moo::new(comprehension(bool_lit(true), vec![])),
3570 );
3571 let or_with_atomic = Expr::Or(Metadata::new(), Moo::new(bool_lit(true)));
3572 let and_with_atomic = Expr::And(Metadata::new(), Moo::new(bool_lit(true)));
3573 let or_with_comprehension = Expr::Or(
3574 Metadata::new(),
3575 Moo::new(comprehension(bool_lit(true), vec![])),
3576 );
3577
3578 assert!(rule_group.has_candidates(config, &and_with_comprehension));
3579 assert!(rule_group.has_candidates(config, &or_with_atomic));
3580 assert!(!rule_group.has_candidates(config, &and_with_atomic));
3581 assert!(!rule_group.has_candidates(config, &or_with_comprehension));
3582 }
3583
3584 #[test]
3585 fn rewriter_subtree_preorder_does_not_enter_comprehensions() {
3586 let tree = root(vec![
3587 comprehension(int_lit(1), vec![int_lit(2)]),
3588 int_lit(3),
3589 ]);
3590 let arena = ExpressionArena::from_root(tree);
3591 let root_ids = rewriter_preorder_ids(&arena);
3592 let comp_id = arena.children(arena.root())[0];
3593
3594 assert_eq!(root_ids.len(), 3);
3595 assert_eq!(
3596 rewriter_reachable_subtree_ids(&arena, comp_id),
3597 vec![comp_id]
3598 );
3599 }
3600
3601 #[test]
3602 fn worklist_ancestor_enqueue_checks_enclosing_nodes_first() {
3603 let tree = root(vec![Expr::Eq(
3604 Metadata::new(),
3605 Moo::new(int_lit(1)),
3606 Moo::new(int_lit(2)),
3607 )]);
3608 let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
3609 let arena = &surfaces[0].arena;
3610 let ids = rewriter_preorder_ids(arena);
3611 let root_id = ids[0];
3612 let eq_id = ids[1];
3613 let left_leaf_id = ids[2];
3614
3615 let rule_groups = test_rule_groups();
3616 let mut scheduler = WorklistScheduler::empty(&rule_groups);
3617 let mut dirty_trace = DirtyTrace::default();
3618
3619 scheduler.enqueue_node_and_ancestors(arena, 0, left_leaf_id, &mut dirty_trace);
3620
3621 assert_eq!(
3622 scheduler.pop_next(
3623 &surfaces,
3624 &rule_groups,
3625 RewriteConfig::optimised(),
3626 &mut dirty_trace
3627 ),
3628 Some((0, 0, root_id, ScheduledMode::CheckNode))
3629 );
3630 assert_eq!(
3631 scheduler.pop_next(
3632 &surfaces,
3633 &rule_groups,
3634 RewriteConfig::optimised(),
3635 &mut dirty_trace
3636 ),
3637 Some((0, 0, eq_id, ScheduledMode::CheckNode))
3638 );
3639 assert_eq!(
3640 scheduler.pop_next(
3641 &surfaces,
3642 &rule_groups,
3643 RewriteConfig::optimised(),
3644 &mut dirty_trace
3645 ),
3646 Some((0, 0, left_leaf_id, ScheduledMode::CheckNode))
3647 );
3648 assert_eq!(
3649 scheduler.pop_next(
3650 &surfaces,
3651 &rule_groups,
3652 RewriteConfig::optimised(),
3653 &mut dirty_trace
3654 ),
3655 None
3656 );
3657 }
3658
3659 #[test]
3660 fn worklist_level_order_prefers_ancestors_over_earlier_descendants() {
3661 let tree = root(vec![Expr::Eq(
3662 Metadata::new(),
3663 Moo::new(int_lit(1)),
3664 Moo::new(int_lit(2)),
3665 )]);
3666 let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
3667 let arena = &surfaces[0].arena;
3668 let ids = rewriter_preorder_ids(arena);
3669 let eq_id = ids[1];
3670 let left_leaf_id = ids[2];
3671
3672 let rule_groups = test_rule_groups();
3673 let mut scheduler = WorklistScheduler::empty(&rule_groups);
3674 let mut dirty_trace = DirtyTrace::default();
3675
3676 scheduler.enqueue_node_at_level(
3677 arena,
3678 0,
3679 left_leaf_id,
3680 0,
3681 ScheduledMode::CheckNode,
3682 Some(&mut dirty_trace),
3683 );
3684 scheduler.enqueue_node_at_level(
3685 arena,
3686 0,
3687 eq_id,
3688 0,
3689 ScheduledMode::CheckNode,
3690 Some(&mut dirty_trace),
3691 );
3692
3693 assert_eq!(
3694 scheduler.pop_next(
3695 &surfaces,
3696 &rule_groups,
3697 RewriteConfig::optimised(),
3698 &mut dirty_trace
3699 ),
3700 Some((0, 0, eq_id, ScheduledMode::CheckNode))
3701 );
3702 assert_eq!(
3703 scheduler.pop_next(
3704 &surfaces,
3705 &rule_groups,
3706 RewriteConfig::optimised(),
3707 &mut dirty_trace
3708 ),
3709 Some((0, 0, left_leaf_id, ScheduledMode::CheckNode))
3710 );
3711 }
3712
3713 #[test]
3714 fn worklist_subtree_descends_lazily_in_breadth_first_order() {
3715 let tree = root(vec![
3716 Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2))),
3717 int_lit(3),
3718 ]);
3719 let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
3720 let arena = &surfaces[0].arena;
3721 let ids = rewriter_preorder_ids(arena);
3722 let root_id = ids[0];
3723 let eq_id = ids[1];
3724 let left_leaf_id = ids[2];
3725 let right_leaf_id = ids[3];
3726 let root_sibling_id = ids[4];
3727
3728 let rule_groups = test_rule_groups_with_two_levels();
3729 let mut scheduler =
3730 WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
3731 let mut dirty_trace = DirtyTrace::default();
3732
3733 let root_work = scheduler.pop_next(
3734 &surfaces,
3735 &rule_groups,
3736 RewriteConfig::optimised(),
3737 &mut dirty_trace,
3738 );
3739 assert_eq!(
3740 root_work,
3741 Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
3742 );
3743 assert_eq!(
3744 scheduler.pop_next(
3745 &surfaces,
3746 &rule_groups,
3747 RewriteConfig::optimised(),
3748 &mut dirty_trace
3749 ),
3750 None
3751 );
3752
3753 scheduler.enqueue_after_no_rewrite(
3754 WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
3755 root_id,
3756 0,
3757 1,
3758 ScheduledMode::TraverseSubtreeRoot,
3759 Some(&mut dirty_trace),
3760 );
3761 let eq_work = scheduler.pop_next(
3762 &surfaces,
3763 &rule_groups,
3764 RewriteConfig::optimised(),
3765 &mut dirty_trace,
3766 );
3767 assert_eq!(
3768 eq_work,
3769 Some((0, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
3770 );
3771 scheduler.enqueue_after_no_rewrite(
3772 WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
3773 eq_id,
3774 0,
3775 1,
3776 ScheduledMode::TraverseSubtreeDescendant,
3777 Some(&mut dirty_trace),
3778 );
3779 assert_eq!(
3780 scheduler.pop_next(
3781 &surfaces,
3782 &rule_groups,
3783 RewriteConfig::optimised(),
3784 &mut dirty_trace
3785 ),
3786 Some((
3787 0,
3788 0,
3789 root_sibling_id,
3790 ScheduledMode::TraverseSubtreeDescendant
3791 ))
3792 );
3793 assert_eq!(
3794 scheduler.pop_next(
3795 &surfaces,
3796 &rule_groups,
3797 RewriteConfig::optimised(),
3798 &mut dirty_trace
3799 ),
3800 Some((0, 0, left_leaf_id, ScheduledMode::TraverseSubtreeDescendant))
3801 );
3802 assert_eq!(
3803 scheduler.pop_next(
3804 &surfaces,
3805 &rule_groups,
3806 RewriteConfig::optimised(),
3807 &mut dirty_trace
3808 ),
3809 Some((
3810 0,
3811 0,
3812 right_leaf_id,
3813 ScheduledMode::TraverseSubtreeDescendant
3814 ))
3815 );
3816 assert_eq!(
3817 scheduler.pop_next(
3818 &surfaces,
3819 &rule_groups,
3820 RewriteConfig::optimised(),
3821 &mut dirty_trace
3822 ),
3823 Some((1, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
3824 );
3825 assert_eq!(
3826 scheduler.pop_next(
3827 &surfaces,
3828 &rule_groups,
3829 RewriteConfig::optimised(),
3830 &mut dirty_trace
3831 ),
3832 None
3833 );
3834
3835 scheduler.enqueue_after_no_rewrite(
3836 WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
3837 root_id,
3838 1,
3839 2,
3840 ScheduledMode::TraverseSubtreeRoot,
3841 Some(&mut dirty_trace),
3842 );
3843 assert_eq!(
3844 scheduler.pop_next(
3845 &surfaces,
3846 &rule_groups,
3847 RewriteConfig::optimised(),
3848 &mut dirty_trace
3849 ),
3850 Some((1, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
3851 );
3852 }
3853
3854 #[test]
3855 fn worklist_refreshes_stale_subtree_carrier_after_descendant_rewrite() {
3856 let tree = root(vec![
3857 Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2))),
3858 int_lit(3),
3859 ]);
3860 let mut surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
3861 let ids = rewriter_preorder_ids(&surfaces[0].arena);
3862 let root_id = ids[0];
3863 let eq_id = ids[1];
3864 let root_sibling_id = ids[4];
3865
3866 let rule_groups = test_rule_groups_with_two_levels();
3867 let mut scheduler =
3868 WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
3869 let mut dirty_trace = DirtyTrace::default();
3870
3871 assert_eq!(
3872 scheduler.pop_next(
3873 &surfaces,
3874 &rule_groups,
3875 RewriteConfig::optimised(),
3876 &mut dirty_trace
3877 ),
3878 Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
3879 );
3880 scheduler.enqueue_after_no_rewrite(
3881 WorklistSchedulingContext::new(
3882 &surfaces[0].arena,
3883 0,
3884 &rule_groups,
3885 RewriteConfig::optimised(),
3886 ),
3887 root_id,
3888 0,
3889 1,
3890 ScheduledMode::TraverseSubtreeRoot,
3891 Some(&mut dirty_trace),
3892 );
3893
3894 assert_eq!(
3895 scheduler.pop_next(
3896 &surfaces,
3897 &rule_groups,
3898 RewriteConfig::optimised(),
3899 &mut dirty_trace
3900 ),
3901 Some((0, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
3902 );
3903 replace_focus_and_sync_ancestors(&mut surfaces[0].arena, eq_id, int_lit(10));
3904 enqueue_worklist_rewrite_impact(
3905 &mut scheduler,
3906 &surfaces[0].arena,
3907 0,
3908 eq_id,
3909 &mut dirty_trace,
3910 );
3911
3912 let mut found_refreshed_root = false;
3913 for _ in 0..32 {
3914 let Some((level, surface, node_id, mode)) = scheduler.pop_next(
3915 &surfaces,
3916 &rule_groups,
3917 RewriteConfig::optimised(),
3918 &mut dirty_trace,
3919 ) else {
3920 break;
3921 };
3922 if (level, surface, node_id, mode)
3923 == (1, 0, root_id, ScheduledMode::TraverseSubtreeRoot)
3924 {
3925 found_refreshed_root = true;
3926 scheduler.enqueue_after_no_rewrite(
3927 WorklistSchedulingContext::new(
3928 &surfaces[0].arena,
3929 0,
3930 &rule_groups,
3931 RewriteConfig::optimised(),
3932 ),
3933 root_id,
3934 1,
3935 2,
3936 ScheduledMode::TraverseSubtreeRoot,
3937 Some(&mut dirty_trace),
3938 );
3939 break;
3940 }
3941 scheduler.enqueue_after_no_rewrite(
3942 WorklistSchedulingContext::new(
3943 &surfaces[surface].arena,
3944 surface,
3945 &rule_groups,
3946 RewriteConfig::optimised(),
3947 ),
3948 node_id,
3949 level,
3950 level + 1,
3951 mode,
3952 Some(&mut dirty_trace),
3953 );
3954 }
3955 assert!(found_refreshed_root);
3956
3957 let mut found_sibling_at_next_level = false;
3958 for _ in 0..32 {
3959 let Some((level, _surface, node_id, mode)) = scheduler.pop_next(
3960 &surfaces,
3961 &rule_groups,
3962 RewriteConfig::optimised(),
3963 &mut dirty_trace,
3964 ) else {
3965 break;
3966 };
3967 if (level, node_id, mode)
3968 == (1, root_sibling_id, ScheduledMode::TraverseSubtreeDescendant)
3969 {
3970 found_sibling_at_next_level = true;
3971 break;
3972 }
3973 }
3974 assert!(found_sibling_at_next_level);
3975 }
3976
3977 #[test]
3978 fn worklist_prunes_child_subtrees_without_candidates_at_level() {
3979 let eq = Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
3980 let tree = root(vec![eq.clone(), int_lit(3)]);
3981 let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
3982 let arena = &surfaces[0].arena;
3983 let ids = rewriter_preorder_ids(arena);
3984 let root_id = ids[0];
3985 let eq_id = ids[1];
3986
3987 let rule_groups = test_rule_groups_targeting_expr(&eq);
3988 let mut scheduler =
3989 WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
3990 let mut dirty_trace = DirtyTrace::default();
3991
3992 assert_eq!(
3993 scheduler.pop_next(
3994 &surfaces,
3995 &rule_groups,
3996 RewriteConfig::optimised(),
3997 &mut dirty_trace
3998 ),
3999 Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
4000 );
4001 scheduler.enqueue_after_no_rewrite(
4002 WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
4003 root_id,
4004 0,
4005 1,
4006 ScheduledMode::TraverseSubtreeRoot,
4007 Some(&mut dirty_trace),
4008 );
4009
4010 assert_eq!(
4011 scheduler.pop_next(
4012 &surfaces,
4013 &rule_groups,
4014 RewriteConfig::optimised(),
4015 &mut dirty_trace
4016 ),
4017 Some((0, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
4018 );
4019 assert_eq!(
4020 scheduler.pop_next(
4021 &surfaces,
4022 &rule_groups,
4023 RewriteConfig::optimised(),
4024 &mut dirty_trace
4025 ),
4026 None
4027 );
4028 }
4029
4030 #[test]
4031 fn worklist_no_rewrite_skips_levels_without_candidates_for_subtree() {
4032 let tree = root(vec![int_lit(1)]);
4033 let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
4034 let arena = &surfaces[0].arena;
4035 let ids = rewriter_preorder_ids(arena);
4036 let root_id = ids[0];
4037 let child_id = ids[1];
4038
4039 let rule_groups = test_rule_groups_with_no_candidate_middle_level();
4040 let mut scheduler =
4041 WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
4042 let mut dirty_trace = DirtyTrace::default();
4043
4044 assert_eq!(
4045 scheduler.pop_next(
4046 &surfaces,
4047 &rule_groups,
4048 RewriteConfig::optimised(),
4049 &mut dirty_trace
4050 ),
4051 Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
4052 );
4053
4054 scheduler.enqueue_after_no_rewrite(
4055 WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
4056 root_id,
4057 0,
4058 1,
4059 ScheduledMode::TraverseSubtreeRoot,
4060 Some(&mut dirty_trace),
4061 );
4062
4063 assert_eq!(
4064 scheduler.pop_next(
4065 &surfaces,
4066 &rule_groups,
4067 RewriteConfig::optimised(),
4068 &mut dirty_trace
4069 ),
4070 Some((0, 0, child_id, ScheduledMode::TraverseSubtreeDescendant))
4071 );
4072 assert_eq!(
4073 scheduler.pop_next(
4074 &surfaces,
4075 &rule_groups,
4076 RewriteConfig::optimised(),
4077 &mut dirty_trace
4078 ),
4079 Some((2, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
4080 );
4081 }
4082
4083 #[test]
4084 fn worklist_reference_invalidation_schedules_affected_path_once() {
4085 let x = Name::user("x");
4086 let tree = root(vec![
4087 Expr::Eq(
4088 Metadata::new(),
4089 Moo::new(reference_expr(&x)),
4090 Moo::new(int_lit(1)),
4091 ),
4092 int_lit(2),
4093 ]);
4094 let arena = ExpressionArena::from_root(tree);
4095 let ids = rewriter_preorder_ids(&arena);
4096 let root_id = ids[0];
4097 let eq_id = ids[1];
4098 let reference_id = ids[2];
4099
4100 let mut affected_nodes = Vec::new();
4101 collect_worklist_nodes_referencing_names(
4102 &arena,
4103 arena.root(),
4104 std::slice::from_ref(&x),
4105 &mut affected_nodes,
4106 );
4107
4108 assert_eq!(
4109 affected_nodes.into_iter().rev().collect_vec(),
4110 vec![root_id, eq_id, reference_id]
4111 );
4112 }
4113
4114 #[test]
4115 fn worklist_caches_symbols_only_failures_until_symbols_change() {
4116 let tree = root(vec![int_lit(1)]);
4117 let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
4118 let node_id = surfaces[0].arena.root();
4119 let rule_groups = vec![RuleGroup::new(
4120 1,
4121 vec![crate::rule_engine::RuleData {
4122 rule: &TEST_SYMBOLS_ONLY_RULE,
4123 priority: 1,
4124 rule_set: &TEST_RULE_SET,
4125 }],
4126 )];
4127 let rule_data = &rule_groups[0].rules[0];
4128 let mut scheduler = WorklistScheduler::empty(&rule_groups);
4129
4130 assert!(scheduler.should_attempt_rule(0, node_id, rule_data));
4131 scheduler.record_rule_failure(0, node_id, rule_data);
4132 assert!(!scheduler.should_attempt_rule(0, node_id, rule_data));
4133
4134 scheduler.invalidate_symbol_rule_failures();
4135 assert!(scheduler.should_attempt_rule(0, node_id, rule_data));
4136 }
4137
4138 #[test]
4139 fn evaluator_batches_ancestor_work_until_the_worklist_is_empty() {
4140 let x = Name::user("x");
4141 let and = Expr::And(
4142 Metadata::new(),
4143 Moo::new(matrix_expr![bool_lit(true), reference_expr(&x)]),
4144 );
4145 let mut surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(root(
4146 vec![and],
4147 )))];
4148 let ids = rewriter_preorder_ids(&surfaces[0].arena);
4149 let and_id = ids
4150 .iter()
4151 .copied()
4152 .find(|&node_id| matches!(surfaces[0].arena.expression(node_id), Expr::And(..)))
4153 .expect("and node");
4154 let true_id = ids
4155 .iter()
4156 .copied()
4157 .find(|&node_id| surfaces[0].arena.expression(node_id) == &bool_lit(true))
4158 .expect("true literal");
4159
4160 replace_focus_and_sync_ancestors(&mut surfaces[0].arena, true_id, bool_lit(false));
4161 let mut work = DeferredEvaluatorWork::default();
4162 work.defer_ancestors(&mut surfaces[0].arena, 0, true_id);
4163 work.defer_ancestors(&mut surfaces[0].arena, 0, true_id);
4164 assert_eq!(
4165 work.nodes.len(),
4166 3,
4167 "matrix, `and`, and root ancestors are unique"
4168 );
4169 assert!(matches!(
4170 surfaces[0].arena.expression(and_id),
4171 Expr::And(..)
4172 ));
4173
4174 let rule_groups = Vec::new();
4175 let mut scheduler = WorklistScheduler::empty(&rule_groups);
4176 let mut dirty_trace = DirtyTrace::default();
4177 assert!(normalise_deferred_evaluators(
4178 &mut work,
4179 &mut Model::new(Default::default()),
4180 &mut surfaces,
4181 &mut scheduler,
4182 &mut dirty_trace,
4183 ));
4184 assert_eq!(surfaces[0].arena.expression(and_id), &bool_lit(false));
4185 assert!(work.is_empty());
4186 }
4187}