Skip to main content

conjure_cp_core/rule_engine/
rewrite.rs

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// Rewriter selection invariant:
44//
45// 1. Higher rule priority wins.
46// 2. Within one priority, an expression is tried before its descendants.
47//
48// Consequently, if A contains B and both are rewriteable at the same priority, A must be selected
49// first. If A rewrites, old descendants such as B are not considered; descendants are only
50// scheduled when no enclosing expression rewrites first. The rewriter does not require old full-scan
51// preorder between unrelated sibling subtrees.
52// This is a semantic requirement of the rewriter, not merely a scheduling optimisation.
53//
54// Rule side effects:
55//
56// A selected rule always replaces the focused expression. It may also append top-level constraints,
57// append CNF clauses, add symbol declarations, or change existing symbol declarations (for example
58// by tightening a domain). Rewriting a value-letting surface writes the replacement expression back
59// to that symbol declaration. Symbol changes invalidate rule-application context caches; changed
60// declarations and CNF effects may require rebuilding rewrite surfaces from the model.
61
62#[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    /// Check only this node at the scheduled rule level.
76    CheckNode,
77    /// Traverse this subtree at the scheduled level and carry it forward if it survives.
78    TraverseSubtreeRoot,
79    /// Traverse this descendant at the current level only; the root carries future levels.
80    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    // Treat bare paths as directories to support a single `nextest` run with
515    // `CONJURE_DIRTY_TRACE=/abs/trace-dir`; each test then writes its own file.
516    // Explicit file output remains available for paths that look like filenames.
517    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    /// Indexed by discriminant id for O(1) lookup of simple variant prefilters.
558    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            // Always keep universal rules eligible. A matching child/atom
680            // prefilter on a sibling rule does not mean universals are
681            // inapplicable — suppressing them here previously skipped
682            // `matrix_to_list` on `SafeIndex` when `matrix_ref_to_atom` matched
683            // `* / Atomic` (permMultElementId).
684            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/// Which rule levels can match the subtree rooted at one arena node.
816///
817/// `known_levels` marks the levels already decided for this node's current generation;
818/// `candidate_levels` holds the answer for those. A stale generation resets both.
819#[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/// Identifies one failed application of a rule whose applicability depends only on symbols.
828///
829/// `rule` is the address of the `&'static Rule` that failed, which identifies it exactly.
830#[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    // Candidate-level skipping can enqueue a descendant into a future rule level before an
863    // ancestor reaches that same level. Each level is therefore ordered by depth, then insertion
864    // sequence, so the semantic invariant "try an expression before its descendants at the same
865    // priority" does not depend on enqueue timing.
866    queues_by_level: Vec<BinaryHeap<ScheduledNode>>,
867    scheduled: HashMap<ScheduledKey, ScheduledMode>,
868    /// Per-surface, per-arena-slot cache of the rule levels that can match a subtree.
869    ///
870    /// Arena node ids are dense indices, so a flat cache avoids hashing a `(surface, node,
871    /// level, generation)` tuple for every worklist descent. A `u128` covers the current 58 rule
872    /// levels; levels beyond that remain correct and simply run uncached.
873    subtree_candidate_cache: Vec<Vec<NodeSubtreeCandidateCache>>,
874    /// Failed applications whose applicability depends only on the symbol table, keyed by the
875    /// symbol-table revision at which they were attempted.
876    failed_symbol_rules: HashMap<FailedSymbolRuleKey, u64>,
877    /// Arena nodes containing direct references, indexed by declaration name.
878    ///
879    /// Replacement appends fresh arena slots, so old entries can remain and are validated lazily
880    /// at invalidation time. This avoids rescanning every expression surface after each
881    /// representation effect.
882    reference_nodes_by_name: HashMap<Name, HashSet<(usize, ExpressionNodeId)>>,
883    symbol_revision: u64,
884    next_sequence: u64,
885}
886
887/// Ancestors waiting for an evaluator pass, held until the worklist runs out of rule work.
888///
889/// A rewrite normalises its own replacement subtree straight away and records its ancestors here.
890/// Because the same ancestor is only recorded once, a wide `and`/`or`/arithmetic node gets one
891/// evaluator pass after all its changed children are done, instead of one per child.
892#[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/// Ancestors waiting to have their rules re-checked, held until the worklist is about to move on
917/// to a lower priority (or emptied straight away after a change to the model).
918///
919/// Rewriting a child can make a higher-priority rule apply to an expression enclosing it, so those
920/// ancestors have to be re-checked before any lower-priority work runs. Collecting them up while
921/// the current priority finishes means a shared wide root is visited once per priority instead of
922/// once per child -- the shape unrolling a comprehension produces.
923#[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    /// Brings just the enclosing expressions of a rewrite up to date, stopping short of the root.
966    ///
967    /// Choosing a representation can change which rule should win for a sibling in the same
968    /// comparison, so the expressions around it cannot wait. The surface root is left in the
969    /// batch on purpose: rebuilding its long list of constraints every time an auxiliary appears
970    /// costs time proportional to the whole model, once per auxiliary.
971    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        // Expressions above a changed child are left alone while the priority finishes, so
1050        // anything already queued against them stays valid. Rebuild deepest first, once each, so
1051        // every ancestor sees up-to-date children. Ancestors on the root surface also get the
1052        // evaluator here, so a parent sees an already-normalised child and the path above a
1053        // rewrite is copied once per batch instead of once per rewrite.
1054        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        // An item taken off the queue while its ancestors were still out of date must not be
1106        // dropped. Put it back now that they are rebuilt, so descendants it had not reached yet
1107        // -- new top-level constraints in particular -- still get visited.
1108        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        // Descendants reached while traversing the current level do not eagerly advance to
1392        // future levels. The traversal root advances and rediscovers descendants only after it
1393        // survives that future level, avoiding stale descendant work when an ancestor rewrites.
1394        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                        // A descendant rewrite updates ancestor generations. Refreshing the
1475                        // traversal root preserves its job of carrying still-unseen sibling
1476                        // descendants to later rule levels.
1477                        let refresh_level = self.next_subtree_candidate_level(
1478                            arena,
1479                            scheduled.surface,
1480                            scheduled.node_id,
1481                            level,
1482                            rule_groups,
1483                            config,
1484                        );
1485                        self.enqueue_node_at_level(
1486                            arena,
1487                            scheduled.surface,
1488                            scheduled.node_id,
1489                            refresh_level,
1490                            scheduled.mode,
1491                            Some(dirty_trace),
1492                        );
1493                    }
1494                    continue;
1495                }
1496
1497                dirty_trace.record_worklist_pop(scheduled.mode);
1498                return Some((level, scheduled.surface, scheduled.node_id, scheduled.mode));
1499            }
1500        }
1501
1502        None
1503    }
1504
1505    fn next_subtree_candidate_level(
1506        &mut self,
1507        arena: &ExpressionArena,
1508        surface: usize,
1509        node_id: ExpressionNodeId,
1510        start_level: usize,
1511        rule_groups: &[RuleGroup<'_>],
1512        config: RewriteConfig,
1513    ) -> usize {
1514        if start_level >= rule_groups.len() || !arena.is_reachable(node_id) {
1515            return rule_groups.len();
1516        }
1517
1518        (start_level..rule_groups.len())
1519            .find(|&level| {
1520                self.subtree_has_candidates_at_level(
1521                    arena,
1522                    surface,
1523                    node_id,
1524                    level,
1525                    rule_groups,
1526                    config,
1527                )
1528            })
1529            .unwrap_or(rule_groups.len())
1530    }
1531
1532    fn subtree_has_candidates_at_level(
1533        &mut self,
1534        arena: &ExpressionArena,
1535        surface: usize,
1536        node_id: ExpressionNodeId,
1537        level: usize,
1538        rule_groups: &[RuleGroup<'_>],
1539        config: RewriteConfig,
1540    ) -> bool {
1541        if level >= rule_groups.len() || !arena.is_reachable(node_id) {
1542            return false;
1543        }
1544
1545        let cache_bit = 1u128.checked_shl(level as u32);
1546        if let Some(bit) = cache_bit {
1547            if self.subtree_candidate_cache.len() <= surface {
1548                self.subtree_candidate_cache
1549                    .resize_with(surface + 1, Vec::new);
1550            }
1551            let surface_cache = &mut self.subtree_candidate_cache[surface];
1552            if surface_cache.len() <= node_id.index() {
1553                surface_cache.resize(node_id.index() + 1, NodeSubtreeCandidateCache::default());
1554            }
1555            let entry = &mut surface_cache[node_id.index()];
1556            let generation = arena.generation(node_id);
1557            if !entry.generation_valid || entry.generation != generation {
1558                *entry = NodeSubtreeCandidateCache {
1559                    generation,
1560                    generation_valid: true,
1561                    ..NodeSubtreeCandidateCache::default()
1562                };
1563            }
1564            if entry.known_levels & bit != 0 {
1565                return entry.candidate_levels & bit != 0;
1566            }
1567        }
1568
1569        let rule_group = &rule_groups[level];
1570        let has_candidates = rule_group.has_candidates_at_node(config, arena, node_id)
1571            || (!matches!(arena.expression(node_id), Expr::Comprehension(_, _))
1572                && arena.children(node_id).iter().any(|&child_id| {
1573                    self.subtree_has_candidates_at_level(
1574                        arena,
1575                        surface,
1576                        child_id,
1577                        level,
1578                        rule_groups,
1579                        config,
1580                    )
1581                }));
1582
1583        if let Some(bit) = cache_bit {
1584            let entry = &mut self.subtree_candidate_cache[surface][node_id.index()];
1585            entry.known_levels |= bit;
1586            if has_candidates {
1587                entry.candidate_levels |= bit;
1588            }
1589        }
1590        has_candidates
1591    }
1592}
1593
1594fn next_worklist_candidate_level(
1595    arena: &ExpressionArena,
1596    node_id: ExpressionNodeId,
1597    start_level: usize,
1598    rule_groups: &[RuleGroup<'_>],
1599    config: RewriteConfig,
1600) -> usize {
1601    if start_level >= rule_groups.len() || !arena.is_reachable(node_id) {
1602        return rule_groups.len();
1603    }
1604
1605    rule_groups
1606        .iter()
1607        .enumerate()
1608        .skip(start_level)
1609        .find_map(|(level, rule_group)| {
1610            rule_group
1611                .has_candidates_at_node(config, arena, node_id)
1612                .then_some(level)
1613        })
1614        .unwrap_or(rule_groups.len())
1615}
1616
1617struct RuleEffectImpact {
1618    added_names: Vec<Name>,
1619    changed_names: Vec<Name>,
1620    invalidated_reference_names: Vec<Name>,
1621    has_new_top: bool,
1622    has_new_sat_decisions: bool,
1623}
1624
1625impl RuleEffectImpact {
1626    fn new(
1627        effect: &crate::rule_engine::rule::RuleEffect,
1628        symbols: &crate::ast::SymbolTable,
1629    ) -> Self {
1630        let mut changed_names: Vec<_> = effect
1631            .changed_symbols(symbols)
1632            .into_iter()
1633            .map(|(name, _, _)| name)
1634            .collect();
1635        for name in effect.updated_declaration_names() {
1636            if !changed_names.contains(&name) {
1637                changed_names.push(name);
1638            }
1639        }
1640        let added_names: Vec<_> = effect.added_symbols(symbols).into_iter().collect();
1641        // A new auxiliary stands in for the declaration it was derived from, so expressions
1642        // referencing that source have to be reconsidered alongside the ones that changed.
1643        let mut invalidated_reference_names = changed_names.clone();
1644        for added_name in &added_names {
1645            let Some(declaration) = effect.symbols.lookup_local(added_name) else {
1646                continue;
1647            };
1648            let mut seen_sources = HashSet::new();
1649            let mut source = declaration.source().clone();
1650            while let Some(source_declaration) = source.take() {
1651                let source_name = source_declaration.name().clone();
1652                if !seen_sources.insert(source_name.clone()) {
1653                    break;
1654                }
1655                if !invalidated_reference_names.contains(&source_name) {
1656                    invalidated_reference_names.push(source_name);
1657                }
1658                source.clone_from(&source_declaration.source());
1659            }
1660        }
1661
1662        Self {
1663            added_names,
1664            changed_names,
1665            invalidated_reference_names,
1666            has_new_top: !effect.new_top.is_empty(),
1667            has_new_sat_decisions: !effect.new_sat_decisions.is_empty(),
1668        }
1669    }
1670
1671    fn has_model_side_effects(&self) -> bool {
1672        self.has_new_top || self.has_new_sat_decisions || self.has_symbol_changes()
1673    }
1674
1675    fn has_symbol_changes(&self) -> bool {
1676        !self.added_names.is_empty() || !self.changed_names.is_empty()
1677    }
1678}
1679
1680struct RewritePassContext<'ctx, 'rules, O> {
1681    rules_grouped: &'ctx Vec<(u16, Vec<RuleData<'rules>>)>,
1682    bucketed_rules: &'ctx Vec<RuleGroup<'rules>>,
1683    stats: &'ctx mut RewriterStats,
1684    dirty_trace: &'ctx mut DirtyTrace,
1685    config: RewriteConfig,
1686    attempt_observer: &'ctx mut O,
1687}
1688
1689struct TracingObserver<'run> {
1690    run_start: &'run Instant,
1691    general_enabled: bool,
1692    csv_enabled: bool,
1693}
1694
1695impl RuleAttemptObserver for TracingObserver<'_> {
1696    fn attempted(
1697        &mut self,
1698        priority: u16,
1699        rule: &RuleData<'_>,
1700        expression: &Expr,
1701        status: RuleAttemptStatus,
1702    ) {
1703        let elapsed_s = self.run_start.elapsed().as_secs_f64();
1704
1705        if self.general_enabled {
1706            trace!(
1707                target: "conjure::rule_attempt",
1708                elapsed_s,
1709                rule_level = priority,
1710                rule_name = rule.rule.name,
1711                rule_set = rule.rule_set.name,
1712                status = status.as_str(),
1713                expression = %expression,
1714                "attempted rule"
1715            );
1716        }
1717
1718        if self.csv_enabled {
1719            let expression = expression.to_string();
1720            trace!(
1721                target: "rule_engine_rule_attempt_trace",
1722                "{elapsed_s:.3},{priority},{},{},{},{}",
1723                csv_escape(rule.rule.name),
1724                csv_escape(rule.rule_set.name),
1725                status.as_str(),
1726                csv_escape(&expression),
1727            );
1728        }
1729    }
1730}
1731
1732/// True when a domain still needs Essence→Essence' abstract representation
1733/// (set/tuple/record/…); concrete bool/int and matrices of those do not.
1734fn domain_needs_abstract_repr(domain: &crate::ast::DomainPtr) -> bool {
1735    domain_needs_abstract_repr_at(domain, true)
1736}
1737
1738/// `is_value` is false for a matrix's index domains, which describe which entries exist rather
1739/// than a value the solver assigns, and so are never encoded.
1740fn 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            // Every matrix has a layout to choose between, so representation selection has work
1748            // to do whatever the elements are.
1749            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
1763/// Whether `ReprGeneral` / `ReprTuplePacked` can do useful work on this model.
1764///
1765/// Models that only use bools, ints, and matrices of those (e.g. solitaire_battleship)
1766/// still paid hundreds of thousands of failed attempts on record/tuple/set rules.
1767fn 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    // Record/tuple/set *literals* also need ReprGeneral even without abstract finds.
1779    // Matrix literals are handled by `ReprMatrixComponents`, not these rule sets.
1780    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
1789/// Rewrites a model by applying rules in priority order, trying enclosing expressions before their
1790/// descendants at each priority.
1791pub fn rewrite_model<'a>(
1792    model: &Model,
1793    rule_sets: &Vec<&'a RuleSet<'a>>,
1794    config: RewriteConfig,
1795) -> Result<Model, RewriteError> {
1796    let _uniform_scope = crate::representation::uniform::Scope::new();
1797    if crate::settings::channelling() == crate::settings::Channelling::Uniform {
1798        for (_, decl) in model.symbols().iter_local() {
1799            if let Some(dom) = decl.domain() {
1800                for (_, state) in decl.reprs().iter() {
1801                    crate::representation::uniform::check_choice(&dom, state.rule())
1802                        .map_err(|e| RewriteError::UniformRepresentation(e.to_string()))?;
1803                    crate::representation::uniform::record_choice(&dom, state.rule());
1804                }
1805            }
1806        }
1807    }
1808    set_current_rewriter(Rewriter::Rewrite(config));
1809
1810    let needs_abstract_repr = model_needs_abstract_repr_rules(model);
1811    let filtered_rule_sets: Vec<&'a RuleSet<'a>> = if needs_abstract_repr {
1812        rule_sets.clone()
1813    } else {
1814        rule_sets
1815            .iter()
1816            .copied()
1817            .filter(|rs| rs.name != "ReprGeneral" && rs.name != "ReprTuplePacked")
1818            .collect()
1819    };
1820
1821    let rules_grouped = get_rules_grouped(&filtered_rule_sets)
1822        .unwrap_or_else(|_| bug!("get_rule_priorities() failed!"))
1823        .into_iter()
1824        .collect_vec();
1825    let bucketed_rules = rules_grouped
1826        .iter()
1827        .map(|(priority, rules)| RuleGroup::new(*priority, rules.clone()))
1828        .collect_vec();
1829
1830    let mut model = introduce_objective_auxiliary(model.clone());
1831    if rule_sets.iter().any(|rules| rules.name == "SAT")
1832        && let Some(objective) = &model.objective
1833        && !model
1834            .sat_decisions()
1835            .iter()
1836            .any(|decision| matches!(decision, crate::ast::SatEncodingDecision::Objective { .. }))
1837        && !model
1838            .root()
1839            .any_expression(|expression| matches!(expression, Expr::SatObjective(..)))
1840    {
1841        model.add_constraint(Expr::SatObjective(
1842            crate::ast::Metadata::new(),
1843            matches!(
1844                objective.direction,
1845                crate::ast::OptimiseDirection::Minimising
1846            ),
1847            crate::ast::Moo::new(objective.expression.clone()),
1848        ));
1849    }
1850    tighten_domains_from_constraints(&mut model);
1851    let mut rewriter_stats = RewriterStats::new();
1852    rewriter_stats.is_optimisation_enabled = Some(!config.is_baseline());
1853    let mut dirty_trace = DirtyTrace::from_env();
1854    let run_start = Instant::now();
1855
1856    if rule_trace_enabled() && default_rule_trace_enabled() {
1857        trace!(
1858            target: "rule_engine_rule_trace",
1859            "Model before rewriting:\n\n{}\n--\n",
1860            model
1861        );
1862    }
1863    if rule_trace_enabled() && rule_attempt_trace_enabled() {
1864        trace!(
1865            target: "rule_engine_rule_attempt_trace",
1866            "elapsed_s,rule_level,rule_name,rule_set,status,expression"
1867        );
1868    }
1869
1870    let general_attempt_trace_enabled = tracing::enabled!(
1871        target: "conjure::rule_attempt",
1872        tracing::Level::TRACE
1873    );
1874    let csv_attempt_trace_enabled = rule_trace_enabled() && rule_attempt_trace_enabled();
1875
1876    if general_attempt_trace_enabled || csv_attempt_trace_enabled {
1877        let mut observer = TracingObserver {
1878            run_start: &run_start,
1879            general_enabled: general_attempt_trace_enabled,
1880            csv_enabled: csv_attempt_trace_enabled,
1881        };
1882        rewrite_to_fixpoint(
1883            &mut model,
1884            &rules_grouped,
1885            &bucketed_rules,
1886            &mut rewriter_stats,
1887            &mut dirty_trace,
1888            config,
1889            &mut observer,
1890        );
1891    } else {
1892        let mut observer = NoopObserver;
1893        rewrite_to_fixpoint(
1894            &mut model,
1895            &rules_grouped,
1896            &bucketed_rules,
1897            &mut rewriter_stats,
1898            &mut dirty_trace,
1899            config,
1900            &mut observer,
1901        );
1902    }
1903
1904    let run_end = Instant::now();
1905    rewriter_stats.rewriter_run_time = Some(run_end - run_start);
1906    model
1907        .context
1908        .write()
1909        .unwrap()
1910        .stats
1911        .add_rewriter_run(rewriter_stats);
1912    dirty_trace.finish(
1913        model
1914            .context
1915            .read()
1916            .unwrap()
1917            .stats
1918            .rewriter_runs
1919            .last()
1920            .expect("rewriter stats were just added"),
1921    );
1922
1923    super::encoding_selection::resolve_encoding_choices(model.sat_decisions_mut());
1924
1925    if rule_trace_enabled() && default_rule_trace_enabled() {
1926        trace!(
1927            target: "rule_engine_rule_trace",
1928            "Final model:\n\n{}",
1929            model
1930        );
1931    }
1932
1933    // Flatten top-level `and` into the root constraint list (and strip `true` / propagate
1934    // `false`) after rewriting. Must not run mid-loop: early flattening of large expanded
1935    // conjunctions explodes worklist rule attempts. Solver adaptors expect one flat constraint
1936    // per root entry.
1937    if let Some(normalised_root) = finish_root_evaluator_normalisation(model.root()) {
1938        model.replace_root(normalised_root);
1939    }
1940
1941    if crate::settings::channelling() == crate::settings::Channelling::Uniform {
1942        for (_, decl) in model.symbols().iter_local() {
1943            if decl.as_find().is_some()
1944                && decl.reprs().is_empty()
1945                && let Some(dom) = decl.domain()
1946                && let Some(selected) = crate::representation::uniform::selected(&dom, false)
1947                && selected.probe_for(decl).is_err()
1948            {
1949                return Err(RewriteError::UniformRepresentation(format!(
1950                    "uniform representation `{}` does not support `{}` with domain `{}`",
1951                    selected.name(),
1952                    decl.name(),
1953                    dom,
1954                )));
1955            }
1956        }
1957    }
1958    Ok(model)
1959}
1960
1961#[allow(clippy::too_many_arguments)]
1962fn rewrite_to_fixpoint<'rules, O: RuleAttemptObserver>(
1963    model: &mut Model,
1964    rules_grouped: &Vec<(u16, Vec<RuleData<'rules>>)>,
1965    bucketed_rules: &Vec<RuleGroup<'rules>>,
1966    stats: &mut RewriterStats,
1967    dirty_trace: &mut DirtyTrace,
1968    config: RewriteConfig,
1969    attempt_observer: &mut O,
1970) {
1971    let mut pass_ctx = RewritePassContext {
1972        rules_grouped,
1973        bucketed_rules,
1974        stats,
1975        dirty_trace,
1976        config,
1977        attempt_observer,
1978    };
1979    if config.worklist {
1980        let _ = try_rewrite_model(model, &mut pass_ctx);
1981    } else {
1982        let mut done_something = true;
1983        while done_something {
1984            done_something = try_rewrite_model(model, &mut pass_ctx).is_some();
1985        }
1986    }
1987}
1988
1989// Tries to rewrite the model until a full scan finds no applicable rules.
1990//
1991// Returns None if no change was made.
1992fn try_rewrite_model<'ctx, 'rules, O: RuleAttemptObserver>(
1993    submodel: &mut Model,
1994    ctx: &mut RewritePassContext<'ctx, 'rules, O>,
1995) -> Option<()> {
1996    ctx.dirty_trace.passes += 1;
1997    if !ctx.config.worklist
1998        && try_rewrite_value_letting_once(submodel, ctx.rules_grouped, ctx.attempt_observer)
1999            .is_some()
2000    {
2001        ctx.dirty_trace.value_letting_rewrites += 1;
2002        increment_counter(&mut ctx.stats.rewriter_value_letting_rewrites);
2003        return Some(());
2004    }
2005
2006    let mut did_rewrite = false;
2007    let mut arena = ExpressionArena::from_root(take_model_root(submodel));
2008    normalise_evaluators_bottom_up(&mut arena, ctx.dirty_trace);
2009
2010    if ctx.config.worklist {
2011        return try_rewrite_model_with_worklist(submodel, ctx, arena);
2012    }
2013
2014    'rewrite_loop: loop {
2015        let mut results: Vec<ApplicableRule<'_, ExpressionNodeId>> = vec![];
2016        let preorder_ids = rewriter_preorder_ids(&arena);
2017        // Iterate over rules by priority in descending order.
2018        'top: for (level, rule_group) in ctx.bucketed_rules.iter().enumerate() {
2019            ctx.dirty_trace.priority_scans += 1;
2020            for &node_id in &preorder_ids {
2021                ctx.dirty_trace.expression_visits += 1;
2022                let mut attempted_rule = false;
2023                {
2024                    let expr = arena.expression(node_id);
2025                    for rd in rule_group.candidates(ctx.config, expr) {
2026                        attempted_rule = true;
2027                        ctx.dirty_trace
2028                            .record_rule_attempt(rule_group.priority, rd.rule.name);
2029                        // Count rule application attempts
2030                        ctx.stats.rewriter_rule_application_attempts =
2031                            Some(ctx.stats.rewriter_rule_application_attempts.unwrap_or(0) + 1);
2032
2033                        match rd.rule.apply(expr, &submodel.symbols()) {
2034                            Ok(red) => {
2035                                ctx.attempt_observer.attempted(
2036                                    rule_group.priority,
2037                                    rd,
2038                                    expr,
2039                                    RuleAttemptStatus::Success,
2040                                );
2041
2042                                // Count successful rule applications
2043                                ctx.stats.rewriter_rule_applications =
2044                                    Some(ctx.stats.rewriter_rule_applications.unwrap_or(0) + 1);
2045
2046                                // Collect applicable rules
2047                                results.push((
2048                                    RuleResult {
2049                                        rule_data: rd.clone(),
2050                                        effect: red,
2051                                    },
2052                                    level,
2053                                    expr.clone(),
2054                                    node_id,
2055                                    root_variable_snapshot_for_default_trace(
2056                                        expr,
2057                                        &submodel.symbols(),
2058                                    ),
2059                                ));
2060                            }
2061                            Err(_) => {
2062                                ctx.attempt_observer.attempted(
2063                                    rule_group.priority,
2064                                    rd,
2065                                    expr,
2066                                    RuleAttemptStatus::Failure,
2067                                );
2068                            }
2069                        }
2070                    }
2071                }
2072                if attempted_rule {
2073                    ctx.dirty_trace.attempted_expressions += 1;
2074                }
2075                // This expression has the highest rule priority so far, so this is what we want to
2076                // rewrite.
2077                if !results.is_empty() {
2078                    break 'top;
2079                }
2080            }
2081        }
2082
2083        if !results.is_empty() {
2084            let selected =
2085                choose_rule_result_index(results.iter().map(|(result, _, _, _, _)| result));
2086            results.swap(0, selected);
2087        }
2088
2089        match results.into_iter().next() {
2090            None => {
2091                submodel.replace_root(arena.into_synced_root_expression());
2092                break;
2093            }
2094            Some((result, _level, expr, node_id, variable_snapshot_before)) => {
2095                let effect = result.effect.materialise(&submodel.symbols());
2096                let variable_snapshots = variable_snapshot_before.map(|before| {
2097                    let after = snapshot_symbols_after_effect(&submodel.symbols(), &effect);
2098                    (before, after)
2099                });
2100                let result = RuleResult {
2101                    rule_data: result.rule_data,
2102                    effect,
2103                };
2104
2105                // Extract the single applicable rule and apply it
2106                log_rule_application(
2107                    &result,
2108                    &expr,
2109                    &submodel.symbols(),
2110                    variable_snapshots
2111                        .as_ref()
2112                        .map(|(before, after)| (before, after)),
2113                );
2114
2115                let effect_impact = RuleEffectImpact::new(&result.effect, &submodel.symbols());
2116                let has_model_side_effects = effect_impact.has_model_side_effects();
2117                let rule_name = result.rule_data.rule.name;
2118                let RuleResult { effect, .. } = result;
2119                let crate::rule_engine::rule::RuleEffect {
2120                    new_expression,
2121                    new_top,
2122                    symbols,
2123                    new_sat_decisions,
2124                    declaration_updates,
2125                    ..
2126                } = effect;
2127                // Replace expr with new_expression
2128                replace_focus_and_sync_ancestors(&mut arena, node_id, new_expression);
2129
2130                // Apply new symbols and top level
2131                ctx.dirty_trace
2132                    .record_rewrite(rule_name, has_model_side_effects);
2133                for update in declaration_updates {
2134                    update.apply();
2135                }
2136                submodel.symbols_mut().extend(symbols);
2137                if effect_impact.has_new_top {
2138                    arena.add_root_children(new_top);
2139                }
2140                submodel.add_sat_decisions(new_sat_decisions);
2141                let _ =
2142                    normalise_evaluators_from_node_to_root(&mut arena, node_id, ctx.dirty_trace);
2143                if has_model_side_effects {
2144                    ctx.dirty_trace.record_side_effect_kept_in_arena();
2145                }
2146
2147                #[cfg(debug_assertions)]
2148                {
2149                    // Check well-formedness without rebuilding the live arena: a rebuild would
2150                    // renumber nodes and can change subsequent full-scan order vs release builds.
2151                    submodel.replace_root(arena.expression(arena.root()).clone());
2152                    let assertion_context = format!("rewriter after applying rule '{rule_name}'");
2153                    debug_assert_model_well_formed(submodel, &assertion_context);
2154                }
2155
2156                did_rewrite = true;
2157                continue 'rewrite_loop;
2158            }
2159        }
2160    }
2161
2162    did_rewrite.then_some(())
2163}
2164
2165fn try_rewrite_model_with_worklist<'ctx, 'rules, O: RuleAttemptObserver>(
2166    submodel: &mut Model,
2167    ctx: &mut RewritePassContext<'ctx, 'rules, O>,
2168    arena: ExpressionArena,
2169) -> Option<()> {
2170    let mut did_rewrite = false;
2171    let root_surface = 0usize;
2172    let (mut surfaces, mut value_letting_surfaces) = build_worklist_surfaces(submodel, arena);
2173    for surface in &mut surfaces {
2174        if surface.active {
2175            normalise_evaluators_bottom_up(&mut surface.arena, ctx.dirty_trace);
2176        }
2177    }
2178    write_worklist_surfaces_to_model(submodel, &surfaces);
2179    let mut scheduler = WorklistScheduler::new(&surfaces, ctx.bucketed_rules, ctx.config);
2180    let mut deferred_evaluators = DeferredEvaluatorWork::default();
2181    let mut deferred_ancestor_rules = DeferredAncestorRuleWork::default();
2182
2183    loop {
2184        let next = loop {
2185            let candidate =
2186                scheduler.pop_next(&surfaces, ctx.bucketed_rules, ctx.config, ctx.dirty_trace);
2187            if let Some((_, surface, node_id, _)) = candidate
2188                && deferred_ancestor_rules.contains(surface, node_id)
2189            {
2190                deferred_ancestor_rules.postpone(candidate.unwrap());
2191                continue;
2192            }
2193            break candidate;
2194        };
2195        let crosses_priority_boundary =
2196            deferred_ancestor_rules.should_flush_before(next.as_ref().map(|item| item.0));
2197        if crosses_priority_boundary {
2198            // `pop_next` removes a live item from the queue. Put it back before injecting the
2199            // higher-priority ancestor checks that must precede it.
2200            if let Some((level, surface, node_id, mode)) = next {
2201                scheduler.enqueue_node_at_level(
2202                    &surfaces[surface].arena,
2203                    surface,
2204                    node_id,
2205                    level,
2206                    mode,
2207                    Some(ctx.dirty_trace),
2208                );
2209            }
2210            deferred_ancestor_rules.flush(
2211                &mut scheduler,
2212                &mut surfaces,
2213                &mut deferred_evaluators,
2214                ctx.dirty_trace,
2215            );
2216            continue;
2217        }
2218
2219        let Some((level, surface_index, node_id, scheduled_mode)) = next else {
2220            if normalise_deferred_evaluators(
2221                &mut deferred_evaluators,
2222                submodel,
2223                &mut surfaces,
2224                &mut scheduler,
2225                ctx.dirty_trace,
2226            ) {
2227                did_rewrite = true;
2228                continue;
2229            }
2230            break;
2231        };
2232        ctx.dirty_trace.priority_scans += 1;
2233        ctx.dirty_trace.expression_visits += 1;
2234
2235        let rule_group = &ctx.bucketed_rules[level];
2236        if !rule_group.has_candidates_at_node(ctx.config, &surfaces[surface_index].arena, node_id) {
2237            ctx.dirty_trace
2238                .record_worklist_no_candidate_pop(scheduled_mode);
2239            scheduler.enqueue_after_no_rewrite(
2240                WorklistSchedulingContext::new(
2241                    &surfaces[surface_index].arena,
2242                    surface_index,
2243                    ctx.bucketed_rules,
2244                    ctx.config,
2245                ),
2246                node_id,
2247                level,
2248                level + 1,
2249                scheduled_mode,
2250                Some(ctx.dirty_trace),
2251            );
2252            continue;
2253        }
2254
2255        let mut results: Vec<ApplicableRule<'_, ExpressionNodeId>> = vec![];
2256        let mut attempted_rule = false;
2257        {
2258            let arena = &surfaces[surface_index].arena;
2259            let expr = arena.expression(node_id);
2260            for rd in rule_group.candidates_at_node(ctx.config, arena, node_id) {
2261                if !scheduler.should_attempt_rule(surface_index, node_id, rd) {
2262                    continue;
2263                }
2264                attempted_rule = true;
2265                ctx.dirty_trace
2266                    .record_rule_attempt(rule_group.priority, rd.rule.name);
2267                ctx.stats.rewriter_rule_application_attempts =
2268                    Some(ctx.stats.rewriter_rule_application_attempts.unwrap_or(0) + 1);
2269
2270                match rd.rule.apply(expr, &submodel.symbols()) {
2271                    Ok(red) => {
2272                        ctx.attempt_observer.attempted(
2273                            rule_group.priority,
2274                            rd,
2275                            expr,
2276                            RuleAttemptStatus::Success,
2277                        );
2278
2279                        ctx.stats.rewriter_rule_applications =
2280                            Some(ctx.stats.rewriter_rule_applications.unwrap_or(0) + 1);
2281
2282                        results.push((
2283                            RuleResult {
2284                                rule_data: rd.clone(),
2285                                effect: red,
2286                            },
2287                            level,
2288                            expr.clone(),
2289                            node_id,
2290                            root_variable_snapshot_for_default_trace(expr, &submodel.symbols()),
2291                        ));
2292                    }
2293                    Err(_) => {
2294                        scheduler.record_rule_failure(surface_index, node_id, rd);
2295                        ctx.attempt_observer.attempted(
2296                            rule_group.priority,
2297                            rd,
2298                            expr,
2299                            RuleAttemptStatus::Failure,
2300                        );
2301                    }
2302                }
2303            }
2304        }
2305
2306        if attempted_rule {
2307            ctx.dirty_trace.attempted_expressions += 1;
2308            ctx.dirty_trace
2309                .record_worklist_rule_attempt_pop(scheduled_mode);
2310        }
2311
2312        if results.is_empty() {
2313            scheduler.enqueue_after_no_rewrite(
2314                WorklistSchedulingContext::new(
2315                    &surfaces[surface_index].arena,
2316                    surface_index,
2317                    ctx.bucketed_rules,
2318                    ctx.config,
2319                ),
2320                node_id,
2321                level,
2322                level + 1,
2323                scheduled_mode,
2324                Some(ctx.dirty_trace),
2325            );
2326            continue;
2327        }
2328
2329        let selected = choose_rule_result_index(results.iter().map(|(result, _, _, _, _)| result));
2330        let (result, _level, expr, node_id, variable_snapshot_before) =
2331            results.swap_remove(selected);
2332
2333        let effect = result.effect.materialise(&submodel.symbols());
2334        let variable_snapshots = variable_snapshot_before.map(|before| {
2335            let after = snapshot_symbols_after_effect(&submodel.symbols(), &effect);
2336            (before, after)
2337        });
2338        let result = RuleResult {
2339            rule_data: result.rule_data,
2340            effect,
2341        };
2342
2343        log_rule_application(
2344            &result,
2345            &expr,
2346            &submodel.symbols(),
2347            variable_snapshots
2348                .as_ref()
2349                .map(|(before, after)| (before, after)),
2350        );
2351
2352        let effect_impact = RuleEffectImpact::new(&result.effect, &submodel.symbols());
2353        let has_model_side_effects = effect_impact.has_model_side_effects();
2354        let rewritten_value_letting_name =
2355            value_letting_surface_name(&surfaces[surface_index].kind).cloned();
2356        let invalidates_symbol_rules =
2357            effect_impact.has_symbol_changes() || rewritten_value_letting_name.is_some();
2358        let rule_name = result.rule_data.rule.name;
2359        let RuleResult { effect, .. } = result;
2360        let crate::rule_engine::rule::RuleEffect {
2361            new_expression,
2362            new_top,
2363            symbols,
2364            new_sat_decisions,
2365            declaration_updates,
2366            ..
2367        } = effect;
2368        {
2369            let arena = &mut surfaces[surface_index].arena;
2370            arena.replace_subtree(node_id, new_expression);
2371        }
2372
2373        ctx.dirty_trace
2374            .record_rewrite(rule_name, has_model_side_effects);
2375        for update in declaration_updates {
2376            update.apply();
2377        }
2378        submodel.symbols_mut().extend(symbols);
2379        let new_top_node_ids = if effect_impact.has_new_top {
2380            surfaces[root_surface].arena.add_root_children(new_top)
2381        } else {
2382            Vec::new()
2383        };
2384        submodel.add_sat_decisions(new_sat_decisions);
2385        let rewrite_impact_node_id = {
2386            let arena = &mut surfaces[surface_index].arena;
2387            normalise_evaluators_subtree_bottom_up(arena, node_id, ctx.dirty_trace);
2388            deferred_evaluators.defer_ancestors(arena, surface_index, node_id);
2389            deferred_ancestor_rules.defer_ancestors(arena, surface_index, node_id, level);
2390            node_id
2391        };
2392        scheduler.index_subtree_references(
2393            &surfaces[surface_index].arena,
2394            surface_index,
2395            rewrite_impact_node_id,
2396        );
2397        for &new_top_node_id in &new_top_node_ids {
2398            if surfaces[root_surface].arena.is_reachable(new_top_node_id) {
2399                normalise_evaluators_subtree_bottom_up(
2400                    &mut surfaces[root_surface].arena,
2401                    new_top_node_id,
2402                    ctx.dirty_trace,
2403                );
2404                deferred_evaluators.defer_ancestors(
2405                    &surfaces[root_surface].arena,
2406                    root_surface,
2407                    new_top_node_id,
2408                );
2409                deferred_ancestor_rules.defer_ancestors(
2410                    &surfaces[root_surface].arena,
2411                    root_surface,
2412                    new_top_node_id,
2413                    level,
2414                );
2415                scheduler.index_subtree_references(
2416                    &surfaces[root_surface].arena,
2417                    root_surface,
2418                    new_top_node_id,
2419                );
2420            }
2421        }
2422        if let Some(name) = rewritten_value_letting_name.as_ref() {
2423            write_value_letting_surface_to_model(submodel, name, &surfaces[surface_index].arena);
2424            ctx.dirty_trace.value_letting_rewrites += 1;
2425            increment_counter(&mut ctx.stats.rewriter_value_letting_rewrites);
2426        }
2427        let mut affected_names = effect_impact.invalidated_reference_names.clone();
2428        if let Some(name) = rewritten_value_letting_name.as_ref()
2429            && !affected_names.contains(name)
2430        {
2431            affected_names.push(name.clone());
2432        }
2433        let mut symbol_surface_names = effect_impact.added_names.clone();
2434        symbol_surface_names.extend(effect_impact.changed_names.iter().cloned());
2435        if let Some(name) = rewritten_value_letting_name.as_ref() {
2436            symbol_surface_names.retain(|candidate| candidate != name);
2437        }
2438        let synced_surfaces = sync_value_letting_surfaces(
2439            submodel,
2440            &mut surfaces,
2441            &mut value_letting_surfaces,
2442            &symbol_surface_names,
2443        );
2444
2445        // Choosing a representation, or any other change to the model, can change which rule
2446        // should win for a sibling at the same priority. Do not batch across that: bring the
2447        // enclosing expressions up to date and re-check their rules before moving to another
2448        // sibling. Rewrites that only change an expression still get batched, and those are the
2449        // common case when unrolling.
2450        if has_model_side_effects {
2451            deferred_ancestor_rules.flush_effect_path_before_root(
2452                &mut scheduler,
2453                &mut surfaces,
2454                &mut deferred_evaluators,
2455                surface_index,
2456                rewrite_impact_node_id,
2457                ctx.dirty_trace,
2458            );
2459        }
2460        {
2461            if has_model_side_effects {
2462                ctx.dirty_trace.record_side_effect_kept_in_arena();
2463            }
2464            if invalidates_symbol_rules {
2465                scheduler.invalidate_symbol_rule_failures();
2466                scheduler.enqueue_node_at_level(
2467                    &surfaces[root_surface].arena,
2468                    root_surface,
2469                    surfaces[root_surface].arena.root(),
2470                    0,
2471                    ScheduledMode::CheckNode,
2472                    Some(ctx.dirty_trace),
2473                );
2474            }
2475            scheduler.enqueue_subtree(
2476                &surfaces[surface_index].arena,
2477                surface_index,
2478                rewrite_impact_node_id,
2479                Some(ctx.dirty_trace),
2480            );
2481            for new_top_node_id in new_top_node_ids {
2482                scheduler.enqueue_subtree(
2483                    &surfaces[root_surface].arena,
2484                    root_surface,
2485                    new_top_node_id,
2486                    Some(ctx.dirty_trace),
2487                );
2488            }
2489            for synced_surface in synced_surfaces {
2490                scheduler.enqueue_surface(
2491                    &surfaces,
2492                    synced_surface,
2493                    ctx.bucketed_rules,
2494                    ctx.config,
2495                    Some(ctx.dirty_trace),
2496                );
2497            }
2498            enqueue_worklist_nodes_referencing_names(
2499                &mut scheduler,
2500                &surfaces,
2501                &affected_names,
2502                ctx.dirty_trace,
2503            );
2504        }
2505
2506        did_rewrite = true;
2507    }
2508
2509    move_worklist_surfaces_to_model(submodel, surfaces);
2510    #[cfg(debug_assertions)]
2511    debug_assert_model_well_formed(submodel, "rewriter after a settled worklist pass");
2512    did_rewrite.then_some(())
2513}
2514
2515#[cfg(test)]
2516fn enqueue_worklist_rewrite_impact(
2517    scheduler: &mut WorklistScheduler,
2518    arena: &ExpressionArena,
2519    surface: usize,
2520    node_id: ExpressionNodeId,
2521    dirty_trace: &mut DirtyTrace,
2522) {
2523    scheduler.enqueue_node_and_ancestors(arena, surface, node_id, dirty_trace);
2524    scheduler.enqueue_subtree(arena, surface, node_id, Some(dirty_trace));
2525}
2526
2527/// Rebuilds and evaluates the waiting ancestors, once the worklist has run out of rule work.
2528///
2529/// Nodes are handled deepest first, so each parent is rebuilt once from children that are already
2530/// up to date. Doing it here rather than after every rewrite is what stops each rewrite from
2531/// copying the whole matrix/`and`/root chain above it. The surfaces that changed go back on the
2532/// worklist, and later rewrites fill the next batch.
2533fn normalise_deferred_evaluators(
2534    work: &mut DeferredEvaluatorWork,
2535    submodel: &mut Model,
2536    surfaces: &mut [RewriteSurface],
2537    scheduler: &mut WorklistScheduler,
2538    dirty_trace: &mut DirtyTrace,
2539) -> bool {
2540    if work.is_empty() {
2541        return false;
2542    }
2543
2544    let nodes = deferred_nodes_in_bottom_up_order(std::mem::take(&mut work.nodes), surfaces);
2545
2546    let mut affected_surfaces = HashSet::new();
2547    let mut changed_value_lettings = HashSet::new();
2548    for (surface, node_id, _) in nodes {
2549        let Some(rewrite_surface) = surfaces.get_mut(surface) else {
2550            continue;
2551        };
2552        if !rewrite_surface.active || !rewrite_surface.arena.is_reachable(node_id) {
2553            continue;
2554        }
2555        affected_surfaces.insert(surface);
2556        rewrite_surface.arena.rebuild_payload_from_children(node_id);
2557
2558        // Root-list reshaping is deliberately left to the final evaluator pass.
2559        if node_id != rewrite_surface.arena.root()
2560            && normalise_evaluator_node_to_fixpoint_without_ancestor_sync(
2561                &mut rewrite_surface.arena,
2562                node_id,
2563                dirty_trace,
2564            )
2565        {
2566            scheduler.index_subtree_references(&rewrite_surface.arena, surface, node_id);
2567            if let Some(name) = value_letting_surface_name(&rewrite_surface.kind) {
2568                changed_value_lettings.insert(name.clone());
2569            }
2570        }
2571    }
2572
2573    let mut wrote_value_letting = false;
2574    let mut changed_value_lettings: Vec<_> = changed_value_lettings.into_iter().collect();
2575    changed_value_lettings.sort();
2576    for name in &changed_value_lettings {
2577        let Some(surface) = surfaces.iter().find(|surface| {
2578            surface.active && value_letting_surface_name(&surface.kind) == Some(name)
2579        }) else {
2580            continue;
2581        };
2582        wrote_value_letting |=
2583            write_value_letting_surface_to_model_without_refresh(submodel, name, &surface.arena);
2584    }
2585    if wrote_value_letting {
2586        submodel.symbols_mut().refresh_local_binding_hashes();
2587        scheduler.invalidate_symbol_rule_failures();
2588        scheduler.enqueue_node_at_level(
2589            &surfaces[0].arena,
2590            0,
2591            surfaces[0].arena.root(),
2592            0,
2593            ScheduledMode::CheckNode,
2594            Some(dirty_trace),
2595        );
2596        enqueue_worklist_nodes_referencing_names(
2597            scheduler,
2598            surfaces,
2599            &changed_value_lettings,
2600            dirty_trace,
2601        );
2602    }
2603
2604    let mut affected_surfaces: Vec<_> = affected_surfaces.into_iter().collect();
2605    affected_surfaces.sort_unstable();
2606    for surface in affected_surfaces.iter().copied() {
2607        let arena = &surfaces[surface].arena;
2608        scheduler.enqueue_subtree(arena, surface, arena.root(), Some(dirty_trace));
2609    }
2610
2611    !affected_surfaces.is_empty()
2612}
2613
2614fn deferred_nodes_in_bottom_up_order(
2615    nodes: impl IntoIterator<Item = (usize, ExpressionNodeId)>,
2616    surfaces: &[RewriteSurface],
2617) -> Vec<(usize, ExpressionNodeId, Option<Vec<usize>>)> {
2618    // Cache each rarely needed path once. Reconstructing paths inside the comparison closure would
2619    // turn sorting into O(n log n) parent-chain walks.
2620    let mut nodes: Vec<_> = nodes
2621        .into_iter()
2622        .map(|(surface, node_id)| {
2623            let path = surfaces
2624                .get(surface)
2625                .filter(|rewrite_surface| rewrite_surface.active)
2626                .filter(|rewrite_surface| rewrite_surface.arena.is_reachable(node_id))
2627                .map(|rewrite_surface| rewrite_surface.arena.preorder_path(node_id));
2628            (surface, node_id, path)
2629        })
2630        .collect();
2631    nodes.sort_by(
2632        |(left_surface, left_node, left_path), (right_surface, right_node, right_path)| {
2633            right_path
2634                .as_ref()
2635                .map_or(0, Vec::len)
2636                .cmp(&left_path.as_ref().map_or(0, Vec::len))
2637                .then_with(|| left_surface.cmp(right_surface))
2638                .then_with(|| match (left_path, right_path) {
2639                    (Some(left_path), Some(right_path)) => left_path.cmp(right_path),
2640                    _ => left_node.cmp(right_node),
2641                })
2642        },
2643    );
2644    nodes
2645}
2646
2647/// Applies evaluator normalisation throughout an existing arena surface.
2648///
2649/// This is the initial half of the evaluator normalisation hook. Evaluation is a privileged pure
2650/// simplification rather than an ordinary rule: before normal rule scheduling starts, each surface
2651/// is normalised bottom-up so parent evaluators can assume already-normal children. Comprehensions
2652/// remain atomic here for the same scoped-rewrite reason as normal scheduler traversal.
2653fn normalise_evaluators_bottom_up(arena: &mut ExpressionArena, dirty_trace: &mut DirtyTrace) {
2654    normalise_evaluators_subtree_bottom_up(arena, arena.root(), dirty_trace);
2655}
2656
2657fn normalise_evaluators_subtree_bottom_up(
2658    arena: &mut ExpressionArena,
2659    subtree_root: ExpressionNodeId,
2660    dirty_trace: &mut DirtyTrace,
2661) -> bool {
2662    let nodes = rewriter_reachable_subtree_ids(arena, subtree_root);
2663    let mut changed = false;
2664
2665    for node_id in nodes.into_iter().rev() {
2666        if !arena.is_reachable(node_id) {
2667            continue;
2668        }
2669        changed |=
2670            normalise_evaluator_node_to_fixpoint_without_ancestor_sync(arena, node_id, dirty_trace);
2671
2672        // Copy the child's new value one level up. Going deepest first means each parent is up
2673        // to date before its own evaluator runs, without touching wider ancestors every time
2674        // something folds. Anything above the subtree root is the caller's job: the worklist
2675        // defers it, the non-worklist path updates it directly.
2676        if node_id != subtree_root
2677            && let Some(parent_id) = arena.parent(node_id)
2678            && arena.is_reachable(parent_id)
2679        {
2680            arena.sync_payload_for_changed_child(parent_id, node_id);
2681        }
2682    }
2683
2684    changed
2685}
2686
2687/// Applies the post-rewrite evaluator hook from `node_id` up to the root.
2688///
2689/// Ordinary rules still obey the rewriter invariant that higher priority wins and, within a
2690/// priority, enclosing expressions are tried before descendants. Evaluators are the explicit
2691/// exception: after an ordinary rule changes a subtree, local full/partial evaluation is allowed to
2692/// fire immediately at that node and then at each ancestor because such simplification is always
2693/// preferable to scheduling another ordinary rule on the unevaluated expression.
2694fn normalise_evaluators_from_node_to_root(
2695    arena: &mut ExpressionArena,
2696    node_id: ExpressionNodeId,
2697    dirty_trace: &mut DirtyTrace,
2698) -> (ExpressionNodeId, bool) {
2699    // Ordinary rewrites can introduce fresh nested arithmetic whose children were not previously
2700    // scheduled. Normalise the replacement subtree bottom-up first, then walk upward so evaluator
2701    // simplification remains privileged without running ordinary rules to a fixpoint.
2702    let subtree_changed = normalise_evaluators_subtree_bottom_up(arena, node_id, dirty_trace);
2703    let mut came_from = node_id;
2704    let mut current = arena.parent(node_id);
2705    let mut highest_rewritten = node_id;
2706    let mut changed = subtree_changed;
2707
2708    while let Some(current_id) = current {
2709        if !arena.is_reachable(current_id) {
2710            break;
2711        }
2712
2713        let rewritten = if current_id == arena.root() {
2714            // Only deep-normalise the root child that contains the rewrite. Full-root selective
2715            // deep on every upward walk re-traverses every non-flat sibling and dominates CPU on
2716            // large models (lee-distance profile).
2717            normalise_root_evaluator_for_child(arena, came_from, dirty_trace)
2718        } else {
2719            normalise_evaluator_node_to_fixpoint(arena, current_id, dirty_trace)
2720        };
2721        if rewritten {
2722            highest_rewritten = current_id;
2723            changed = true;
2724        }
2725        came_from = current_id;
2726        current = arena.parent(current_id);
2727    }
2728
2729    (highest_rewritten, changed)
2730}
2731
2732/// Deep-normalises only the root constraint that `child_id` belongs to.
2733///
2734/// `child_id` must be a direct child of the arena root. Sibling constraints are left untouched.
2735///
2736/// The replacement is written to `child_id` rather than to the root. Rebuilding the root here
2737/// would clone every sibling constraint and re-import the whole model into the arena, which this
2738/// hook cannot afford: it runs after every rewrite.
2739fn normalise_root_evaluator_for_child(
2740    arena: &mut ExpressionArena,
2741    child_id: ExpressionNodeId,
2742    dirty_trace: &mut DirtyTrace,
2743) -> bool {
2744    let root_id = arena.root();
2745    if !arena.children(root_id).contains(&child_id) {
2746        return normalise_evaluator_node_to_fixpoint(arena, root_id, dirty_trace);
2747    }
2748
2749    let Some(replacement) = normalise_root_constraint_deep(arena.expression(child_id)) else {
2750        return false;
2751    };
2752
2753    dirty_trace.replacement_subtree_clears += 1;
2754    arena.replace_subtree(child_id, replacement);
2755    dirty_trace.record_rewrite("evaluator_normalisation_hook", false);
2756    sync_ancestor_payloads(arena, child_id);
2757    true
2758}
2759
2760fn normalise_evaluator_node_to_fixpoint(
2761    arena: &mut ExpressionArena,
2762    node_id: ExpressionNodeId,
2763    dirty_trace: &mut DirtyTrace,
2764) -> bool {
2765    let changed =
2766        normalise_evaluator_node_to_fixpoint_without_ancestor_sync(arena, node_id, dirty_trace);
2767
2768    if changed {
2769        sync_ancestor_payloads(arena, node_id);
2770    }
2771
2772    changed
2773}
2774
2775fn normalise_evaluator_node_to_fixpoint_without_ancestor_sync(
2776    arena: &mut ExpressionArena,
2777    node_id: ExpressionNodeId,
2778    dirty_trace: &mut DirtyTrace,
2779) -> bool {
2780    let mut changed = false;
2781
2782    while arena.is_reachable(node_id) {
2783        let expr = arena.expression(node_id);
2784        let Some(replacement) = normalise_evaluator_local(expr) else {
2785            break;
2786        };
2787
2788        dirty_trace.replacement_subtree_clears += 1;
2789        arena.replace_subtree(node_id, replacement);
2790        dirty_trace.record_rewrite("evaluator_normalisation_hook", false);
2791        changed = true;
2792    }
2793
2794    changed
2795}
2796
2797fn build_worklist_surfaces(
2798    submodel: &Model,
2799    root_arena: ExpressionArena,
2800) -> (Vec<RewriteSurface>, HashMap<Name, usize>) {
2801    let mut surfaces = vec![RewriteSurface::root(root_arena)];
2802    let mut value_letting_surfaces = HashMap::new();
2803
2804    let symbols = submodel.symbols();
2805    for (name, decl) in symbols.iter_local() {
2806        let letting_expr = decl.as_value_letting().map(|expr| expr.clone());
2807        if let Some(expr) = letting_expr {
2808            let surface = surfaces.len();
2809            surfaces.push(RewriteSurface::value_letting(name.clone(), expr));
2810            value_letting_surfaces.insert(name.clone(), surface);
2811        }
2812    }
2813
2814    (surfaces, value_letting_surfaces)
2815}
2816
2817fn value_letting_surface_name(kind: &RewriteSurfaceKind) -> Option<&Name> {
2818    match kind {
2819        RewriteSurfaceKind::Root => None,
2820        RewriteSurfaceKind::ValueLetting { name } => Some(name),
2821    }
2822}
2823
2824fn current_value_letting_expression(submodel: &Model, name: &Name) -> Option<Expr> {
2825    let declaration = {
2826        let symbols = submodel.symbols();
2827        symbols.lookup_local(name)
2828    }?;
2829    declaration.as_value_letting().map(|expr| expr.clone())
2830}
2831
2832fn sync_value_letting_surfaces(
2833    submodel: &Model,
2834    surfaces: &mut Vec<RewriteSurface>,
2835    value_letting_surfaces: &mut HashMap<Name, usize>,
2836    names: &[Name],
2837) -> Vec<usize> {
2838    let mut synced_surfaces = Vec::new();
2839    let mut seen = HashSet::new();
2840
2841    for name in names {
2842        if !seen.insert(name.clone()) {
2843            continue;
2844        }
2845
2846        if let Some(expr) = current_value_letting_expression(submodel, name) {
2847            if let Some(old_surface) = value_letting_surfaces.get(name).copied()
2848                && let Some(surface) = surfaces.get_mut(old_surface)
2849            {
2850                surface.active = false;
2851            }
2852            let new_surface = surfaces.len();
2853            surfaces.push(RewriteSurface::value_letting(name.clone(), expr));
2854            value_letting_surfaces.insert(name.clone(), new_surface);
2855            synced_surfaces.push(new_surface);
2856        } else if let Some(old_surface) = value_letting_surfaces.remove(name)
2857            && let Some(surface) = surfaces.get_mut(old_surface)
2858        {
2859            surface.active = false;
2860        }
2861    }
2862
2863    synced_surfaces
2864}
2865
2866fn write_worklist_surfaces_to_model(submodel: &mut Model, surfaces: &[RewriteSurface]) {
2867    let root = surfaces[0]
2868        .arena
2869        .expression(surfaces[0].arena.root())
2870        .clone();
2871    submodel.replace_root(root);
2872
2873    let mut wrote_value_letting = false;
2874    for surface in surfaces.iter().skip(1) {
2875        if !surface.active {
2876            continue;
2877        }
2878        let Some(name) = value_letting_surface_name(&surface.kind) else {
2879            continue;
2880        };
2881        let expression = surface.arena.expression(surface.arena.root()).clone();
2882        wrote_value_letting |=
2883            write_value_letting_expression_to_model_without_refresh(submodel, name, expression);
2884    }
2885    if wrote_value_letting {
2886        submodel.symbols_mut().refresh_local_binding_hashes();
2887    }
2888}
2889
2890fn move_worklist_surfaces_to_model(submodel: &mut Model, surfaces: Vec<RewriteSurface>) {
2891    let mut surfaces = surfaces.into_iter();
2892    let root = surfaces.next().expect("worklist must have a root surface");
2893    submodel.replace_root(root.arena.into_synced_root_expression());
2894
2895    let mut wrote_value_letting = false;
2896    for surface in surfaces {
2897        if !surface.active {
2898            continue;
2899        }
2900        let RewriteSurfaceKind::ValueLetting { name } = surface.kind else {
2901            continue;
2902        };
2903        wrote_value_letting |= write_value_letting_expression_to_model_without_refresh(
2904            submodel,
2905            &name,
2906            surface.arena.into_synced_root_expression(),
2907        );
2908    }
2909    if wrote_value_letting {
2910        submodel.symbols_mut().refresh_local_binding_hashes();
2911    }
2912}
2913
2914fn write_value_letting_surface_to_model(
2915    submodel: &mut Model,
2916    name: &Name,
2917    arena: &ExpressionArena,
2918) -> bool {
2919    let written = write_value_letting_surface_to_model_without_refresh(submodel, name, arena);
2920    if written {
2921        submodel.symbols_mut().refresh_local_binding_hashes();
2922    }
2923    written
2924}
2925
2926fn write_value_letting_surface_to_model_without_refresh(
2927    submodel: &mut Model,
2928    name: &Name,
2929    arena: &ExpressionArena,
2930) -> bool {
2931    write_value_letting_expression_to_model_without_refresh(
2932        submodel,
2933        name,
2934        arena.expression_from(arena.root()),
2935    )
2936}
2937
2938fn write_value_letting_expression_to_model_without_refresh(
2939    submodel: &mut Model,
2940    name: &Name,
2941    expression: Expr,
2942) -> bool {
2943    let declaration = {
2944        let symbols = submodel.symbols();
2945        symbols.lookup_local(name)
2946    };
2947    let Some(mut declaration) = declaration else {
2948        return false;
2949    };
2950    {
2951        let Some(mut letting) = declaration.as_value_letting_mut() else {
2952            return false;
2953        };
2954
2955        *letting = expression;
2956    }
2957    true
2958}
2959
2960fn enqueue_worklist_nodes_referencing_names(
2961    scheduler: &mut WorklistScheduler,
2962    surfaces: &[RewriteSurface],
2963    names: &[Name],
2964    dirty_trace: &mut DirtyTrace,
2965) {
2966    if names.is_empty() {
2967        return;
2968    }
2969
2970    // A `BTreeSet` rather than a hash set: the scheduler breaks depth ties by enqueue order, so
2971    // the order these are inserted in has to be the same on every run.
2972    let mut affected_nodes = BTreeSet::new();
2973    for name in names {
2974        let Some(reference_nodes) = scheduler.reference_nodes_by_name.get(name) else {
2975            continue;
2976        };
2977        for &(surface_index, reference_node) in reference_nodes {
2978            let Some(surface) = surfaces.get(surface_index) else {
2979                continue;
2980            };
2981            if !surface.active
2982                || !surface.arena.is_reachable(reference_node)
2983                || !expression_directly_references_any(
2984                    surface.arena.expression(reference_node),
2985                    std::slice::from_ref(name),
2986                )
2987            {
2988                continue;
2989            }
2990
2991            let mut current = Some(reference_node);
2992            while let Some(node_id) = current {
2993                affected_nodes.insert((surface_index, node_id));
2994                current = surface.arena.parent(node_id);
2995            }
2996        }
2997    }
2998
2999    for (surface_index, node_id) in affected_nodes {
3000        let surface = &surfaces[surface_index];
3001        scheduler.enqueue_node_at_level(
3002            &surface.arena,
3003            surface_index,
3004            node_id,
3005            0,
3006            ScheduledMode::CheckNode,
3007            Some(dirty_trace),
3008        );
3009    }
3010}
3011
3012#[cfg(test)]
3013fn collect_worklist_nodes_referencing_names(
3014    arena: &ExpressionArena,
3015    node_id: ExpressionNodeId,
3016    names: &[Name],
3017    affected_nodes: &mut Vec<ExpressionNodeId>,
3018) -> bool {
3019    if !arena.is_reachable(node_id) {
3020        return false;
3021    }
3022
3023    let mut references_changed_name =
3024        expression_directly_references_any(arena.expression(node_id), names);
3025    if !matches!(arena.expression(node_id), Expr::Comprehension(_, _)) {
3026        for &child_id in arena.children(node_id) {
3027            references_changed_name |=
3028                collect_worklist_nodes_referencing_names(arena, child_id, names, affected_nodes);
3029        }
3030    }
3031
3032    if references_changed_name {
3033        affected_nodes.push(node_id);
3034    }
3035    references_changed_name
3036}
3037
3038fn expression_directly_references_any(expr: &Expr, names: &[Name]) -> bool {
3039    let Expr::Atomic(_, Atom::Reference(reference)) = expr else {
3040        return false;
3041    };
3042    names.iter().any(|name| &*reference.name() == name)
3043}
3044
3045fn increment_counter(counter: &mut Option<usize>) {
3046    *counter = Some(counter.unwrap_or(0) + 1);
3047}
3048
3049/// Returns expression node ids in rewriter preorder, without entering comprehensions.
3050fn rewriter_preorder_ids(arena: &ExpressionArena) -> Vec<ExpressionNodeId> {
3051    rewriter_reachable_subtree_ids(arena, arena.root())
3052}
3053
3054/// Returns reachable expression node ids under `node_id` in rewriter preorder.
3055///
3056/// The rewrite pass treats comprehensions as atomic here; comprehension expansion is responsible
3057/// for rewriting their bodies in the right scoped context.
3058fn rewriter_reachable_subtree_ids(
3059    arena: &ExpressionArena,
3060    node_id: ExpressionNodeId,
3061) -> Vec<ExpressionNodeId> {
3062    fn collect(
3063        arena: &ExpressionArena,
3064        node_id: ExpressionNodeId,
3065        nodes: &mut Vec<ExpressionNodeId>,
3066    ) {
3067        if !arena.is_reachable(node_id) {
3068            return;
3069        }
3070        nodes.push(node_id);
3071        if matches!(arena.expression(node_id), Expr::Comprehension(_, _)) {
3072            return;
3073        }
3074
3075        for child in arena.children(node_id) {
3076            collect(arena, *child, nodes);
3077        }
3078    }
3079
3080    let mut nodes = Vec::new();
3081    collect(arena, node_id, &mut nodes);
3082    nodes
3083}
3084
3085/// Replaces the focused expression and updates ancestor payloads to match it.
3086fn replace_focus_and_sync_ancestors(
3087    arena: &mut ExpressionArena,
3088    node_id: ExpressionNodeId,
3089    new_focus: Expr,
3090) {
3091    arena.replace_subtree(node_id, new_focus);
3092    sync_ancestor_payloads(arena, node_id);
3093}
3094
3095fn sync_ancestor_payloads(arena: &mut ExpressionArena, node_id: ExpressionNodeId) {
3096    let mut child_id = node_id;
3097    let mut ancestor = arena.parent(node_id);
3098    while let Some(ancestor_id) = ancestor {
3099        // Same-arity parent update: clone only the changed child into the parent payload.
3100        arena.sync_payload_for_changed_child(ancestor_id, child_id);
3101        child_id = ancestor_id;
3102        ancestor = arena.parent(ancestor_id);
3103    }
3104}
3105
3106fn take_model_root(model: &mut Model) -> Expr {
3107    model.replace_root(Expr::Root(Metadata::new(), Vec::new()))
3108}
3109
3110fn rule_is_universal(rule_data: &RuleData<'_>) -> bool {
3111    rule_data.rule.prefilters.is_none()
3112}
3113
3114fn rule_matches_self_discriminant(rule_data: &RuleData<'_>, expr_discriminant: usize) -> bool {
3115    rule_data
3116        .rule
3117        .prefilters
3118        .is_some_and(|prefilters| {
3119            prefilters.iter().any(|prefilter| {
3120                matches!(prefilter, RulePrefilter::Variant(discriminant) if *discriminant == expr_discriminant)
3121            })
3122        })
3123}
3124
3125fn rule_matches_specific_prefilter(
3126    rule_data: &RuleData<'_>,
3127    expr: &Expr,
3128    arena_node: Option<(&ExpressionArena, ExpressionNodeId)>,
3129) -> bool {
3130    if rule_is_universal(rule_data) {
3131        return false;
3132    }
3133
3134    let expr_discriminant = discriminant_from_value(expr);
3135    rule_data.rule.prefilters.is_some_and(|prefilters| {
3136        prefilters.iter().any(|prefilter| match prefilter {
3137            RulePrefilter::Variant(discriminant) => *discriminant == expr_discriminant,
3138            RulePrefilter::Child { child } => {
3139                has_direct_child_discriminant(expr, arena_node, *child)
3140            }
3141            RulePrefilter::VariantChild { variant, child } => {
3142                *variant == expr_discriminant
3143                    && has_direct_child_discriminant(expr, arena_node, *child)
3144            }
3145            RulePrefilter::Atom(atom_kind) => expr_atom_kind(expr) == Some(*atom_kind),
3146            RulePrefilter::ChildAtom(atom_kind) => {
3147                expr_has_direct_child_atom_kind(expr, *atom_kind)
3148            }
3149        })
3150    })
3151}
3152
3153fn has_direct_child_discriminant(
3154    expr: &Expr,
3155    arena_node: Option<(&ExpressionArena, ExpressionNodeId)>,
3156    target_discriminant: usize,
3157) -> bool {
3158    if let Some((arena, node_id)) = arena_node {
3159        arena.has_direct_child_discriminant(node_id, target_discriminant)
3160    } else {
3161        expr_has_direct_child_discriminant(expr, &[target_discriminant])
3162    }
3163}
3164
3165fn expr_atom_kind(expr: &Expr) -> Option<AtomKind> {
3166    match expr {
3167        Expr::Atomic(_, Atom::Literal(_)) => Some(AtomKind::Literal),
3168        Expr::Atomic(_, Atom::Reference(_)) => Some(AtomKind::Reference),
3169        _ => None,
3170    }
3171}
3172
3173/// Returns whether `expr` has an immediate `Atomic` child of this kind.
3174///
3175/// Direct children are walked rather than read off the arena's cached child discriminants, which
3176/// record `Expression` variants only and so cannot tell a reference from a literal.
3177fn expr_has_direct_child_atom_kind(expr: &Expr, target: AtomKind) -> bool {
3178    let mut found = false;
3179    expr.for_each_expr_child(&mut |child| {
3180        if !found && expr_atom_kind(child) == Some(target) {
3181            found = true;
3182        }
3183    });
3184    found
3185}
3186
3187fn expr_has_direct_child_discriminant(expr: &Expr, target_discriminants: &[usize]) -> bool {
3188    if target_discriminants.is_empty() {
3189        return false;
3190    }
3191
3192    let mut found = false;
3193    expr.for_each_expr_child(&mut |child| {
3194        if !found && target_discriminants.contains(&discriminant_from_value(child)) {
3195            found = true;
3196        }
3197    });
3198    found
3199}
3200
3201fn csv_escape(field: &str) -> String {
3202    if field.contains([',', '"', '\n', '\r']) {
3203        format!("\"{}\"", field.replace('"', "\"\""))
3204    } else {
3205        field.to_string()
3206    }
3207}
3208
3209#[cfg(test)]
3210mod tests {
3211    use crate::ast::comprehension::ComprehensionBuilder;
3212    use crate::ast::{Atom, DeclarationPtr, ExpressionArena, Literal, Moo, SymbolTablePtr};
3213    use crate::matrix_expr;
3214
3215    use super::*;
3216
3217    fn int_lit(value: i32) -> Expr {
3218        Expr::Atomic(Metadata::new(), Atom::Literal(Literal::Int(value)))
3219    }
3220
3221    fn bool_lit(value: bool) -> Expr {
3222        Expr::Atomic(Metadata::new(), Atom::Literal(Literal::Bool(value)))
3223    }
3224
3225    fn root(exprs: Vec<Expr>) -> Expr {
3226        Expr::Root(Metadata::new(), exprs)
3227    }
3228
3229    fn comprehension(return_expression: Expr, guards: Vec<Expr>) -> Expr {
3230        let mut builder = ComprehensionBuilder::new(SymbolTablePtr::new());
3231        for guard in guards {
3232            builder = builder.guard(guard);
3233        }
3234        Expr::Comprehension(
3235            Metadata::new(),
3236            Moo::new(builder.with_return_value(return_expression)),
3237        )
3238    }
3239
3240    #[test]
3241    fn dirty_trace_filename_replaces_path_separators_and_module_delimiters() {
3242        assert_eq!(
3243            sanitize_dirty_trace_filename("generated_tests::savilerow/quasiGroup6"),
3244            "generated_tests--savilerow-quasiGroup6"
3245        );
3246    }
3247
3248    #[test]
3249    fn dirty_trace_bare_path_is_directory_destination() {
3250        assert_eq!(
3251            dirty_trace_destination_from_env_value("trace-dir".into()),
3252            DirtyTraceDestination::Directory(PathBuf::from("trace-dir"))
3253        );
3254    }
3255
3256    #[test]
3257    fn dirty_trace_txt_path_is_file_destination() {
3258        assert_eq!(
3259            dirty_trace_destination_from_env_value("trace.txt".into()),
3260            DirtyTraceDestination::File(PathBuf::from("trace.txt"))
3261        );
3262    }
3263
3264    fn reference_expr(name: &Name) -> Expr {
3265        use crate::ast::{Domain, Range, Reference};
3266
3267        Expr::Atomic(
3268            Metadata::new(),
3269            Atom::Reference(Reference::new(DeclarationPtr::new_find(
3270                name.clone(),
3271                Domain::int(vec![Range::Bounded(1, 3)]),
3272            ))),
3273        )
3274    }
3275
3276    fn test_rule_set_applies(_: &crate::settings::SolverFamily) -> bool {
3277        true
3278    }
3279
3280    fn never_apply_test_rule(
3281        _: &Expr,
3282        _: &crate::ast::SymbolTable,
3283    ) -> crate::rule_engine::ApplicationResult {
3284        Err(crate::rule_engine::ApplicationError::RuleNotApplicable)
3285    }
3286
3287    static TEST_RULE_SET: RuleSet<'static> =
3288        RuleSet::new("test-rule-set", &[], test_rule_set_applies);
3289    static TEST_RULE: crate::rule_engine::Rule<'static> = crate::rule_engine::Rule::new(
3290        "never-apply-test-rule",
3291        never_apply_test_rule,
3292        &[("test-rule-set", 1)],
3293    );
3294    static TEST_NO_TARGET_RULE: crate::rule_engine::Rule<'static> = crate::rule_engine::Rule {
3295        name: "no-target-test-rule",
3296        application: never_apply_test_rule,
3297        rule_sets: &[("test-rule-set", 1)],
3298        prefilters: Some(&[]),
3299        failure_invalidation: crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3300    };
3301    static TEST_SYMBOLS_ONLY_RULE: crate::rule_engine::Rule<'static> = crate::rule_engine::Rule {
3302        name: "symbols-only-test-rule",
3303        application: never_apply_test_rule,
3304        rule_sets: &[("test-rule-set", 1)],
3305        prefilters: None,
3306        failure_invalidation: crate::rule_engine::RuleFailureInvalidation::SymbolsOnly,
3307    };
3308    fn test_rule_groups_at_priorities(priorities: &[u16]) -> Vec<RuleGroup<'static>> {
3309        priorities
3310            .iter()
3311            .map(|&priority| {
3312                RuleGroup::new(
3313                    priority,
3314                    vec![crate::rule_engine::RuleData {
3315                        rule: &TEST_RULE,
3316                        priority,
3317                        rule_set: &TEST_RULE_SET,
3318                    }],
3319                )
3320            })
3321            .collect()
3322    }
3323
3324    fn test_rule_groups() -> Vec<RuleGroup<'static>> {
3325        test_rule_groups_at_priorities(&[1])
3326    }
3327
3328    fn test_rule_groups_with_two_levels() -> Vec<RuleGroup<'static>> {
3329        test_rule_groups_at_priorities(&[1, 2])
3330    }
3331
3332    fn test_rule_groups_with_no_candidate_middle_level() -> Vec<RuleGroup<'static>> {
3333        vec![
3334            RuleGroup::new(
3335                1,
3336                vec![crate::rule_engine::RuleData {
3337                    rule: &TEST_RULE,
3338                    priority: 1,
3339                    rule_set: &TEST_RULE_SET,
3340                }],
3341            ),
3342            RuleGroup::new(
3343                2,
3344                vec![crate::rule_engine::RuleData {
3345                    rule: &TEST_NO_TARGET_RULE,
3346                    priority: 2,
3347                    rule_set: &TEST_RULE_SET,
3348                }],
3349            ),
3350            RuleGroup::new(
3351                3,
3352                vec![crate::rule_engine::RuleData {
3353                    rule: &TEST_RULE,
3354                    priority: 3,
3355                    rule_set: &TEST_RULE_SET,
3356                }],
3357            ),
3358        ]
3359    }
3360
3361    fn test_rule_groups_targeting_expr(expr: &Expr) -> Vec<RuleGroup<'static>> {
3362        let discriminant = discriminant_from_value(expr);
3363        let mut rules_by_discriminant = Vec::new();
3364        rules_by_discriminant.resize_with(discriminant + 1, || None);
3365        rules_by_discriminant[discriminant] = Some(vec![crate::rule_engine::RuleData {
3366            rule: &TEST_RULE,
3367            priority: 1,
3368            rule_set: &TEST_RULE_SET,
3369        }]);
3370
3371        vec![RuleGroup {
3372            priority: 1,
3373            rules: vec![crate::rule_engine::RuleData {
3374                rule: &TEST_RULE,
3375                priority: 1,
3376                rule_set: &TEST_RULE_SET,
3377            }],
3378            rules_by_discriminant,
3379            universal_rules: Vec::new(),
3380            has_non_discriminant_filters: false,
3381        }]
3382    }
3383
3384    #[test]
3385    fn rule_group_child_filter_matches_immediate_child_kind() {
3386        let bubble_discriminant = discriminant_from_value(&Expr::Bubble(
3387            Metadata::new(),
3388            Moo::new(int_lit(0)),
3389            Moo::new(bool_lit(true)),
3390        ));
3391        let child_prefilters: &'static [RulePrefilter] = Box::leak(
3392            vec![RulePrefilter::Child {
3393                child: bubble_discriminant,
3394            }]
3395            .into_boxed_slice(),
3396        );
3397        let child_bubble_rule: &'static crate::rule_engine::Rule<'static> =
3398            Box::leak(Box::new(crate::rule_engine::Rule {
3399                name: "child-bubble-test-rule",
3400                application: never_apply_test_rule,
3401                rule_sets: &[("test-rule-set", 1)],
3402                prefilters: Some(child_prefilters),
3403                failure_invalidation:
3404                    crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3405            }));
3406        let rule_group = RuleGroup::new(
3407            1,
3408            vec![crate::rule_engine::RuleData {
3409                rule: child_bubble_rule,
3410                priority: 1,
3411                rule_set: &TEST_RULE_SET,
3412            }],
3413        );
3414        let config = RewriteConfig::optimised();
3415
3416        let expr_with_bubble_child = Expr::Eq(
3417            Metadata::new(),
3418            Moo::new(Expr::Bubble(
3419                Metadata::new(),
3420                Moo::new(int_lit(1)),
3421                Moo::new(bool_lit(true)),
3422            )),
3423            Moo::new(int_lit(2)),
3424        );
3425        let expr_without_bubble_child =
3426            Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
3427
3428        assert!(rule_group.has_candidates(config, &expr_with_bubble_child));
3429        assert_eq!(
3430            rule_group
3431                .candidates(config, &expr_with_bubble_child)
3432                .map(|rule_data| rule_data.rule.name)
3433                .collect_vec(),
3434            vec!["child-bubble-test-rule"]
3435        );
3436        assert!(!rule_group.has_candidates(config, &expr_without_bubble_child));
3437    }
3438
3439    #[test]
3440    fn rule_group_atom_filter_matches_atomic_reference() {
3441        let atom_reference_rule: &'static crate::rule_engine::Rule<'static> =
3442            Box::leak(Box::new(crate::rule_engine::Rule {
3443                name: "atom-reference-test-rule",
3444                application: never_apply_test_rule,
3445                rule_sets: &[("test-rule-set", 1)],
3446                prefilters: Some(&[RulePrefilter::Atom(AtomKind::Reference)]),
3447                failure_invalidation:
3448                    crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3449            }));
3450        let rule_group = RuleGroup::new(
3451            1,
3452            vec![crate::rule_engine::RuleData {
3453                rule: atom_reference_rule,
3454                priority: 1,
3455                rule_set: &TEST_RULE_SET,
3456            }],
3457        );
3458        let config = RewriteConfig::optimised();
3459        let reference = reference_expr(&Name::user("x"));
3460        let literal = int_lit(1);
3461        let composite = Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
3462
3463        assert!(rule_group.has_candidates(config, &reference));
3464        assert_eq!(
3465            rule_group
3466                .candidates(config, &reference)
3467                .map(|rule_data| rule_data.rule.name)
3468                .collect_vec(),
3469            vec!["atom-reference-test-rule"]
3470        );
3471        assert!(!rule_group.has_candidates(config, &literal));
3472        assert!(!rule_group.has_candidates(config, &composite));
3473    }
3474
3475    #[test]
3476    fn rule_group_child_atom_filter_matches_direct_atomic_reference_child() {
3477        let child_atom_rule: &'static crate::rule_engine::Rule<'static> =
3478            Box::leak(Box::new(crate::rule_engine::Rule {
3479                name: "child-atom-reference-test-rule",
3480                application: never_apply_test_rule,
3481                rule_sets: &[("test-rule-set", 1)],
3482                prefilters: Some(&[RulePrefilter::ChildAtom(AtomKind::Reference)]),
3483                failure_invalidation:
3484                    crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3485            }));
3486        let rule_group = RuleGroup::new(
3487            1,
3488            vec![crate::rule_engine::RuleData {
3489                rule: child_atom_rule,
3490                priority: 1,
3491                rule_set: &TEST_RULE_SET,
3492            }],
3493        );
3494        let config = RewriteConfig::optimised();
3495        let with_reference_child = Expr::Eq(
3496            Metadata::new(),
3497            Moo::new(reference_expr(&Name::user("x"))),
3498            Moo::new(int_lit(2)),
3499        );
3500        // Literal children have the same `Atomic` variant, so only the atom kind separates them.
3501        let with_literal_children =
3502            Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
3503        // The reference itself is not a match: the filter is about children.
3504        let bare_reference = reference_expr(&Name::user("x"));
3505
3506        assert!(rule_group.has_candidates(config, &with_reference_child));
3507        assert_eq!(
3508            rule_group
3509                .candidates(config, &with_reference_child)
3510                .map(|rule_data| rule_data.rule.name)
3511                .collect_vec(),
3512            vec!["child-atom-reference-test-rule"]
3513        );
3514        assert!(!rule_group.has_candidates(config, &with_literal_children));
3515        assert!(!rule_group.has_candidates(config, &bare_reference));
3516    }
3517
3518    #[test]
3519    fn rule_group_includes_universal_rules_in_variant_buckets() {
3520        let lex_discriminant = discriminant_from_value(&Expr::LexLt(
3521            Metadata::new(),
3522            Moo::new(int_lit(1)),
3523            Moo::new(int_lit(2)),
3524        ));
3525        let lex_prefilters: &'static [RulePrefilter] =
3526            Box::leak(Box::new([RulePrefilter::Variant(lex_discriminant)]));
3527        let variant_rule: &'static crate::rule_engine::Rule<'static> =
3528            Box::leak(Box::new(crate::rule_engine::Rule {
3529                name: "variant-specific-test-rule",
3530                application: never_apply_test_rule,
3531                rule_sets: &[("test-rule-set", 1)],
3532                prefilters: Some(lex_prefilters),
3533                failure_invalidation:
3534                    crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3535            }));
3536        let universal_rule: &'static crate::rule_engine::Rule<'static> =
3537            Box::leak(Box::new(crate::rule_engine::Rule {
3538                name: "universal-test-rule",
3539                application: never_apply_test_rule,
3540                rule_sets: &[("test-rule-set", 1)],
3541                prefilters: None,
3542                failure_invalidation:
3543                    crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3544            }));
3545        let rule_group = RuleGroup::new(
3546            1,
3547            vec![
3548                crate::rule_engine::RuleData {
3549                    rule: variant_rule,
3550                    priority: 1,
3551                    rule_set: &TEST_RULE_SET,
3552                },
3553                crate::rule_engine::RuleData {
3554                    rule: universal_rule,
3555                    priority: 1,
3556                    rule_set: &TEST_RULE_SET,
3557                },
3558            ],
3559        );
3560        let config = RewriteConfig::optimised();
3561        let lex = Expr::LexLt(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
3562
3563        assert!(rule_group.has_candidates(config, &lex));
3564        assert_eq!(
3565            rule_group
3566                .candidates(config, &lex)
3567                .map(|rule_data| rule_data.rule.name)
3568                .collect_vec(),
3569            vec!["variant-specific-test-rule", "universal-test-rule"]
3570        );
3571    }
3572
3573    #[test]
3574    fn rule_group_variant_child_filter_does_not_cross_product_alternatives() {
3575        let and_discriminant = discriminant_from_value(&Expr::And(
3576            Metadata::new(),
3577            Moo::new(matrix_expr![bool_lit(true)]),
3578        ));
3579        let or_discriminant = discriminant_from_value(&Expr::Or(
3580            Metadata::new(),
3581            Moo::new(matrix_expr![bool_lit(true)]),
3582        ));
3583        let comprehension_discriminant =
3584            discriminant_from_value(&comprehension(bool_lit(true), vec![]));
3585        let atomic_discriminant = discriminant_from_value(&bool_lit(true));
3586        let paired_prefilters: &'static [RulePrefilter] = Box::leak(
3587            vec![
3588                RulePrefilter::VariantChild {
3589                    variant: and_discriminant,
3590                    child: comprehension_discriminant,
3591                },
3592                RulePrefilter::VariantChild {
3593                    variant: or_discriminant,
3594                    child: atomic_discriminant,
3595                },
3596            ]
3597            .into_boxed_slice(),
3598        );
3599        let paired_rule: &'static crate::rule_engine::Rule<'static> =
3600            Box::leak(Box::new(crate::rule_engine::Rule {
3601                name: "paired-prefilter-test-rule",
3602                application: never_apply_test_rule,
3603                rule_sets: &[("test-rule-set", 1)],
3604                prefilters: Some(paired_prefilters),
3605                failure_invalidation:
3606                    crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
3607            }));
3608        let rule_group = RuleGroup::new(
3609            1,
3610            vec![crate::rule_engine::RuleData {
3611                rule: paired_rule,
3612                priority: 1,
3613                rule_set: &TEST_RULE_SET,
3614            }],
3615        );
3616        let config = RewriteConfig::optimised();
3617        let and_with_comprehension = Expr::And(
3618            Metadata::new(),
3619            Moo::new(comprehension(bool_lit(true), vec![])),
3620        );
3621        let or_with_atomic = Expr::Or(Metadata::new(), Moo::new(bool_lit(true)));
3622        let and_with_atomic = Expr::And(Metadata::new(), Moo::new(bool_lit(true)));
3623        let or_with_comprehension = Expr::Or(
3624            Metadata::new(),
3625            Moo::new(comprehension(bool_lit(true), vec![])),
3626        );
3627
3628        assert!(rule_group.has_candidates(config, &and_with_comprehension));
3629        assert!(rule_group.has_candidates(config, &or_with_atomic));
3630        assert!(!rule_group.has_candidates(config, &and_with_atomic));
3631        assert!(!rule_group.has_candidates(config, &or_with_comprehension));
3632    }
3633
3634    #[test]
3635    fn rewriter_subtree_preorder_does_not_enter_comprehensions() {
3636        let tree = root(vec![
3637            comprehension(int_lit(1), vec![int_lit(2)]),
3638            int_lit(3),
3639        ]);
3640        let arena = ExpressionArena::from_root(tree);
3641        let root_ids = rewriter_preorder_ids(&arena);
3642        let comp_id = arena.children(arena.root())[0];
3643
3644        assert_eq!(root_ids.len(), 3);
3645        assert_eq!(
3646            rewriter_reachable_subtree_ids(&arena, comp_id),
3647            vec![comp_id]
3648        );
3649    }
3650
3651    #[test]
3652    fn worklist_ancestor_enqueue_checks_enclosing_nodes_first() {
3653        let tree = root(vec![Expr::Eq(
3654            Metadata::new(),
3655            Moo::new(int_lit(1)),
3656            Moo::new(int_lit(2)),
3657        )]);
3658        let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
3659        let arena = &surfaces[0].arena;
3660        let ids = rewriter_preorder_ids(arena);
3661        let root_id = ids[0];
3662        let eq_id = ids[1];
3663        let left_leaf_id = ids[2];
3664
3665        let rule_groups = test_rule_groups();
3666        let mut scheduler = WorklistScheduler::empty(&rule_groups);
3667        let mut dirty_trace = DirtyTrace::default();
3668
3669        scheduler.enqueue_node_and_ancestors(arena, 0, left_leaf_id, &mut dirty_trace);
3670
3671        assert_eq!(
3672            scheduler.pop_next(
3673                &surfaces,
3674                &rule_groups,
3675                RewriteConfig::optimised(),
3676                &mut dirty_trace
3677            ),
3678            Some((0, 0, root_id, ScheduledMode::CheckNode))
3679        );
3680        assert_eq!(
3681            scheduler.pop_next(
3682                &surfaces,
3683                &rule_groups,
3684                RewriteConfig::optimised(),
3685                &mut dirty_trace
3686            ),
3687            Some((0, 0, eq_id, ScheduledMode::CheckNode))
3688        );
3689        assert_eq!(
3690            scheduler.pop_next(
3691                &surfaces,
3692                &rule_groups,
3693                RewriteConfig::optimised(),
3694                &mut dirty_trace
3695            ),
3696            Some((0, 0, left_leaf_id, ScheduledMode::CheckNode))
3697        );
3698        assert_eq!(
3699            scheduler.pop_next(
3700                &surfaces,
3701                &rule_groups,
3702                RewriteConfig::optimised(),
3703                &mut dirty_trace
3704            ),
3705            None
3706        );
3707    }
3708
3709    #[test]
3710    fn worklist_level_order_prefers_ancestors_over_earlier_descendants() {
3711        let tree = root(vec![Expr::Eq(
3712            Metadata::new(),
3713            Moo::new(int_lit(1)),
3714            Moo::new(int_lit(2)),
3715        )]);
3716        let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
3717        let arena = &surfaces[0].arena;
3718        let ids = rewriter_preorder_ids(arena);
3719        let eq_id = ids[1];
3720        let left_leaf_id = ids[2];
3721
3722        let rule_groups = test_rule_groups();
3723        let mut scheduler = WorklistScheduler::empty(&rule_groups);
3724        let mut dirty_trace = DirtyTrace::default();
3725
3726        scheduler.enqueue_node_at_level(
3727            arena,
3728            0,
3729            left_leaf_id,
3730            0,
3731            ScheduledMode::CheckNode,
3732            Some(&mut dirty_trace),
3733        );
3734        scheduler.enqueue_node_at_level(
3735            arena,
3736            0,
3737            eq_id,
3738            0,
3739            ScheduledMode::CheckNode,
3740            Some(&mut dirty_trace),
3741        );
3742
3743        assert_eq!(
3744            scheduler.pop_next(
3745                &surfaces,
3746                &rule_groups,
3747                RewriteConfig::optimised(),
3748                &mut dirty_trace
3749            ),
3750            Some((0, 0, eq_id, ScheduledMode::CheckNode))
3751        );
3752        assert_eq!(
3753            scheduler.pop_next(
3754                &surfaces,
3755                &rule_groups,
3756                RewriteConfig::optimised(),
3757                &mut dirty_trace
3758            ),
3759            Some((0, 0, left_leaf_id, ScheduledMode::CheckNode))
3760        );
3761    }
3762
3763    #[test]
3764    fn worklist_subtree_descends_lazily_in_breadth_first_order() {
3765        let tree = root(vec![
3766            Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2))),
3767            int_lit(3),
3768        ]);
3769        let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
3770        let arena = &surfaces[0].arena;
3771        let ids = rewriter_preorder_ids(arena);
3772        let root_id = ids[0];
3773        let eq_id = ids[1];
3774        let left_leaf_id = ids[2];
3775        let right_leaf_id = ids[3];
3776        let root_sibling_id = ids[4];
3777
3778        let rule_groups = test_rule_groups_with_two_levels();
3779        let mut scheduler =
3780            WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
3781        let mut dirty_trace = DirtyTrace::default();
3782
3783        let root_work = scheduler.pop_next(
3784            &surfaces,
3785            &rule_groups,
3786            RewriteConfig::optimised(),
3787            &mut dirty_trace,
3788        );
3789        assert_eq!(
3790            root_work,
3791            Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
3792        );
3793        assert_eq!(
3794            scheduler.pop_next(
3795                &surfaces,
3796                &rule_groups,
3797                RewriteConfig::optimised(),
3798                &mut dirty_trace
3799            ),
3800            None
3801        );
3802
3803        scheduler.enqueue_after_no_rewrite(
3804            WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
3805            root_id,
3806            0,
3807            1,
3808            ScheduledMode::TraverseSubtreeRoot,
3809            Some(&mut dirty_trace),
3810        );
3811        let eq_work = scheduler.pop_next(
3812            &surfaces,
3813            &rule_groups,
3814            RewriteConfig::optimised(),
3815            &mut dirty_trace,
3816        );
3817        assert_eq!(
3818            eq_work,
3819            Some((0, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
3820        );
3821        scheduler.enqueue_after_no_rewrite(
3822            WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
3823            eq_id,
3824            0,
3825            1,
3826            ScheduledMode::TraverseSubtreeDescendant,
3827            Some(&mut dirty_trace),
3828        );
3829        assert_eq!(
3830            scheduler.pop_next(
3831                &surfaces,
3832                &rule_groups,
3833                RewriteConfig::optimised(),
3834                &mut dirty_trace
3835            ),
3836            Some((
3837                0,
3838                0,
3839                root_sibling_id,
3840                ScheduledMode::TraverseSubtreeDescendant
3841            ))
3842        );
3843        assert_eq!(
3844            scheduler.pop_next(
3845                &surfaces,
3846                &rule_groups,
3847                RewriteConfig::optimised(),
3848                &mut dirty_trace
3849            ),
3850            Some((0, 0, left_leaf_id, ScheduledMode::TraverseSubtreeDescendant))
3851        );
3852        assert_eq!(
3853            scheduler.pop_next(
3854                &surfaces,
3855                &rule_groups,
3856                RewriteConfig::optimised(),
3857                &mut dirty_trace
3858            ),
3859            Some((
3860                0,
3861                0,
3862                right_leaf_id,
3863                ScheduledMode::TraverseSubtreeDescendant
3864            ))
3865        );
3866        assert_eq!(
3867            scheduler.pop_next(
3868                &surfaces,
3869                &rule_groups,
3870                RewriteConfig::optimised(),
3871                &mut dirty_trace
3872            ),
3873            Some((1, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
3874        );
3875        assert_eq!(
3876            scheduler.pop_next(
3877                &surfaces,
3878                &rule_groups,
3879                RewriteConfig::optimised(),
3880                &mut dirty_trace
3881            ),
3882            None
3883        );
3884
3885        scheduler.enqueue_after_no_rewrite(
3886            WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
3887            root_id,
3888            1,
3889            2,
3890            ScheduledMode::TraverseSubtreeRoot,
3891            Some(&mut dirty_trace),
3892        );
3893        assert_eq!(
3894            scheduler.pop_next(
3895                &surfaces,
3896                &rule_groups,
3897                RewriteConfig::optimised(),
3898                &mut dirty_trace
3899            ),
3900            Some((1, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
3901        );
3902    }
3903
3904    #[test]
3905    fn worklist_refreshes_stale_subtree_carrier_after_descendant_rewrite() {
3906        let tree = root(vec![
3907            Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2))),
3908            int_lit(3),
3909        ]);
3910        let mut surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
3911        let ids = rewriter_preorder_ids(&surfaces[0].arena);
3912        let root_id = ids[0];
3913        let eq_id = ids[1];
3914        let root_sibling_id = ids[4];
3915
3916        let rule_groups = test_rule_groups_with_two_levels();
3917        let mut scheduler =
3918            WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
3919        let mut dirty_trace = DirtyTrace::default();
3920
3921        assert_eq!(
3922            scheduler.pop_next(
3923                &surfaces,
3924                &rule_groups,
3925                RewriteConfig::optimised(),
3926                &mut dirty_trace
3927            ),
3928            Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
3929        );
3930        scheduler.enqueue_after_no_rewrite(
3931            WorklistSchedulingContext::new(
3932                &surfaces[0].arena,
3933                0,
3934                &rule_groups,
3935                RewriteConfig::optimised(),
3936            ),
3937            root_id,
3938            0,
3939            1,
3940            ScheduledMode::TraverseSubtreeRoot,
3941            Some(&mut dirty_trace),
3942        );
3943
3944        assert_eq!(
3945            scheduler.pop_next(
3946                &surfaces,
3947                &rule_groups,
3948                RewriteConfig::optimised(),
3949                &mut dirty_trace
3950            ),
3951            Some((0, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
3952        );
3953        replace_focus_and_sync_ancestors(&mut surfaces[0].arena, eq_id, int_lit(10));
3954        enqueue_worklist_rewrite_impact(
3955            &mut scheduler,
3956            &surfaces[0].arena,
3957            0,
3958            eq_id,
3959            &mut dirty_trace,
3960        );
3961
3962        let mut found_refreshed_root = false;
3963        for _ in 0..32 {
3964            let Some((level, surface, node_id, mode)) = scheduler.pop_next(
3965                &surfaces,
3966                &rule_groups,
3967                RewriteConfig::optimised(),
3968                &mut dirty_trace,
3969            ) else {
3970                break;
3971            };
3972            if (level, surface, node_id, mode)
3973                == (1, 0, root_id, ScheduledMode::TraverseSubtreeRoot)
3974            {
3975                found_refreshed_root = true;
3976                scheduler.enqueue_after_no_rewrite(
3977                    WorklistSchedulingContext::new(
3978                        &surfaces[0].arena,
3979                        0,
3980                        &rule_groups,
3981                        RewriteConfig::optimised(),
3982                    ),
3983                    root_id,
3984                    1,
3985                    2,
3986                    ScheduledMode::TraverseSubtreeRoot,
3987                    Some(&mut dirty_trace),
3988                );
3989                break;
3990            }
3991            scheduler.enqueue_after_no_rewrite(
3992                WorklistSchedulingContext::new(
3993                    &surfaces[surface].arena,
3994                    surface,
3995                    &rule_groups,
3996                    RewriteConfig::optimised(),
3997                ),
3998                node_id,
3999                level,
4000                level + 1,
4001                mode,
4002                Some(&mut dirty_trace),
4003            );
4004        }
4005        assert!(found_refreshed_root);
4006
4007        let mut found_sibling_at_next_level = false;
4008        for _ in 0..32 {
4009            let Some((level, _surface, node_id, mode)) = scheduler.pop_next(
4010                &surfaces,
4011                &rule_groups,
4012                RewriteConfig::optimised(),
4013                &mut dirty_trace,
4014            ) else {
4015                break;
4016            };
4017            if (level, node_id, mode)
4018                == (1, root_sibling_id, ScheduledMode::TraverseSubtreeDescendant)
4019            {
4020                found_sibling_at_next_level = true;
4021                break;
4022            }
4023        }
4024        assert!(found_sibling_at_next_level);
4025    }
4026
4027    #[test]
4028    fn worklist_prunes_child_subtrees_without_candidates_at_level() {
4029        let eq = Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
4030        let tree = root(vec![eq.clone(), int_lit(3)]);
4031        let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
4032        let arena = &surfaces[0].arena;
4033        let ids = rewriter_preorder_ids(arena);
4034        let root_id = ids[0];
4035        let eq_id = ids[1];
4036
4037        let rule_groups = test_rule_groups_targeting_expr(&eq);
4038        let mut scheduler =
4039            WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
4040        let mut dirty_trace = DirtyTrace::default();
4041
4042        assert_eq!(
4043            scheduler.pop_next(
4044                &surfaces,
4045                &rule_groups,
4046                RewriteConfig::optimised(),
4047                &mut dirty_trace
4048            ),
4049            Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
4050        );
4051        scheduler.enqueue_after_no_rewrite(
4052            WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
4053            root_id,
4054            0,
4055            1,
4056            ScheduledMode::TraverseSubtreeRoot,
4057            Some(&mut dirty_trace),
4058        );
4059
4060        assert_eq!(
4061            scheduler.pop_next(
4062                &surfaces,
4063                &rule_groups,
4064                RewriteConfig::optimised(),
4065                &mut dirty_trace
4066            ),
4067            Some((0, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
4068        );
4069        assert_eq!(
4070            scheduler.pop_next(
4071                &surfaces,
4072                &rule_groups,
4073                RewriteConfig::optimised(),
4074                &mut dirty_trace
4075            ),
4076            None
4077        );
4078    }
4079
4080    #[test]
4081    fn worklist_no_rewrite_skips_levels_without_candidates_for_subtree() {
4082        let tree = root(vec![int_lit(1)]);
4083        let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
4084        let arena = &surfaces[0].arena;
4085        let ids = rewriter_preorder_ids(arena);
4086        let root_id = ids[0];
4087        let child_id = ids[1];
4088
4089        let rule_groups = test_rule_groups_with_no_candidate_middle_level();
4090        let mut scheduler =
4091            WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
4092        let mut dirty_trace = DirtyTrace::default();
4093
4094        assert_eq!(
4095            scheduler.pop_next(
4096                &surfaces,
4097                &rule_groups,
4098                RewriteConfig::optimised(),
4099                &mut dirty_trace
4100            ),
4101            Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
4102        );
4103
4104        scheduler.enqueue_after_no_rewrite(
4105            WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
4106            root_id,
4107            0,
4108            1,
4109            ScheduledMode::TraverseSubtreeRoot,
4110            Some(&mut dirty_trace),
4111        );
4112
4113        assert_eq!(
4114            scheduler.pop_next(
4115                &surfaces,
4116                &rule_groups,
4117                RewriteConfig::optimised(),
4118                &mut dirty_trace
4119            ),
4120            Some((0, 0, child_id, ScheduledMode::TraverseSubtreeDescendant))
4121        );
4122        assert_eq!(
4123            scheduler.pop_next(
4124                &surfaces,
4125                &rule_groups,
4126                RewriteConfig::optimised(),
4127                &mut dirty_trace
4128            ),
4129            Some((2, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
4130        );
4131    }
4132
4133    #[test]
4134    fn worklist_reference_invalidation_schedules_affected_path_once() {
4135        let x = Name::user("x");
4136        let tree = root(vec![
4137            Expr::Eq(
4138                Metadata::new(),
4139                Moo::new(reference_expr(&x)),
4140                Moo::new(int_lit(1)),
4141            ),
4142            int_lit(2),
4143        ]);
4144        let arena = ExpressionArena::from_root(tree);
4145        let ids = rewriter_preorder_ids(&arena);
4146        let root_id = ids[0];
4147        let eq_id = ids[1];
4148        let reference_id = ids[2];
4149
4150        let mut affected_nodes = Vec::new();
4151        collect_worklist_nodes_referencing_names(
4152            &arena,
4153            arena.root(),
4154            std::slice::from_ref(&x),
4155            &mut affected_nodes,
4156        );
4157
4158        assert_eq!(
4159            affected_nodes.into_iter().rev().collect_vec(),
4160            vec![root_id, eq_id, reference_id]
4161        );
4162    }
4163
4164    #[test]
4165    fn worklist_caches_symbols_only_failures_until_symbols_change() {
4166        let tree = root(vec![int_lit(1)]);
4167        let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
4168        let node_id = surfaces[0].arena.root();
4169        let rule_groups = vec![RuleGroup::new(
4170            1,
4171            vec![crate::rule_engine::RuleData {
4172                rule: &TEST_SYMBOLS_ONLY_RULE,
4173                priority: 1,
4174                rule_set: &TEST_RULE_SET,
4175            }],
4176        )];
4177        let rule_data = &rule_groups[0].rules[0];
4178        let mut scheduler = WorklistScheduler::empty(&rule_groups);
4179
4180        assert!(scheduler.should_attempt_rule(0, node_id, rule_data));
4181        scheduler.record_rule_failure(0, node_id, rule_data);
4182        assert!(!scheduler.should_attempt_rule(0, node_id, rule_data));
4183
4184        scheduler.invalidate_symbol_rule_failures();
4185        assert!(scheduler.should_attempt_rule(0, node_id, rule_data));
4186    }
4187
4188    #[test]
4189    fn evaluator_batches_ancestor_work_until_the_worklist_is_empty() {
4190        let x = Name::user("x");
4191        let and = Expr::And(
4192            Metadata::new(),
4193            Moo::new(matrix_expr![bool_lit(true), reference_expr(&x)]),
4194        );
4195        let mut surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(root(
4196            vec![and],
4197        )))];
4198        let ids = rewriter_preorder_ids(&surfaces[0].arena);
4199        let and_id = ids
4200            .iter()
4201            .copied()
4202            .find(|&node_id| matches!(surfaces[0].arena.expression(node_id), Expr::And(..)))
4203            .expect("and node");
4204        let true_id = ids
4205            .iter()
4206            .copied()
4207            .find(|&node_id| surfaces[0].arena.expression(node_id) == &bool_lit(true))
4208            .expect("true literal");
4209
4210        replace_focus_and_sync_ancestors(&mut surfaces[0].arena, true_id, bool_lit(false));
4211        let mut work = DeferredEvaluatorWork::default();
4212        work.defer_ancestors(&mut surfaces[0].arena, 0, true_id);
4213        work.defer_ancestors(&mut surfaces[0].arena, 0, true_id);
4214        assert_eq!(
4215            work.nodes.len(),
4216            3,
4217            "matrix, `and`, and root ancestors are unique"
4218        );
4219        assert!(matches!(
4220            surfaces[0].arena.expression(and_id),
4221            Expr::And(..)
4222        ));
4223
4224        let rule_groups = Vec::new();
4225        let mut scheduler = WorklistScheduler::empty(&rule_groups);
4226        let mut dirty_trace = DirtyTrace::default();
4227        assert!(normalise_deferred_evaluators(
4228            &mut work,
4229            &mut Model::new(Default::default()),
4230            &mut surfaces,
4231            &mut scheduler,
4232            &mut dirty_trace,
4233        ));
4234        assert_eq!(surfaces[0].arena.expression(and_id), &bool_lit(false));
4235        assert!(work.is_empty());
4236    }
4237}