1
use super::{
2
    AtomKind, RewriteError, RuleFailureInvalidation, RulePrefilter, RuleSet,
3
    resolve_rules::RuleData,
4
};
5
use 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

            
31
use itertools::Itertools;
32
use 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
};
41
use 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)]
63
use crate::ast::assertions::debug_assert_model_well_formed;
64

            
65
type ApplicableRule<'a, CtxFnType> = (
66
    RuleResult<'a>,
67
    usize,
68
    Expr,
69
    CtxFnType,
70
    Option<VariableDeclarationSnapshot>,
71
);
72

            
73
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74
enum 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

            
83
impl ScheduledMode {
84
96463759
    fn includes(self, other: Self) -> bool {
85
96463759
        matches!(
86
96463759
            (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
96463759
    }
107

            
108
2689651747
    fn descends_on_failure(self) -> bool {
109
2689651747
        matches!(
110
2689651747
            self,
111
            ScheduledMode::TraverseSubtreeRoot | ScheduledMode::TraverseSubtreeDescendant
112
        )
113
2689651747
    }
114

            
115
2689651747
    fn next_self_mode(self) -> Option<Self> {
116
2689651747
        match self {
117
412449679
            ScheduledMode::CheckNode => Some(ScheduledMode::CheckNode),
118
382052582
            ScheduledMode::TraverseSubtreeRoot => Some(ScheduledMode::TraverseSubtreeRoot),
119
1895149486
            ScheduledMode::TraverseSubtreeDescendant => None,
120
        }
121
2689651747
    }
122

            
123
825809592
    fn advances_as_subtree(self) -> bool {
124
825809592
        matches!(self, ScheduledMode::TraverseSubtreeRoot)
125
825809592
    }
126
}
127

            
128
#[derive(Default, Debug)]
129
struct WorklistModeCounts {
130
    check_node: usize,
131
    traverse_subtree: usize,
132
}
133

            
134
impl 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)]
150
enum WorklistStaleReason {
151
    ModeMismatch,
152
    MissingSurface,
153
    InactiveSurface,
154
    UnreachableNode,
155
    GenerationMismatch,
156
}
157

            
158
#[derive(Default, Debug)]
159
struct WorklistStaleReasonCounts {
160
    mode_mismatch: WorklistModeCounts,
161
    missing_surface: WorklistModeCounts,
162
    inactive_surface: WorklistModeCounts,
163
    unreachable_node: WorklistModeCounts,
164
    generation_mismatch: WorklistModeCounts,
165
}
166

            
167
impl 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)]
180
struct 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)]
209
enum DirtyTraceDestination {
210
    #[default]
211
    Stderr,
212
    File(PathBuf),
213
    Directory(PathBuf),
214
}
215

            
216
impl DirtyTrace {
217
1351539
    fn from_env() -> Self {
218
1351539
        let Some(destination) = std::env::var_os("CONJURE_DIRTY_TRACE") else {
219
1351539
            return Self::default();
220
        };
221

            
222
        Self {
223
            enabled: true,
224
            destination: dirty_trace_destination_from_env_value(destination),
225
            ..Self::default()
226
        }
227
1351539
    }
228

            
229
81135587
    fn record_rewrite(&mut self, rule_name: &str, side_effects: bool) {
230
81135587
        if !self.enabled {
231
81135587
            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
81135587
    }
245

            
246
30750329
    fn record_side_effect_kept_in_arena(&mut self) {
247
30750329
        if !self.enabled {
248
30750329
            return;
249
        }
250
        self.side_effects_kept_in_arena += 1;
251
30750329
    }
252

            
253
2686245224
    fn record_rule_attempt(&mut self, priority: u16, rule_name: &str) {
254
2686245224
        self.rule_attempts += 1;
255
2686245224
        if !self.enabled {
256
2686245224
            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
2686245224
    }
264

            
265
2999259064
    fn record_worklist_enqueue(&mut self, mode: ScheduledMode) {
266
2999259064
        if !self.enabled {
267
2999259064
            return;
268
        }
269
        self.worklist_enqueues += 1;
270
        self.worklist_enqueues_by_mode.increment(mode);
271
2999259064
    }
272

            
273
2815260001
    fn record_worklist_pop(&mut self, mode: ScheduledMode) {
274
2815260001
        if !self.enabled {
275
2815260001
            return;
276
        }
277
        self.worklist_pops += 1;
278
        self.worklist_pops_by_mode.increment(mode);
279
2815260001
    }
280

            
281
186113813
    fn record_worklist_stale_pop(&mut self, mode: ScheduledMode, reason: WorklistStaleReason) {
282
186113813
        if !self.enabled {
283
186113813
            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
186113813
    }
289

            
290
1398605707
    fn record_worklist_no_candidate_pop(&mut self, mode: ScheduledMode) {
291
1398605707
        if !self.enabled {
292
1398605707
            return;
293
        }
294
        self.worklist_no_candidate_pops_by_mode.increment(mode);
295
1398605707
    }
296

            
297
1343493166
    fn record_worklist_rule_attempt_pop(&mut self, mode: ScheduledMode) {
298
1343493166
        if !self.enabled {
299
1343493166
            return;
300
        }
301
        self.worklist_rule_attempt_pops_by_mode.increment(mode);
302
1343493166
    }
303

            
304
2277202068
    fn record_worklist_child_descent(&mut self, mode: ScheduledMode, children: usize) {
305
2277202068
        if !self.enabled {
306
2277202068
            return;
307
        }
308
        self.worklist_child_descents_by_mode.add(mode, children);
309
2277202068
    }
310

            
311
1351539
    fn finish(&self, stats: &RewriterStats) {
312
1351539
        if !self.enabled {
313
1351539
            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
1351539
    }
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

            
497
2
fn dirty_trace_destination_from_env_value(
498
2
    destination: std::ffi::OsString,
499
2
) -> DirtyTraceDestination {
500
2
    if destination.is_empty()
501
2
        || destination == "1"
502
2
        || destination
503
2
            .to_str()
504
2
            .is_some_and(|value| value.eq_ignore_ascii_case("true"))
505
    {
506
        return DirtyTraceDestination::Stderr;
507
2
    }
508

            
509
2
    let path = PathBuf::from(destination);
510
2
    if path.is_file() {
511
        return DirtyTraceDestination::File(path);
512
2
    }
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
2
    if path.is_dir() || path.extension().is_none() {
518
1
        DirtyTraceDestination::Directory(path)
519
    } else {
520
1
        DirtyTraceDestination::File(path)
521
    }
522
2
}
523

            
524
fn 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

            
533
1
fn sanitize_dirty_trace_filename(name: &str) -> String {
534
1
    let sanitized = name
535
1
        .chars()
536
38
        .map(|character| {
537
38
            if character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.') {
538
35
                character
539
            } else {
540
3
                '-'
541
            }
542
38
        })
543
        .collect::<String>();
544

            
545
1
    let sanitized = sanitized.trim_matches('-');
546
1
    if sanitized.is_empty() {
547
        "unknown".to_string()
548
    } else {
549
1
        sanitized.to_string()
550
    }
551
1
}
552

            
553
#[derive(Clone)]
554
struct 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

            
563
enum 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

            
573
impl<'group, 'rules> Iterator for CandidateRules<'group, 'rules> {
574
    type Item = &'group RuleData<'rules>;
575

            
576
4054709398
    fn next(&mut self) -> Option<Self::Item> {
577
4054709398
        match self {
578
1947690083
            CandidateRules::Slice(iter) => iter.next(),
579
            CandidateRules::Filtered {
580
                iter,
581
                expr,
582
                arena_node,
583
                include_universal,
584
            } => loop {
585
13949761473
                let rule_data = iter.next()?;
586
13280801487
                if rule_matches_specific_prefilter(rule_data, expr, *arena_node)
587
11842742158
                    || (*include_universal && rule_is_universal(rule_data))
588
                {
589
1438059329
                    return Some(rule_data);
590
11842742158
                }
591
            },
592
        }
593
4054709398
    }
594
}
595

            
596
impl<'a> RuleGroup<'a> {
597
73735250
    fn new(priority: u16, rules: Vec<RuleData<'a>>) -> Self {
598
73735250
        let discriminants = rules
599
73735250
            .iter()
600
359566827
            .filter_map(|rd| rd.rule.prefilters)
601
            .flatten()
602
621630603
            .filter_map(|prefilter| match prefilter {
603
604894064
                RulePrefilter::Variant(discriminant) => Some(*discriminant),
604
                RulePrefilter::Child { .. }
605
                | RulePrefilter::VariantChild { .. }
606
                | RulePrefilter::Atom(_)
607
16736539
                | RulePrefilter::ChildAtom(_) => None,
608
621630603
            })
609
            .collect_vec();
610

            
611
73735250
        let mut rules_by_discriminant = Vec::new();
612
73735250
        if let Some(max_discriminant) = discriminants.iter().copied().max() {
613
72315417
            rules_by_discriminant.resize_with(max_discriminant + 1, || None);
614
1419833
        }
615

            
616
73735250
        let target_discriminants = discriminants.into_iter().unique().collect_vec();
617

            
618
73735250
        for &discriminant in &target_discriminants {
619
338868613
            rules_by_discriminant[discriminant] = Some(
620
338868613
                rules
621
338868613
                    .iter()
622
3506973998
                    .filter(|rd| {
623
3506973998
                        rule_is_universal(rd) || rule_matches_self_discriminant(rd, discriminant)
624
3506973998
                    })
625
                    .cloned()
626
                    .collect(),
627
            );
628
        }
629

            
630
73735250
        let universal_rules: Vec<RuleData<'a>> = rules
631
73735250
            .iter()
632
359566827
            .filter(|rd| rule_is_universal(rd))
633
            .cloned()
634
            .collect();
635
332314429
        let has_non_discriminant_filters = rules.iter().any(|rd| {
636
332314429
            rd.rule.prefilters.is_some_and(|prefilters| {
637
332282661
                prefilters
638
332282661
                    .iter()
639
581773450
                    .any(|prefilter| !matches!(prefilter, RulePrefilter::Variant(_)))
640
332282661
            })
641
332314429
        });
642
73735250
        Self {
643
73735250
            priority,
644
73735250
            rules,
645
73735250
            rules_by_discriminant,
646
73735250
            universal_rules,
647
73735250
            has_non_discriminant_filters,
648
73735250
        }
649
73735250
    }
650

            
651
456559
    fn candidates<'group>(
652
456559
        &'group self,
653
456559
        config: RewriteConfig,
654
456559
        expr: &'group Expr,
655
456559
    ) -> CandidateRules<'group, 'a> {
656
456559
        self.candidates_with_arena(config, expr, None)
657
456559
    }
658

            
659
1355750388
    fn candidates_at_node<'group>(
660
1355750388
        &'group self,
661
1355750388
        config: RewriteConfig,
662
1355750388
        arena: &'group ExpressionArena,
663
1355750388
        node_id: ExpressionNodeId,
664
1355750388
    ) -> CandidateRules<'group, 'a> {
665
1355750388
        self.candidates_with_arena(config, arena.expression(node_id), Some((arena, node_id)))
666
1355750388
    }
667

            
668
1356206947
    fn candidates_with_arena<'group>(
669
1356206947
        &'group self,
670
1356206947
        config: RewriteConfig,
671
1356206947
        expr: &'group Expr,
672
1356206947
        arena_node: Option<(&'group ExpressionArena, ExpressionNodeId)>,
673
1356206947
    ) -> CandidateRules<'group, 'a> {
674
1356206947
        if !config.prefilter {
675
456555
            return CandidateRules::Slice(self.rules.iter());
676
1355750392
        }
677

            
678
1355750392
        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
668959986
            return CandidateRules::Filtered {
685
668959986
                iter: self.rules.iter(),
686
668959986
                expr,
687
668959986
                arena_node,
688
668959986
                include_universal: true,
689
668959986
            };
690
686790406
        }
691

            
692
686790406
        let discriminant = discriminant_from_value(expr);
693
686790406
        CandidateRules::Slice(
694
686790406
            self.rules_by_discriminant
695
686790406
                .get(discriminant)
696
686790406
                .and_then(Option::as_deref)
697
686790406
                .unwrap_or(&self.universal_rules)
698
686790406
                .iter(),
699
686790406
        )
700
1356206947
    }
701

            
702
    #[cfg(test)]
703
13
    fn has_candidates(&self, config: RewriteConfig, expr: &Expr) -> bool {
704
13
        self.has_candidates_with_arena(config, expr, None)
705
13
    }
706

            
707
30971919691
    fn has_candidates_at_node(
708
30971919691
        &self,
709
30971919691
        config: RewriteConfig,
710
30971919691
        arena: &ExpressionArena,
711
30971919691
        node_id: ExpressionNodeId,
712
30971919691
    ) -> bool {
713
30971919691
        self.has_candidates_with_arena(config, arena.expression(node_id), Some((arena, node_id)))
714
30971919691
    }
715

            
716
30971919704
    fn has_candidates_with_arena(
717
30971919704
        &self,
718
30971919704
        config: RewriteConfig,
719
30971919704
        expr: &Expr,
720
30971919704
        arena_node: Option<(&ExpressionArena, ExpressionNodeId)>,
721
30971919704
    ) -> bool {
722
30971919704
        if !config.prefilter {
723
            return !self.rules.is_empty();
724
30971919704
        }
725

            
726
30971919704
        if self.has_non_discriminant_filters {
727
3829820617
            return !self.universal_rules.is_empty()
728
3829820617
                || self
729
3829820617
                    .rules
730
3829820617
                    .iter()
731
42561555347
                    .any(|rule_data| rule_matches_specific_prefilter(rule_data, expr, arena_node));
732
27142099087
        }
733

            
734
27142099087
        let discriminant = discriminant_from_value(expr);
735
27142099087
        !self
736
27142099087
            .rules_by_discriminant
737
27142099087
            .get(discriminant)
738
27142099087
            .and_then(Option::as_deref)
739
27142099087
            .unwrap_or(&self.universal_rules)
740
27142099087
            .is_empty()
741
30971919704
    }
742
}
743

            
744
#[derive(Clone, Debug)]
745
enum RewriteSurfaceKind {
746
    Root,
747
    ValueLetting { name: Name },
748
}
749

            
750
struct RewriteSurface {
751
    kind: RewriteSurfaceKind,
752
    arena: ExpressionArena,
753
    active: bool,
754
}
755

            
756
impl RewriteSurface {
757
1351493
    fn root(arena: ExpressionArena) -> Self {
758
1351493
        Self {
759
1351493
            kind: RewriteSurfaceKind::Root,
760
1351493
            arena,
761
1351493
            active: true,
762
1351493
        }
763
1351493
    }
764

            
765
1287110
    fn value_letting(name: Name, expr: Expr) -> Self {
766
1287110
        Self {
767
1287110
            kind: RewriteSurfaceKind::ValueLetting { name },
768
1287110
            arena: ExpressionArena::from_root(expr),
769
1287110
            active: true,
770
1287110
        }
771
1287110
    }
772
}
773

            
774
#[derive(Clone, Copy, Debug)]
775
struct ScheduledNode {
776
    surface: usize,
777
    node_id: ExpressionNodeId,
778
    generation: u32,
779
    mode: ScheduledMode,
780
    depth: usize,
781
    sequence: u64,
782
}
783

            
784
impl PartialEq for ScheduledNode {
785
    fn eq(&self, other: &Self) -> bool {
786
        self.depth == other.depth && self.sequence == other.sequence
787
    }
788
}
789

            
790
impl Eq for ScheduledNode {}
791

            
792
impl PartialOrd for ScheduledNode {
793
9417721937
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
794
9417721937
        Some(self.cmp(other))
795
9417721937
    }
796
}
797

            
798
impl Ord for ScheduledNode {
799
9417721937
    fn cmp(&self, other: &Self) -> Ordering {
800
9417721937
        other
801
9417721937
            .depth
802
9417721937
            .cmp(&self.depth)
803
6450391320
            .then_with(|| other.sequence.cmp(&self.sequence))
804
9417721937
    }
805
}
806

            
807
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
808
struct 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)]
820
struct 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)]
831
struct FailedSymbolRuleKey {
832
    surface: usize,
833
    node_id: ExpressionNodeId,
834
    rule: usize,
835
}
836

            
837
#[derive(Clone, Copy)]
838
struct WorklistSchedulingContext<'arena, 'groups, 'rules> {
839
    arena: &'arena ExpressionArena,
840
    surface: usize,
841
    rule_groups: &'groups [RuleGroup<'rules>],
842
    config: RewriteConfig,
843
}
844

            
845
impl<'arena, 'groups, 'rules> WorklistSchedulingContext<'arena, 'groups, 'rules> {
846
2689651747
    fn new(
847
2689651747
        arena: &'arena ExpressionArena,
848
2689651747
        surface: usize,
849
2689651747
        rule_groups: &'groups [RuleGroup<'rules>],
850
2689651747
        config: RewriteConfig,
851
2689651747
    ) -> Self {
852
2689651747
        Self {
853
2689651747
            arena,
854
2689651747
            surface,
855
2689651747
            rule_groups,
856
2689651747
            config,
857
2689651747
        }
858
2689651747
    }
859
}
860

            
861
struct 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)]
893
struct DeferredEvaluatorWork {
894
    nodes: HashSet<(usize, ExpressionNodeId)>,
895
}
896

            
897
impl DeferredEvaluatorWork {
898
76560058
    fn defer_ancestors(
899
76560058
        &mut self,
900
76560058
        arena: &ExpressionArena,
901
76560058
        surface: usize,
902
76560058
        node_id: ExpressionNodeId,
903
76560058
    ) {
904
76560058
        let mut current = arena.parent(node_id);
905
338311621
        while let Some(current_id) = current {
906
261751563
            self.nodes.insert((surface, current_id));
907
261751563
            current = arena.parent(current_id);
908
        }
909
76560058
    }
910

            
911
1351499
    fn is_empty(&self) -> bool {
912
1351499
        self.nodes.is_empty()
913
1351499
    }
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)]
924
struct DeferredAncestorRuleWork {
925
    nodes: HashSet<(usize, ExpressionNodeId)>,
926
    postponed: Vec<(usize, usize, ExpressionNodeId, ScheduledMode)>,
927
    flush_after_level: Option<usize>,
928
}
929

            
930
impl DeferredAncestorRuleWork {
931
76560056
    fn defer_ancestors(
932
76560056
        &mut self,
933
76560056
        arena: &ExpressionArena,
934
76560056
        surface: usize,
935
76560056
        node_id: ExpressionNodeId,
936
76560056
        rewrite_level: usize,
937
76560056
    ) {
938
76560056
        self.flush_after_level = Some(
939
76560056
            self.flush_after_level
940
76560056
                .map_or(rewrite_level, |level| level.min(rewrite_level)),
941
        );
942

            
943
76560056
        let mut current = arena.parent(node_id);
944
338311613
        while let Some(current_id) = current {
945
261751557
            self.nodes.insert((surface, current_id));
946
261751557
            current = arena.parent(current_id);
947
        }
948
76560056
    }
949

            
950
2781544151
    fn should_flush_before(&self, next_level: Option<usize>) -> bool {
951
2781544151
        let Some(flush_after_level) = self.flush_after_level else {
952
1592577051
            return false;
953
        };
954
1188967100
        next_level.is_none_or(|level| level > flush_after_level)
955
2781544151
    }
956

            
957
2815259976
    fn contains(&self, surface: usize, node_id: ExpressionNodeId) -> bool {
958
2815259976
        self.nodes.contains(&(surface, node_id))
959
2815259976
    }
960

            
961
35136746
    fn postpone(&mut self, item: (usize, usize, ExpressionNodeId, ScheduledMode)) {
962
35136746
        self.postponed.push(item);
963
35136746
    }
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
30750257
    fn flush_effect_path_before_root(
972
30750257
        &mut self,
973
30750257
        scheduler: &mut WorklistScheduler,
974
30750257
        surfaces: &mut [RewriteSurface],
975
30750257
        deferred_evaluators: &mut DeferredEvaluatorWork,
976
30750257
        surface: usize,
977
30750257
        node_id: ExpressionNodeId,
978
30750257
        dirty_trace: &mut DirtyTrace,
979
30750257
    ) {
980
30750257
        let Some(rewrite_surface) = surfaces.get(surface) else {
981
            return;
982
        };
983
30750257
        let root = rewrite_surface.arena.root();
984
30750257
        let mut path = Vec::new();
985
30750257
        let mut current = rewrite_surface.arena.parent(node_id);
986
81294113
        while let Some(current_id) = current {
987
66784775
            if current_id == root {
988
16240919
                break;
989
50543856
            }
990
50543856
            path.push(current_id);
991
50543856
            current = rewrite_surface.arena.parent(current_id);
992
        }
993

            
994
30750257
        for ancestor in path {
995
50543856
            self.nodes.remove(&(surface, ancestor));
996
50543856
            let Some(rewrite_surface) = surfaces.get_mut(surface) else {
997
                continue;
998
            };
999
50543856
            if !rewrite_surface.active || !rewrite_surface.arena.is_reachable(ancestor) {
                continue;
50543856
            }
50543856
            rewrite_surface
50543856
                .arena
50543856
                .rebuild_payload_from_children(ancestor);
50543856
            let evaluator_changed = if matches!(rewrite_surface.kind, RewriteSurfaceKind::Root) {
50542876
                deferred_evaluators.nodes.remove(&(surface, ancestor));
50542876
                normalise_evaluator_node_to_fixpoint_without_ancestor_sync(
50542876
                    &mut rewrite_surface.arena,
50542876
                    ancestor,
50542876
                    dirty_trace,
50542876
                )
            } else {
980
                false
            };
50543856
            if evaluator_changed {
12383
                scheduler.index_subtree_references(&rewrite_surface.arena, surface, ancestor);
12383
                scheduler.enqueue_subtree(
12383
                    &rewrite_surface.arena,
12383
                    surface,
12383
                    ancestor,
12383
                    Some(dirty_trace),
12383
                );
            } else {
50531473
                scheduler.enqueue_node_at_level(
50531473
                    &rewrite_surface.arena,
50531473
                    surface,
50531473
                    ancestor,
50531473
                    0,
50531473
                    ScheduledMode::CheckNode,
50531473
                    Some(dirty_trace),
50531473
                );
            }
        }
30750257
    }
25836559
    fn flush(
25836559
        &mut self,
25836559
        scheduler: &mut WorklistScheduler,
25836559
        surfaces: &mut [RewriteSurface],
25836559
        deferred_evaluators: &mut DeferredEvaluatorWork,
25836559
        dirty_trace: &mut DirtyTrace,
25836559
    ) {
25836559
        let nodes = deferred_nodes_in_bottom_up_order(std::mem::take(&mut self.nodes), surfaces);
25836559
        let postponed = std::mem::take(&mut self.postponed);
25836559
        self.flush_after_level = None;
        // Expressions above a changed child are left alone while the priority finishes, so
        // anything already queued against them stays valid. Rebuild deepest first, once each, so
        // every ancestor sees up-to-date children. Ancestors on the root surface also get the
        // evaluator here, so a parent sees an already-normalised child and the path above a
        // rewrite is copied once per batch instead of once per rewrite.
25836559
        let mut evaluator_changed = HashSet::new();
25836559
        for &(surface, node_id, _) in &nodes {
92859216
            let Some(rewrite_surface) = surfaces.get_mut(surface) else {
                continue;
            };
92859216
            if !rewrite_surface.active || !rewrite_surface.arena.is_reachable(node_id) {
                continue;
92859216
            }
92859216
            rewrite_surface.arena.rebuild_payload_from_children(node_id);
92859216
            if matches!(rewrite_surface.kind, RewriteSurfaceKind::Root) {
92852828
                deferred_evaluators.nodes.remove(&(surface, node_id));
92852828
                if node_id != rewrite_surface.arena.root()
73264228
                    && normalise_evaluator_node_to_fixpoint_without_ancestor_sync(
73264228
                        &mut rewrite_surface.arena,
73264228
                        node_id,
73264228
                        dirty_trace,
73264228
                    )
                {
234498
                    evaluator_changed.insert((surface, node_id));
92618330
                }
6388
            }
        }
25836559
        for (surface, node_id, _) in nodes {
92859216
            let Some(rewrite_surface) = surfaces.get(surface) else {
                continue;
            };
92859216
            if !rewrite_surface.active || !rewrite_surface.arena.is_reachable(node_id) {
220110
                continue;
92639106
            }
92639106
            if evaluator_changed.contains(&(surface, node_id)) {
211907
                scheduler.index_subtree_references(&rewrite_surface.arena, surface, node_id);
211907
                scheduler.enqueue_subtree(
211907
                    &rewrite_surface.arena,
211907
                    surface,
211907
                    node_id,
211907
                    Some(dirty_trace),
211907
                );
            } else {
92427199
                scheduler.enqueue_node_at_level(
92427199
                    &rewrite_surface.arena,
92427199
                    surface,
92427199
                    node_id,
92427199
                    0,
92427199
                    ScheduledMode::CheckNode,
92427199
                    Some(dirty_trace),
92427199
                );
            }
        }
        // An item taken off the queue while its ancestors were still out of date must not be
        // dropped. Put it back now that they are rebuilt, so descendants it had not reached yet
        // -- new top-level constraints in particular -- still get visited.
25836559
        for (level, surface, node_id, mode) in postponed {
35136746
            let Some(rewrite_surface) = surfaces.get(surface) else {
                continue;
            };
35136746
            if !rewrite_surface.active || !rewrite_surface.arena.is_reachable(node_id) {
144797
                continue;
34991949
            }
34991949
            scheduler.enqueue_node_at_level(
34991949
                &rewrite_surface.arena,
34991949
                surface,
34991949
                node_id,
34991949
                level,
34991949
                mode,
34991949
                Some(dirty_trace),
34991949
            );
        }
25836559
    }
}
impl WorklistScheduler {
1351493
    fn empty(rule_groups: &[RuleGroup<'_>]) -> Self {
1351493
        Self {
1351493
            queues_by_level: vec![BinaryHeap::new(); rule_groups.len()],
1351493
            scheduled: HashMap::new(),
1351493
            subtree_candidate_cache: Vec::new(),
1351493
            failed_symbol_rules: HashMap::new(),
1351493
            reference_nodes_by_name: HashMap::new(),
1351493
            symbol_revision: 0,
1351493
            next_sequence: 0,
1351493
        }
1351493
    }
1351489
    fn new(
1351489
        surfaces: &[RewriteSurface],
1351489
        rule_groups: &[RuleGroup<'_>],
1351489
        config: RewriteConfig,
1351489
    ) -> Self {
1351489
        let mut scheduler = Self::empty(rule_groups);
1351489
        for surface in 0..surfaces.len() {
2114751
            scheduler.enqueue_surface(surfaces, surface, rule_groups, config, None);
        }
1351489
        scheduler
1351489
    }
2638599
    fn enqueue_surface(
2638599
        &mut self,
2638599
        surfaces: &[RewriteSurface],
2638599
        surface: usize,
2638599
        _rule_groups: &[RuleGroup<'_>],
2638599
        _config: RewriteConfig,
2638599
        dirty_trace: Option<&mut DirtyTrace>,
2638599
    ) {
2638599
        let Some(rewrite_surface) = surfaces.get(surface) else {
            return;
        };
2638599
        if !rewrite_surface.active {
            return;
2638599
        }
2638599
        self.index_subtree_references(
2638599
            &rewrite_surface.arena,
2638599
            surface,
2638599
            rewrite_surface.arena.root(),
2638599
        );
2638599
        self.enqueue_subtree(
2638599
            &rewrite_surface.arena,
2638599
            surface,
2638599
            rewrite_surface.arena.root(),
2638599
            dirty_trace,
2638599
        );
2638599
    }
79422946
    fn index_subtree_references(
79422946
        &mut self,
79422946
        arena: &ExpressionArena,
79422946
        surface: usize,
79422946
        subtree_root: ExpressionNodeId,
79422946
    ) {
79422946
        for node_id in rewriter_reachable_subtree_ids(arena, subtree_root) {
1488792672
            let Expr::Atomic(_, Atom::Reference(reference)) = arena.expression(node_id) else {
914621815
                continue;
            };
574170857
            self.reference_nodes_by_name
574170857
                .entry(reference.name().clone())
574170857
                .or_default()
574170857
                .insert((surface, node_id));
        }
79422946
    }
2696141682
    fn should_attempt_rule(
2696141682
        &self,
2696141682
        surface: usize,
2696141682
        node_id: ExpressionNodeId,
2696141682
        rule_data: &RuleData<'_>,
2696141682
    ) -> bool {
2696141682
        if rule_data.rule.failure_invalidation != RuleFailureInvalidation::SymbolsOnly {
2669498409
            return true;
26643273
        }
26643273
        self.failed_symbol_rules.get(&FailedSymbolRuleKey {
26643273
            surface,
26643273
            node_id,
26643273
            rule: rule_data.rule as *const _ as usize,
26643273
        }) != Some(&self.symbol_revision)
2696141682
    }
2619175634
    fn record_rule_failure(
2619175634
        &mut self,
2619175634
        surface: usize,
2619175634
        node_id: ExpressionNodeId,
2619175634
        rule_data: &RuleData<'_>,
2619175634
    ) {
2619175634
        if rule_data.rule.failure_invalidation == RuleFailureInvalidation::SymbolsOnly {
13543971
            self.failed_symbol_rules.insert(
13543971
                FailedSymbolRuleKey {
13543971
                    surface,
13543971
                    node_id,
13543971
                    rule: rule_data.rule as *const _ as usize,
13543971
                },
13543971
                self.symbol_revision,
13543971
            );
2605631663
        }
2619175634
    }
17344660
    fn invalidate_symbol_rule_failures(&mut self) {
17344660
        let (next_revision, overflowed) = self.symbol_revision.overflowing_add(1);
17344660
        self.symbol_revision = next_revision;
17344660
        if overflowed {
            self.failed_symbol_rules.clear();
17344660
        }
17344660
    }
79424479
    fn enqueue_subtree(
79424479
        &mut self,
79424479
        arena: &ExpressionArena,
79424479
        surface: usize,
79424479
        node_id: ExpressionNodeId,
79424479
        dirty_trace: Option<&mut DirtyTrace>,
79424479
    ) {
79424479
        self.enqueue_node_at_level(
79424479
            arena,
79424479
            surface,
79424479
            node_id,
79424479
            0,
79424479
            ScheduledMode::TraverseSubtreeRoot,
79424479
            dirty_trace,
79424479
        );
79424479
    }
    #[cfg(test)]
2
    fn enqueue_node_and_ancestors(
2
        &mut self,
2
        arena: &ExpressionArena,
2
        surface: usize,
2
        node_id: ExpressionNodeId,
2
        dirty_trace: &mut DirtyTrace,
2
    ) {
2
        let mut chain = Vec::new();
2
        let mut current = Some(node_id);
7
        while let Some(current_id) = current {
5
            chain.push(current_id);
5
            current = arena.parent(current_id);
        }
2
        for current_id in chain.into_iter().rev() {
5
            self.enqueue_node_at_level(
5
                arena,
5
                surface,
5
                current_id,
5
                0,
5
                ScheduledMode::CheckNode,
5
                Some(dirty_trace),
5
            );
        }
2
    }
2277202068
    fn enqueue_children_at_level(
2277202068
        &mut self,
2277202068
        context: WorklistSchedulingContext<'_, '_, '_>,
2277202068
        node_id: ExpressionNodeId,
2277202068
        level: usize,
2277202068
        mut dirty_trace: Option<&mut DirtyTrace>,
2277202068
    ) -> usize {
2277202068
        if matches!(context.arena.expression(node_id), Expr::Comprehension(_, _)) {
7703708
            return 0;
2269498360
        }
2269498360
        let mut child_count = 0;
2269498360
        for &child_id in context.arena.children(node_id) {
4494879736
            if !self.subtree_has_candidates_at_level(
4494879736
                context.arena,
4494879736
                context.surface,
4494879736
                child_id,
4494879736
                level,
4494879736
                context.rule_groups,
4494879736
                context.config,
4494879736
            ) {
2522314279
                continue;
1972565457
            }
1972565457
            child_count += 1;
1972565457
            self.enqueue_node_at_level(
1972565457
                context.arena,
1972565457
                context.surface,
1972565457
                child_id,
1972565457
                level,
1972565457
                ScheduledMode::TraverseSubtreeDescendant,
1972565457
                dirty_trace.as_deref_mut(),
1972565457
            );
        }
2269498360
        child_count
2277202068
    }
3121192554
    fn enqueue_node_at_level(
3121192554
        &mut self,
3121192554
        arena: &ExpressionArena,
3121192554
        surface: usize,
3121192554
        node_id: ExpressionNodeId,
3121192554
        level: usize,
3121192554
        mode: ScheduledMode,
3121192554
        mut dirty_trace: Option<&mut DirtyTrace>,
3121192554
    ) {
3121192554
        if level >= self.queues_by_level.len() {
42481903
            return;
3078710651
        }
3078710651
        if !arena.is_reachable(node_id) {
12325
            return;
3078698326
        }
3078698326
        let scheduled = ScheduledNode {
3078698326
            surface,
3078698326
            node_id,
3078698326
            generation: arena.generation(node_id),
3078698326
            mode,
3078698326
            depth: arena.depth(node_id),
3078698326
            sequence: self.next_sequence,
3078698326
        };
3078698326
        self.next_sequence += 1;
3078698326
        let key = ScheduledKey {
3078698326
            level,
3078698326
            surface,
3078698326
            node_id,
3078698326
            generation: scheduled.generation,
3078698326
        };
3078698326
        match self.scheduled.entry(key) {
            std::collections::hash_map::Entry::Vacant(entry) => {
2982234567
                entry.insert(mode);
2982234567
                self.queues_by_level[level].push(scheduled);
2982234567
                if let Some(trace) = dirty_trace.as_deref_mut() {
2980119816
                    trace.record_worklist_enqueue(mode);
2114751
                }
            }
            std::collections::hash_map::Entry::Occupied(mut entry) => {
96463759
                if !entry.get().includes(mode) {
19139248
                    entry.insert(mode);
19139248
                    self.queues_by_level[level].push(scheduled);
19139248
                    if let Some(trace) = dirty_trace {
19139248
                        trace.record_worklist_enqueue(mode);
                    }
77324511
                }
            }
        }
3121192554
    }
2689651747
    fn enqueue_after_no_rewrite(
2689651747
        &mut self,
2689651747
        context: WorklistSchedulingContext<'_, '_, '_>,
2689651747
        node_id: ExpressionNodeId,
2689651747
        level: usize,
2689651747
        next_self_level: usize,
2689651747
        mode: ScheduledMode,
2689651747
        mut dirty_trace: Option<&mut DirtyTrace>,
2689651747
    ) {
2689651747
        if mode.descends_on_failure() {
            let child_count =
2277202068
                self.enqueue_children_at_level(context, node_id, level, dirty_trace.as_deref_mut());
2277202068
            if let Some(trace) = dirty_trace.as_deref_mut() {
2277202068
                trace.record_worklist_child_descent(mode, child_count);
            }
412449679
        }
2689651747
        let Some(next_self_mode) = mode.next_self_mode() else {
1895149486
            return;
        };
        // Descendants reached while traversing the current level do not eagerly advance to
        // future levels. The traversal root advances and rediscovers descendants only after it
        // survives that future level, avoiding stale descendant work when an ancestor rewrites.
794502261
        let next_self_level = if mode.advances_as_subtree() {
382052582
            self.next_subtree_candidate_level(
382052582
                context.arena,
382052582
                context.surface,
382052582
                node_id,
382052582
                next_self_level,
382052582
                context.rule_groups,
382052582
                context.config,
382052582
            )
        } else {
412449679
            next_worklist_candidate_level(
412449679
                context.arena,
412449679
                node_id,
412449679
                next_self_level,
412449679
                context.rule_groups,
412449679
                context.config,
412449679
            )
        };
794502261
        self.enqueue_node_at_level(
794502261
            context.arena,
794502261
            context.surface,
794502261
            node_id,
794502261
            next_self_level,
794502261
            next_self_mode,
794502261
            dirty_trace,
794502261
        );
2689651747
    }
2816680926
    fn pop_next(
2816680926
        &mut self,
2816680926
        surfaces: &[RewriteSurface],
2816680926
        rule_groups: &[RuleGroup<'_>],
2816680926
        config: RewriteConfig,
2816680926
        dirty_trace: &mut DirtyTrace,
2816680926
    ) -> Option<(usize, usize, ExpressionNodeId, ScheduledMode)> {
2816680926
        for level in 0..self.queues_by_level.len() {
45953081520
            while let Some(scheduled) = self.queues_by_level[level].pop() {
3001373814
                let key = ScheduledKey {
3001373814
                    level,
3001373814
                    surface: scheduled.surface,
3001373814
                    node_id: scheduled.node_id,
3001373814
                    generation: scheduled.generation,
3001373814
                };
3001373814
                if self.scheduled.get(&key).copied() != Some(scheduled.mode) {
19139248
                    dirty_trace.record_worklist_stale_pop(
19139248
                        scheduled.mode,
19139248
                        WorklistStaleReason::ModeMismatch,
19139248
                    );
19139248
                    continue;
2982234566
                }
2982234566
                self.scheduled.remove(&key);
2982234566
                let Some(surface) = surfaces.get(scheduled.surface) else {
                    dirty_trace.record_worklist_stale_pop(
                        scheduled.mode,
                        WorklistStaleReason::MissingSurface,
                    );
                    continue;
                };
2982234566
                if !surface.active {
                    dirty_trace.record_worklist_stale_pop(
                        scheduled.mode,
                        WorklistStaleReason::InactiveSurface,
                    );
                    continue;
2982234566
                }
2982234566
                let arena = &surface.arena;
2982234566
                if !arena.is_reachable(scheduled.node_id) {
135667234
                    dirty_trace.record_worklist_stale_pop(
135667234
                        scheduled.mode,
135667234
                        WorklistStaleReason::UnreachableNode,
135667234
                    );
135667234
                    continue;
2846567332
                }
2846567332
                if arena.generation(scheduled.node_id) != scheduled.generation {
31307331
                    dirty_trace.record_worklist_stale_pop(
31307331
                        scheduled.mode,
31307331
                        WorklistStaleReason::GenerationMismatch,
31307331
                    );
31307331
                    if scheduled.mode.advances_as_subtree() {
                        // A descendant rewrite updates ancestor generations. Refreshing the
                        // traversal root preserves its job of carrying still-unseen sibling
                        // descendants to later rule levels.
8055398
                        let refresh_level = self.next_subtree_candidate_level(
8055398
                            arena,
8055398
                            scheduled.surface,
8055398
                            scheduled.node_id,
8055398
                            level,
8055398
                            rule_groups,
8055398
                            config,
8055398
                        );
8055398
                        self.enqueue_node_at_level(
8055398
                            arena,
8055398
                            scheduled.surface,
8055398
                            scheduled.node_id,
8055398
                            refresh_level,
8055398
                            scheduled.mode,
8055398
                            Some(dirty_trace),
8055398
                        );
23251933
                    }
31307331
                    continue;
2815260001
                }
2815260001
                dirty_trace.record_worklist_pop(scheduled.mode);
2815260001
                return Some((level, scheduled.surface, scheduled.node_id, scheduled.mode));
            }
        }
1420925
        None
2816680926
    }
390107980
    fn next_subtree_candidate_level(
390107980
        &mut self,
390107980
        arena: &ExpressionArena,
390107980
        surface: usize,
390107980
        node_id: ExpressionNodeId,
390107980
        start_level: usize,
390107980
        rule_groups: &[RuleGroup<'_>],
390107980
        config: RewriteConfig,
390107980
    ) -> usize {
390107980
        if start_level >= rule_groups.len() || !arena.is_reachable(node_id) {
416376
            return rule_groups.len();
389691604
        }
389691604
        (start_level..rule_groups.len())
1896467040
            .find(|&level| {
1896467040
                self.subtree_has_candidates_at_level(
1896467040
                    arena,
1896467040
                    surface,
1896467040
                    node_id,
1896467040
                    level,
1896467040
                    rule_groups,
1896467040
                    config,
1896467040
                )
1896467040
            })
389691604
            .unwrap_or(rule_groups.len())
390107980
    }
24291510632
    fn subtree_has_candidates_at_level(
24291510632
        &mut self,
24291510632
        arena: &ExpressionArena,
24291510632
        surface: usize,
24291510632
        node_id: ExpressionNodeId,
24291510632
        level: usize,
24291510632
        rule_groups: &[RuleGroup<'_>],
24291510632
        config: RewriteConfig,
24291510632
    ) -> bool {
24291510632
        if level >= rule_groups.len() || !arena.is_reachable(node_id) {
            return false;
24291510632
        }
24291510632
        let cache_bit = 1u128.checked_shl(level as u32);
24291510632
        if let Some(bit) = cache_bit {
24291510632
            if self.subtree_candidate_cache.len() <= surface {
2638599
                self.subtree_candidate_cache
2638599
                    .resize_with(surface + 1, Vec::new);
24288872033
            }
24291510632
            let surface_cache = &mut self.subtree_candidate_cache[surface];
24291510632
            if surface_cache.len() <= node_id.index() {
1303548998
                surface_cache.resize(node_id.index() + 1, NodeSubtreeCandidateCache::default());
22987961634
            }
24291510632
            let entry = &mut surface_cache[node_id.index()];
24291510632
            let generation = arena.generation(node_id);
24291510632
            if !entry.generation_valid || entry.generation != generation {
1471803163
                *entry = NodeSubtreeCandidateCache {
1471803163
                    generation,
1471803163
                    generation_valid: true,
1471803163
                    ..NodeSubtreeCandidateCache::default()
1471803163
                };
22819707469
            }
24291510632
            if entry.known_levels & bit != 0 {
1586656689
                return entry.candidate_levels & bit != 0;
22704853943
            }
        }
22704853943
        let rule_group = &rule_groups[level];
22704853943
        let has_candidates = rule_group.has_candidates_at_node(config, arena, node_id)
21531619431
            || (!matches!(arena.expression(node_id), Expr::Comprehension(_, _))
21463034228
                && arena.children(node_id).iter().any(|&child_id| {
17900163856
                    self.subtree_has_candidates_at_level(
17900163856
                        arena,
17900163856
                        surface,
17900163856
                        child_id,
17900163856
                        level,
17900163856
                        rule_groups,
17900163856
                        config,
17900163856
                    )
17900163856
                }));
22704853943
        if let Some(bit) = cache_bit {
22704853943
            let entry = &mut self.subtree_candidate_cache[surface][node_id.index()];
22704853943
            entry.known_levels |= bit;
22704853943
            if has_candidates {
2393198168
                entry.candidate_levels |= bit;
20311655775
            }
        }
22704853943
        has_candidates
24291510632
    }
}
412449679
fn next_worklist_candidate_level(
412449679
    arena: &ExpressionArena,
412449679
    node_id: ExpressionNodeId,
412449679
    start_level: usize,
412449679
    rule_groups: &[RuleGroup<'_>],
412449679
    config: RewriteConfig,
412449679
) -> usize {
412449679
    if start_level >= rule_groups.len() || !arena.is_reachable(node_id) {
609
        return rule_groups.len();
412449070
    }
412449070
    rule_groups
412449070
        .iter()
412449070
        .enumerate()
412449070
        .skip(start_level)
5512709653
        .find_map(|(level, rule_group)| {
5512709653
            rule_group
5512709653
                .has_candidates_at_node(config, arena, node_id)
5512709653
                .then_some(level)
5512709653
        })
412449070
        .unwrap_or(rule_groups.len())
412449679
}
struct RuleEffectImpact {
    added_names: Vec<Name>,
    changed_names: Vec<Name>,
    invalidated_reference_names: Vec<Name>,
    has_new_top: bool,
    has_new_sat_decisions: bool,
}
impl RuleEffectImpact {
64704822
    fn new(
64704822
        effect: &crate::rule_engine::rule::RuleEffect,
64704822
        symbols: &crate::ast::SymbolTable,
64704822
    ) -> Self {
64704822
        let mut changed_names: Vec<_> = effect
64704822
            .changed_symbols(symbols)
64704822
            .into_iter()
            .map(|(name, _, _)| name)
            .collect();
64704822
        for name in effect.updated_declaration_names() {
10527
            if !changed_names.contains(&name) {
10527
                changed_names.push(name);
            }
        }
64704822
        let added_names: Vec<_> = effect.added_symbols(symbols).into_iter().collect();
        // A new auxiliary stands in for the declaration it was derived from, so expressions
        // referencing that source have to be reconsidered alongside the ones that changed.
64704822
        let mut invalidated_reference_names = changed_names.clone();
64704822
        for added_name in &added_names {
623464612
            let Some(declaration) = effect.symbols.lookup_local(added_name) else {
                continue;
            };
623464612
            let mut seen_sources = HashSet::new();
623464612
            let mut source = declaration.source().clone();
661325608
            while let Some(source_declaration) = source.take() {
37860996
                let source_name = source_declaration.name().clone();
37860996
                if !seen_sources.insert(source_name.clone()) {
                    break;
37860996
                }
37860996
                if !invalidated_reference_names.contains(&source_name) {
7035695
                    invalidated_reference_names.push(source_name);
30825301
                }
37860996
                source.clone_from(&source_declaration.source());
            }
        }
64704822
        Self {
64704822
            added_names,
64704822
            changed_names,
64704822
            invalidated_reference_names,
64704822
            has_new_top: !effect.new_top.is_empty(),
64704822
            has_new_sat_decisions: !effect.new_sat_decisions.is_empty(),
64704822
        }
64704822
    }
64704822
    fn has_model_side_effects(&self) -> bool {
64704822
        self.has_new_top || self.has_new_sat_decisions || self.has_symbol_changes()
64704822
    }
99471311
    fn has_symbol_changes(&self) -> bool {
99471311
        !self.added_names.is_empty() || !self.changed_names.is_empty()
99471311
    }
}
struct RewritePassContext<'ctx, 'rules, O> {
    rules_grouped: &'ctx Vec<(u16, Vec<RuleData<'rules>>)>,
    bucketed_rules: &'ctx Vec<RuleGroup<'rules>>,
    stats: &'ctx mut RewriterStats,
    dirty_trace: &'ctx mut DirtyTrace,
    config: RewriteConfig,
    attempt_observer: &'ctx mut O,
}
struct TracingObserver<'run> {
    run_start: &'run Instant,
    general_enabled: bool,
    csv_enabled: bool,
}
impl RuleAttemptObserver for TracingObserver<'_> {
4899128
    fn attempted(
4899128
        &mut self,
4899128
        priority: u16,
4899128
        rule: &RuleData<'_>,
4899128
        expression: &Expr,
4899128
        status: RuleAttemptStatus,
4899128
    ) {
4899128
        let elapsed_s = self.run_start.elapsed().as_secs_f64();
4899128
        if self.general_enabled {
4899128
            trace!(
                target: "conjure::rule_attempt",
                elapsed_s,
4899128
                rule_level = priority,
4899128
                rule_name = rule.rule.name,
4899128
                rule_set = rule.rule_set.name,
4899128
                status = status.as_str(),
4899128
                expression = %expression,
4899128
                "attempted rule"
            );
        }
4899128
        if self.csv_enabled {
1440124
            let expression = expression.to_string();
1440124
            trace!(
                target: "rule_engine_rule_attempt_trace",
1440124
                "{elapsed_s:.3},{priority},{},{},{},{}",
1440124
                csv_escape(rule.rule.name),
1440124
                csv_escape(rule.rule_set.name),
1440124
                status.as_str(),
1440124
                csv_escape(&expression),
            );
3459004
        }
4899128
    }
}
/// True when a domain still needs Essence→Essence' abstract representation
/// (set/tuple/record/…); concrete bool/int and matrices of those do not.
4925978
fn domain_needs_abstract_repr(domain: &crate::ast::DomainPtr) -> bool {
4925978
    domain_needs_abstract_repr_at(domain, true)
4925978
}
/// `is_value` is false for a matrix's index domains, which describe which entries exist rather
/// than a value the solver assigns, and so are never encoded.
4926529
fn domain_needs_abstract_repr_at(domain: &crate::ast::DomainPtr, is_value: bool) -> bool {
    use crate::ast::{Domain, GroundDomain, UnresolvedDomain};
4926529
    let int_is_abstract = is_value && crate::settings::ints_need_representation();
4926529
    match domain.as_ref() {
4913685
        Domain::Ground(gd) => match gd.as_ref() {
3630203
            GroundDomain::Empty(..) | GroundDomain::Bool => false,
692383
            GroundDomain::Int(_, _) => int_is_abstract,
            // Every matrix has a layout to choose between, so representation selection has work
            // to do whatever the elements are.
163936
            GroundDomain::Matrix(..) => true,
427163
            _ => true,
        },
12844
        Domain::Unresolved(ud) => match ud.as_ref() {
10405
            UnresolvedDomain::Int(..) => int_is_abstract,
1221
            UnresolvedDomain::Matrix(..) => true,
551
            UnresolvedDomain::Reference(re) => re
551
                .domain()
551
                .is_some_and(|d| domain_needs_abstract_repr_at(&d, is_value)),
667
            _ => true,
        },
    }
4926529
}
/// Whether `ReprGeneral` / `ReprTuplePacked` can do useful work on this model.
///
/// Models that only use bools, ints, and matrices of those (e.g. solitaire_battleship)
/// still paid hundreds of thousands of failed attempts on record/tuple/set rules.
1351539
fn model_needs_abstract_repr_rules(model: &Model) -> bool {
    use crate::ast::{AbstractLiteral, Atom, Expression as Expr, Literal};
1351539
    for (_, decl) in model.symbols().iter_local() {
4934649
        if let Some(domain) = decl.domain()
4925978
            && domain_needs_abstract_repr(&domain)
        {
1266533
            return true;
3668116
        }
    }
    // Record/tuple/set *literals* also need ReprGeneral even without abstract finds.
    // Matrix literals are handled by `ReprMatrixComponents`, not these rule sets.
4953199
    model.root().any_expression(|expr| match expr {
660773
        Expr::AbstractLiteral(_, abs) => !matches!(abs, AbstractLiteral::Matrix(..)),
        Expr::Atomic(_, Atom::Literal(Literal::AbstractLiteral(abs))) => {
            !matches!(abs, AbstractLiteral::Matrix(..))
        }
4292426
        _ => false,
4953199
    })
1351539
}
/// Rewrites a model by applying rules in priority order, trying enclosing expressions before their
/// descendants at each priority.
1351539
pub fn rewrite_model<'a>(
1351539
    model: &Model,
1351539
    rule_sets: &Vec<&'a RuleSet<'a>>,
1351539
    config: RewriteConfig,
1351539
) -> Result<Model, RewriteError> {
1351539
    let _uniform_scope = crate::representation::uniform::Scope::new();
1351539
    if crate::settings::channelling() == crate::settings::Channelling::Uniform {
1302104
        for (_, decl) in model.symbols().iter_local() {
22618790
            if let Some(dom) = decl.domain() {
22610177
                for (_, state) in decl.reprs().iter() {
204479
                    crate::representation::uniform::check_choice(&dom, state.rule())
                        .map_err(|e| RewriteError::UniformRepresentation(e.to_string()))?;
204479
                    crate::representation::uniform::record_choice(&dom, state.rule());
                }
8613
            }
        }
49435
    }
1351539
    set_current_rewriter(Rewriter::Rewrite(config));
1351539
    let needs_abstract_repr = model_needs_abstract_repr_rules(model);
1351539
    let filtered_rule_sets: Vec<&'a RuleSet<'a>> = if needs_abstract_repr {
1267229
        rule_sets.clone()
    } else {
84310
        rule_sets
84310
            .iter()
84310
            .copied()
926761
            .filter(|rs| rs.name != "ReprGeneral" && rs.name != "ReprTuplePacked")
            .collect()
    };
1351539
    let rules_grouped = get_rules_grouped(&filtered_rule_sets)
        .unwrap_or_else(|_| bug!("get_rule_priorities() failed!"))
        .into_iter()
        .collect_vec();
1351539
    let bucketed_rules = rules_grouped
1351539
        .iter()
73735235
        .map(|(priority, rules)| RuleGroup::new(*priority, rules.clone()))
        .collect_vec();
1351539
    let mut model = introduce_objective_auxiliary(model.clone());
8776783
    if rule_sets.iter().any(|rules| rules.name == "SAT")
1284668
        && let Some(objective) = &model.objective
20580
        && !model
20580
            .sat_decisions()
20580
            .iter()
            .any(|decision| matches!(decision, crate::ast::SatEncodingDecision::Objective { .. }))
20580
        && !model
20580
            .root()
326296
            .any_expression(|expression| matches!(expression, Expr::SatObjective(..)))
    {
20580
        model.add_constraint(Expr::SatObjective(
20580
            crate::ast::Metadata::new(),
20580
            matches!(
20580
                objective.direction,
                crate::ast::OptimiseDirection::Minimising
            ),
20580
            crate::ast::Moo::new(objective.expression.clone()),
        ));
1330959
    }
1351539
    tighten_domains_from_constraints(&mut model);
1351539
    let mut rewriter_stats = RewriterStats::new();
1351539
    rewriter_stats.is_optimisation_enabled = Some(!config.is_baseline());
1351539
    let mut dirty_trace = DirtyTrace::from_env();
1351539
    let run_start = Instant::now();
1351539
    if rule_trace_enabled() && default_rule_trace_enabled() {
45740
        trace!(
            target: "rule_engine_rule_trace",
            "Model before rewriting:\n\n{}\n--\n",
45740
            model
        );
1305799
    }
1351539
    if rule_trace_enabled() && rule_attempt_trace_enabled() {
16
        trace!(
            target: "rule_engine_rule_attempt_trace",
16
            "elapsed_s,rule_level,rule_name,rule_set,status,expression"
        );
1351523
    }
1351539
    let general_attempt_trace_enabled = tracing::enabled!(
        target: "conjure::rule_attempt",
1351539
        tracing::Level::TRACE
    );
1351539
    let csv_attempt_trace_enabled = rule_trace_enabled() && rule_attempt_trace_enabled();
1351539
    if general_attempt_trace_enabled || csv_attempt_trace_enabled {
312
        let mut observer = TracingObserver {
312
            run_start: &run_start,
312
            general_enabled: general_attempt_trace_enabled,
312
            csv_enabled: csv_attempt_trace_enabled,
312
        };
312
        rewrite_to_fixpoint(
312
            &mut model,
312
            &rules_grouped,
312
            &bucketed_rules,
312
            &mut rewriter_stats,
312
            &mut dirty_trace,
312
            config,
312
            &mut observer,
312
        );
    } else {
1351227
        let mut observer = NoopObserver;
1351227
        rewrite_to_fixpoint(
1351227
            &mut model,
1351227
            &rules_grouped,
1351227
            &bucketed_rules,
1351227
            &mut rewriter_stats,
1351227
            &mut dirty_trace,
1351227
            config,
1351227
            &mut observer,
1351227
        );
    }
1351539
    let run_end = Instant::now();
1351539
    rewriter_stats.rewriter_run_time = Some(run_end - run_start);
1351539
    model
1351539
        .context
1351539
        .write()
1351539
        .unwrap()
1351539
        .stats
1351539
        .add_rewriter_run(rewriter_stats);
1351539
    dirty_trace.finish(
1351539
        model
1351539
            .context
1351539
            .read()
1351539
            .unwrap()
1351539
            .stats
1351539
            .rewriter_runs
1351539
            .last()
1351539
            .expect("rewriter stats were just added"),
1351539
    );
1351539
    super::encoding_selection::resolve_encoding_choices(model.sat_decisions_mut());
1351539
    if rule_trace_enabled() && default_rule_trace_enabled() {
45740
        trace!(
            target: "rule_engine_rule_trace",
            "Final model:\n\n{}",
45740
            model
        );
1305799
    }
    // Flatten top-level `and` into the root constraint list (and strip `true` / propagate
    // `false`) after rewriting. Must not run mid-loop: early flattening of large expanded
    // conjunctions explodes worklist rule attempts. Solver adaptors expect one flat constraint
    // per root entry.
1351539
    if let Some(normalised_root) = finish_root_evaluator_normalisation(model.root()) {
291707
        model.replace_root(normalised_root);
1059832
    }
1351539
    if crate::settings::channelling() == crate::settings::Channelling::Uniform {
1302104
        for (_, decl) in model.symbols().iter_local() {
645049882
            if decl.as_find().is_some()
643905368
                && decl.reprs().is_empty()
638645137
                && let Some(dom) = decl.domain()
638645137
                && let Some(selected) = crate::representation::uniform::selected(&dom, false)
43906
                && selected.probe_for(decl).is_err()
            {
                return Err(RewriteError::UniformRepresentation(format!(
                    "uniform representation `{}` does not support `{}` with domain `{}`",
                    selected.name(),
                    decl.name(),
                    dom,
                )));
645049882
            }
        }
49435
    }
1351539
    Ok(model)
1351539
}
#[allow(clippy::too_many_arguments)]
1351539
fn rewrite_to_fixpoint<'rules, O: RuleAttemptObserver>(
1351539
    model: &mut Model,
1351539
    rules_grouped: &Vec<(u16, Vec<RuleData<'rules>>)>,
1351539
    bucketed_rules: &Vec<RuleGroup<'rules>>,
1351539
    stats: &mut RewriterStats,
1351539
    dirty_trace: &mut DirtyTrace,
1351539
    config: RewriteConfig,
1351539
    attempt_observer: &mut O,
1351539
) {
1351539
    let mut pass_ctx = RewritePassContext {
1351539
        rules_grouped,
1351539
        bucketed_rules,
1351539
        stats,
1351539
        dirty_trace,
1351539
        config,
1351539
        attempt_observer,
1351539
    };
1351539
    if config.worklist {
1351485
        let _ = try_rewrite_model(model, &mut pass_ctx);
    } else {
54
        let mut done_something = true;
161
        while done_something {
107
            done_something = try_rewrite_model(model, &mut pass_ctx).is_some();
        }
    }
1351539
}
// Tries to rewrite the model until a full scan finds no applicable rules.
//
// Returns None if no change was made.
1351592
fn try_rewrite_model<'ctx, 'rules, O: RuleAttemptObserver>(
1351592
    submodel: &mut Model,
1351592
    ctx: &mut RewritePassContext<'ctx, 'rules, O>,
1351592
) -> Option<()> {
1351592
    ctx.dirty_trace.passes += 1;
1351592
    if !ctx.config.worklist
107
        && try_rewrite_value_letting_once(submodel, ctx.rules_grouped, ctx.attempt_observer)
107
            .is_some()
    {
        ctx.dirty_trace.value_letting_rewrites += 1;
        increment_counter(&mut ctx.stats.rewriter_value_letting_rewrites);
        return Some(());
1351592
    }
1351592
    let mut did_rewrite = false;
1351592
    let mut arena = ExpressionArena::from_root(take_model_root(submodel));
1351592
    normalise_evaluators_bottom_up(&mut arena, ctx.dirty_trace);
1351592
    if ctx.config.worklist {
1351485
        return try_rewrite_model_with_worklist(submodel, ctx, arena);
107
    }
    'rewrite_loop: loop {
571
        let mut results: Vec<ApplicableRule<'_, ExpressionNodeId>> = vec![];
571
        let preorder_ids = rewriter_preorder_ids(&arena);
        // Iterate over rules by priority in descending order.
571
        'top: for (level, rule_group) in ctx.bucketed_rules.iter().enumerate() {
21151
            ctx.dirty_trace.priority_scans += 1;
21151
            for &node_id in &preorder_ids {
456555
                ctx.dirty_trace.expression_visits += 1;
456555
                let mut attempted_rule = false;
                {
456555
                    let expr = arena.expression(node_id);
456555
                    for rd in rule_group.candidates(ctx.config, expr) {
2360767
                        attempted_rule = true;
2360767
                        ctx.dirty_trace
2360767
                            .record_rule_attempt(rule_group.priority, rd.rule.name);
                        // Count rule application attempts
2360767
                        ctx.stats.rewriter_rule_application_attempts =
2360767
                            Some(ctx.stats.rewriter_rule_application_attempts.unwrap_or(0) + 1);
2360767
                        match rd.rule.apply(expr, &submodel.symbols()) {
                            Ok(red) => {
464
                                ctx.attempt_observer.attempted(
464
                                    rule_group.priority,
464
                                    rd,
464
                                    expr,
464
                                    RuleAttemptStatus::Success,
464
                                );
                                // Count successful rule applications
464
                                ctx.stats.rewriter_rule_applications =
464
                                    Some(ctx.stats.rewriter_rule_applications.unwrap_or(0) + 1);
                                // Collect applicable rules
464
                                results.push((
464
                                    RuleResult {
464
                                        rule_data: rd.clone(),
464
                                        effect: red,
464
                                    },
464
                                    level,
464
                                    expr.clone(),
464
                                    node_id,
464
                                    root_variable_snapshot_for_default_trace(
464
                                        expr,
464
                                        &submodel.symbols(),
464
                                    ),
464
                                ));
                            }
                            Err(_) => {
2360303
                                ctx.attempt_observer.attempted(
2360303
                                    rule_group.priority,
2360303
                                    rd,
2360303
                                    expr,
2360303
                                    RuleAttemptStatus::Failure,
2360303
                                );
                            }
                        }
                    }
                }
456555
                if attempted_rule {
456555
                    ctx.dirty_trace.attempted_expressions += 1;
                }
                // This expression has the highest rule priority so far, so this is what we want to
                // rewrite.
456555
                if !results.is_empty() {
464
                    break 'top;
456091
                }
            }
        }
571
        if !results.is_empty() {
            let selected =
464
                choose_rule_result_index(results.iter().map(|(result, _, _, _, _)| result));
464
            results.swap(0, selected);
107
        }
571
        match results.into_iter().next() {
            None => {
107
                submodel.replace_root(arena.into_synced_root_expression());
107
                break;
            }
            Some((result, _level, expr, node_id, variable_snapshot_before)) => {
464
                let effect = result.effect.materialise(&submodel.symbols());
464
                let variable_snapshots = variable_snapshot_before.map(|before| {
                    let after = snapshot_symbols_after_effect(&submodel.symbols(), &effect);
                    (before, after)
                });
464
                let result = RuleResult {
464
                    rule_data: result.rule_data,
464
                    effect,
464
                };
                // Extract the single applicable rule and apply it
464
                log_rule_application(
464
                    &result,
464
                    &expr,
464
                    &submodel.symbols(),
464
                    variable_snapshots
464
                        .as_ref()
                        .map(|(before, after)| (before, after)),
                );
464
                let effect_impact = RuleEffectImpact::new(&result.effect, &submodel.symbols());
464
                let has_model_side_effects = effect_impact.has_model_side_effects();
464
                let rule_name = result.rule_data.rule.name;
464
                let RuleResult { effect, .. } = result;
                let crate::rule_engine::rule::RuleEffect {
                    new_expression,
                    new_top,
                    symbols,
                    new_sat_decisions,
                    declaration_updates,
                    ..
464
                } = effect;
                // Replace expr with new_expression
464
                replace_focus_and_sync_ancestors(&mut arena, node_id, new_expression);
                // Apply new symbols and top level
464
                ctx.dirty_trace
464
                    .record_rewrite(rule_name, has_model_side_effects);
464
                for update in declaration_updates {
                    update.apply();
                }
464
                submodel.symbols_mut().extend(symbols);
464
                if effect_impact.has_new_top {
48
                    arena.add_root_children(new_top);
416
                }
464
                submodel.add_sat_decisions(new_sat_decisions);
                let _ =
464
                    normalise_evaluators_from_node_to_root(&mut arena, node_id, ctx.dirty_trace);
464
                if has_model_side_effects {
72
                    ctx.dirty_trace.record_side_effect_kept_in_arena();
392
                }
                #[cfg(debug_assertions)]
                {
                    // Check well-formedness without rebuilding the live arena: a rebuild would
                    // renumber nodes and can change subsequent full-scan order vs release builds.
464
                    submodel.replace_root(arena.expression(arena.root()).clone());
464
                    let assertion_context = format!("rewriter after applying rule '{rule_name}'");
464
                    debug_assert_model_well_formed(submodel, &assertion_context);
                }
464
                did_rewrite = true;
464
                continue 'rewrite_loop;
            }
        }
    }
107
    did_rewrite.then_some(())
1351592
}
1351485
fn try_rewrite_model_with_worklist<'ctx, 'rules, O: RuleAttemptObserver>(
1351485
    submodel: &mut Model,
1351485
    ctx: &mut RewritePassContext<'ctx, 'rules, O>,
1351485
    arena: ExpressionArena,
1351485
) -> Option<()> {
1351485
    let mut did_rewrite = false;
1351485
    let root_surface = 0usize;
1351485
    let (mut surfaces, mut value_letting_surfaces) = build_worklist_surfaces(submodel, arena);
1351485
    for surface in &mut surfaces {
2114747
        if surface.active {
2114747
            normalise_evaluators_bottom_up(&mut surface.arena, ctx.dirty_trace);
        }
    }
1351485
    write_worklist_surfaces_to_model(submodel, &surfaces);
1351485
    let mut scheduler = WorklistScheduler::new(&surfaces, ctx.bucketed_rules, ctx.config);
1351485
    let mut deferred_evaluators = DeferredEvaluatorWork::default();
1351485
    let mut deferred_ancestor_rules = DeferredAncestorRuleWork::default();
    loop {
        let next = loop {
            let candidate =
2816680897
                scheduler.pop_next(&surfaces, ctx.bucketed_rules, ctx.config, ctx.dirty_trace);
2816680897
            if let Some((_, surface, node_id, _)) = candidate
2815259976
                && deferred_ancestor_rules.contains(surface, node_id)
            {
35136746
                deferred_ancestor_rules.postpone(candidate.unwrap());
35136746
                continue;
2781544151
            }
2781544151
            break candidate;
        };
        let crosses_priority_boundary =
2781544151
            deferred_ancestor_rules.should_flush_before(next.as_ref().map(|item| item.0));
2781544151
        if crosses_priority_boundary {
            // `pop_next` removes a live item from the queue. Put it back before injecting the
            // higher-priority ancestor checks that must precede it.
25836559
            if let Some((level, surface, node_id, mode)) = next {
25767135
                scheduler.enqueue_node_at_level(
25767135
                    &surfaces[surface].arena,
25767135
                    surface,
25767135
                    node_id,
25767135
                    level,
25767135
                    mode,
25767135
                    Some(ctx.dirty_trace),
25767135
                );
69424
            }
25836559
            deferred_ancestor_rules.flush(
25836559
                &mut scheduler,
25836559
                &mut surfaces,
25836559
                &mut deferred_evaluators,
25836559
                ctx.dirty_trace,
25836559
            );
25836559
            continue;
2755707592
        }
2755707592
        let Some((level, surface_index, node_id, scheduled_mode)) = next else {
1351497
            if normalise_deferred_evaluators(
1351497
                &mut deferred_evaluators,
1351497
                submodel,
1351497
                &mut surfaces,
1351497
                &mut scheduler,
1351497
                ctx.dirty_trace,
1351497
            ) {
12
                did_rewrite = true;
12
                continue;
1351485
            }
1351485
            break;
        };
2754356095
        ctx.dirty_trace.priority_scans += 1;
2754356095
        ctx.dirty_trace.expression_visits += 1;
2754356095
        let rule_group = &ctx.bucketed_rules[level];
2754356095
        if !rule_group.has_candidates_at_node(ctx.config, &surfaces[surface_index].arena, node_id) {
1398605707
            ctx.dirty_trace
1398605707
                .record_worklist_no_candidate_pop(scheduled_mode);
1398605707
            scheduler.enqueue_after_no_rewrite(
1398605707
                WorklistSchedulingContext::new(
1398605707
                    &surfaces[surface_index].arena,
1398605707
                    surface_index,
1398605707
                    ctx.bucketed_rules,
1398605707
                    ctx.config,
1398605707
                ),
1398605707
                node_id,
1398605707
                level,
1398605707
                level + 1,
1398605707
                scheduled_mode,
1398605707
                Some(ctx.dirty_trace),
1398605707
            );
1398605707
            continue;
1355750388
        }
1355750388
        let mut results: Vec<ApplicableRule<'_, ExpressionNodeId>> = vec![];
1355750388
        let mut attempted_rule = false;
        {
1355750388
            let arena = &surfaces[surface_index].arena;
1355750388
            let expr = arena.expression(node_id);
1355750388
            for rd in rule_group.candidates_at_node(ctx.config, arena, node_id) {
2696141679
                if !scheduler.should_attempt_rule(surface_index, node_id, rd) {
12257222
                    continue;
2683884457
                }
2683884457
                attempted_rule = true;
2683884457
                ctx.dirty_trace
2683884457
                    .record_rule_attempt(rule_group.priority, rd.rule.name);
2683884457
                ctx.stats.rewriter_rule_application_attempts =
2683884457
                    Some(ctx.stats.rewriter_rule_application_attempts.unwrap_or(0) + 1);
2683884457
                match rd.rule.apply(expr, &submodel.symbols()) {
                    Ok(red) => {
64708824
                        ctx.attempt_observer.attempted(
64708824
                            rule_group.priority,
64708824
                            rd,
64708824
                            expr,
64708824
                            RuleAttemptStatus::Success,
64708824
                        );
64708824
                        ctx.stats.rewriter_rule_applications =
64708824
                            Some(ctx.stats.rewriter_rule_applications.unwrap_or(0) + 1);
64708824
                        results.push((
64708824
                            RuleResult {
64708824
                                rule_data: rd.clone(),
64708824
                                effect: red,
64708824
                            },
64708824
                            level,
64708824
                            expr.clone(),
64708824
                            node_id,
64708824
                            root_variable_snapshot_for_default_trace(expr, &submodel.symbols()),
64708824
                        ));
                    }
                    Err(_) => {
2619175633
                        scheduler.record_rule_failure(surface_index, node_id, rd);
2619175633
                        ctx.attempt_observer.attempted(
2619175633
                            rule_group.priority,
2619175633
                            rd,
2619175633
                            expr,
2619175633
                            RuleAttemptStatus::Failure,
2619175633
                        );
                    }
                }
            }
        }
1355750388
        if attempted_rule {
1343493166
            ctx.dirty_trace.attempted_expressions += 1;
1343493166
            ctx.dirty_trace
1343493166
                .record_worklist_rule_attempt_pop(scheduled_mode);
12257222
        }
1355750388
        if results.is_empty() {
1291046030
            scheduler.enqueue_after_no_rewrite(
1291046030
                WorklistSchedulingContext::new(
1291046030
                    &surfaces[surface_index].arena,
1291046030
                    surface_index,
1291046030
                    ctx.bucketed_rules,
1291046030
                    ctx.config,
1291046030
                ),
1291046030
                node_id,
1291046030
                level,
1291046030
                level + 1,
1291046030
                scheduled_mode,
1291046030
                Some(ctx.dirty_trace),
1291046030
            );
1291046030
            continue;
64704358
        }
64704358
        let selected = choose_rule_result_index(results.iter().map(|(result, _, _, _, _)| result));
        let (result, _level, expr, node_id, variable_snapshot_before) =
64704358
            results.swap_remove(selected);
64704358
        let effect = result.effect.materialise(&submodel.symbols());
64704358
        let variable_snapshots = variable_snapshot_before.map(|before| {
41005
            let after = snapshot_symbols_after_effect(&submodel.symbols(), &effect);
41005
            (before, after)
41005
        });
64704358
        let result = RuleResult {
64704358
            rule_data: result.rule_data,
64704358
            effect,
64704358
        };
64704358
        log_rule_application(
64704358
            &result,
64704358
            &expr,
64704358
            &submodel.symbols(),
64704358
            variable_snapshots
64704358
                .as_ref()
41005
                .map(|(before, after)| (before, after)),
        );
64704358
        let effect_impact = RuleEffectImpact::new(&result.effect, &submodel.symbols());
64704358
        let has_model_side_effects = effect_impact.has_model_side_effects();
        let rewritten_value_letting_name =
64704358
            value_letting_surface_name(&surfaces[surface_index].kind).cloned();
        let invalidates_symbol_rules =
64704358
            effect_impact.has_symbol_changes() || rewritten_value_letting_name.is_some();
64704358
        let rule_name = result.rule_data.rule.name;
64704358
        let RuleResult { effect, .. } = result;
        let crate::rule_engine::rule::RuleEffect {
            new_expression,
            new_top,
            symbols,
            new_sat_decisions,
            declaration_updates,
            ..
64704358
        } = effect;
        {
64704358
            let arena = &mut surfaces[surface_index].arena;
64704358
            arena.replace_subtree(node_id, new_expression);
        }
64704358
        ctx.dirty_trace
64704358
            .record_rewrite(rule_name, has_model_side_effects);
64704358
        for update in declaration_updates {
10527
            update.apply();
        }
64704358
        submodel.symbols_mut().extend(symbols);
64704358
        let new_top_node_ids = if effect_impact.has_new_top {
3872077
            surfaces[root_surface].arena.add_root_children(new_top)
        } else {
60832281
            Vec::new()
        };
64704358
        submodel.add_sat_decisions(new_sat_decisions);
        let rewrite_impact_node_id = {
64704358
            let arena = &mut surfaces[surface_index].arena;
64704358
            normalise_evaluators_subtree_bottom_up(arena, node_id, ctx.dirty_trace);
64704358
            deferred_evaluators.defer_ancestors(arena, surface_index, node_id);
64704358
            deferred_ancestor_rules.defer_ancestors(arena, surface_index, node_id, level);
64704358
            node_id
        };
64704358
        scheduler.index_subtree_references(
64704358
            &surfaces[surface_index].arena,
64704358
            surface_index,
64704358
            rewrite_impact_node_id,
64704358
        );
64704358
        for &new_top_node_id in &new_top_node_ids {
11855698
            if surfaces[root_surface].arena.is_reachable(new_top_node_id) {
11855698
                normalise_evaluators_subtree_bottom_up(
11855698
                    &mut surfaces[root_surface].arena,
11855698
                    new_top_node_id,
11855698
                    ctx.dirty_trace,
11855698
                );
11855698
                deferred_evaluators.defer_ancestors(
11855698
                    &surfaces[root_surface].arena,
11855698
                    root_surface,
11855698
                    new_top_node_id,
11855698
                );
11855698
                deferred_ancestor_rules.defer_ancestors(
11855698
                    &surfaces[root_surface].arena,
11855698
                    root_surface,
11855698
                    new_top_node_id,
11855698
                    level,
11855698
                );
11855698
                scheduler.index_subtree_references(
11855698
                    &surfaces[root_surface].arena,
11855698
                    root_surface,
11855698
                    new_top_node_id,
11855698
                );
            }
        }
64704358
        if let Some(name) = rewritten_value_letting_name.as_ref() {
10080
            write_value_letting_surface_to_model(submodel, name, &surfaces[surface_index].arena);
10080
            ctx.dirty_trace.value_letting_rewrites += 1;
10080
            increment_counter(&mut ctx.stats.rewriter_value_letting_rewrites);
64694278
        }
64704358
        let mut affected_names = effect_impact.invalidated_reference_names.clone();
64704358
        if let Some(name) = rewritten_value_letting_name.as_ref()
10080
            && !affected_names.contains(name)
        {
10080
            affected_names.push(name.clone());
64694278
        }
64704358
        let mut symbol_surface_names = effect_impact.added_names.clone();
64704358
        symbol_surface_names.extend(effect_impact.changed_names.iter().cloned());
64704358
        if let Some(name) = rewritten_value_letting_name.as_ref() {
10080
            symbol_surface_names.retain(|candidate| candidate != name);
64694278
        }
64704358
        let synced_surfaces = sync_value_letting_surfaces(
64704358
            submodel,
64704358
            &mut surfaces,
64704358
            &mut value_letting_surfaces,
64704358
            &symbol_surface_names,
64704358
        );
        // Choosing a representation, or any other change to the model, can change which rule
        // should win for a sibling at the same priority. Do not batch across that: bring the
        // enclosing expressions up to date and re-check their rules before moving to another
        // sibling. Rewrites that only change an expression still get batched, and those are the
        // common case when unrolling.
64704358
        if has_model_side_effects {
30750257
            deferred_ancestor_rules.flush_effect_path_before_root(
30750257
                &mut scheduler,
30750257
                &mut surfaces,
30750257
                &mut deferred_evaluators,
30750257
                surface_index,
30750257
                rewrite_impact_node_id,
30750257
                ctx.dirty_trace,
30750257
            );
33954101
        }
        {
64704358
            if has_model_side_effects {
30750257
                ctx.dirty_trace.record_side_effect_kept_in_arena();
33954101
            }
64704358
            if invalidates_symbol_rules {
17344659
                scheduler.invalidate_symbol_rule_failures();
17344659
                scheduler.enqueue_node_at_level(
17344659
                    &surfaces[root_surface].arena,
17344659
                    root_surface,
17344659
                    surfaces[root_surface].arena.root(),
17344659
                    0,
17344659
                    ScheduledMode::CheckNode,
17344659
                    Some(ctx.dirty_trace),
17344659
                );
47359699
            }
64704358
            scheduler.enqueue_subtree(
64704358
                &surfaces[surface_index].arena,
64704358
                surface_index,
64704358
                rewrite_impact_node_id,
64704358
                Some(ctx.dirty_trace),
64704358
            );
64704358
            for new_top_node_id in new_top_node_ids {
11855698
                scheduler.enqueue_subtree(
11855698
                    &surfaces[root_surface].arena,
11855698
                    root_surface,
11855698
                    new_top_node_id,
11855698
                    Some(ctx.dirty_trace),
11855698
                );
            }
64704358
            for synced_surface in synced_surfaces {
523848
                scheduler.enqueue_surface(
523848
                    &surfaces,
523848
                    synced_surface,
523848
                    ctx.bucketed_rules,
523848
                    ctx.config,
523848
                    Some(ctx.dirty_trace),
523848
                );
            }
64704358
            enqueue_worklist_nodes_referencing_names(
64704358
                &mut scheduler,
64704358
                &surfaces,
64704358
                &affected_names,
64704358
                ctx.dirty_trace,
64704358
            );
        }
64704358
        did_rewrite = true;
    }
1351485
    move_worklist_surfaces_to_model(submodel, surfaces);
    #[cfg(debug_assertions)]
1351485
    debug_assert_model_well_formed(submodel, "rewriter after a settled worklist pass");
1351485
    did_rewrite.then_some(())
1351485
}
#[cfg(test)]
1
fn enqueue_worklist_rewrite_impact(
1
    scheduler: &mut WorklistScheduler,
1
    arena: &ExpressionArena,
1
    surface: usize,
1
    node_id: ExpressionNodeId,
1
    dirty_trace: &mut DirtyTrace,
1
) {
1
    scheduler.enqueue_node_and_ancestors(arena, surface, node_id, dirty_trace);
1
    scheduler.enqueue_subtree(arena, surface, node_id, Some(dirty_trace));
1
}
/// Rebuilds and evaluates the waiting ancestors, once the worklist has run out of rule work.
///
/// Nodes are handled deepest first, so each parent is rebuilt once from children that are already
/// up to date. Doing it here rather than after every rewrite is what stops each rewrite from
/// copying the whole matrix/`and`/root chain above it. The surfaces that changed go back on the
/// worklist, and later rewrites fill the next batch.
1351498
fn normalise_deferred_evaluators(
1351498
    work: &mut DeferredEvaluatorWork,
1351498
    submodel: &mut Model,
1351498
    surfaces: &mut [RewriteSurface],
1351498
    scheduler: &mut WorklistScheduler,
1351498
    dirty_trace: &mut DirtyTrace,
1351498
) -> bool {
1351498
    if work.is_empty() {
1351485
        return false;
13
    }
13
    let nodes = deferred_nodes_in_bottom_up_order(std::mem::take(&mut work.nodes), surfaces);
13
    let mut affected_surfaces = HashSet::new();
13
    let mut changed_value_lettings = HashSet::new();
13
    for (surface, node_id, _) in nodes {
4327
        let Some(rewrite_surface) = surfaces.get_mut(surface) else {
            continue;
        };
4327
        if !rewrite_surface.active || !rewrite_surface.arena.is_reachable(node_id) {
2792
            continue;
1535
        }
1535
        affected_surfaces.insert(surface);
1535
        rewrite_surface.arena.rebuild_payload_from_children(node_id);
        // Root-list reshaping is deliberately left to the final evaluator pass.
1535
        if node_id != rewrite_surface.arena.root()
2
            && normalise_evaluator_node_to_fixpoint_without_ancestor_sync(
2
                &mut rewrite_surface.arena,
2
                node_id,
2
                dirty_trace,
2
            )
        {
1
            scheduler.index_subtree_references(&rewrite_surface.arena, surface, node_id);
1
            if let Some(name) = value_letting_surface_name(&rewrite_surface.kind) {
                changed_value_lettings.insert(name.clone());
1
            }
1534
        }
    }
13
    let mut wrote_value_letting = false;
13
    let mut changed_value_lettings: Vec<_> = changed_value_lettings.into_iter().collect();
13
    changed_value_lettings.sort();
13
    for name in &changed_value_lettings {
        let Some(surface) = surfaces.iter().find(|surface| {
            surface.active && value_letting_surface_name(&surface.kind) == Some(name)
        }) else {
            continue;
        };
        wrote_value_letting |=
            write_value_letting_surface_to_model_without_refresh(submodel, name, &surface.arena);
    }
13
    if wrote_value_letting {
        submodel.symbols_mut().refresh_local_binding_hashes();
        scheduler.invalidate_symbol_rule_failures();
        scheduler.enqueue_node_at_level(
            &surfaces[0].arena,
            0,
            surfaces[0].arena.root(),
            0,
            ScheduledMode::CheckNode,
            Some(dirty_trace),
        );
        enqueue_worklist_nodes_referencing_names(
            scheduler,
            surfaces,
            &changed_value_lettings,
            dirty_trace,
        );
13
    }
13
    let mut affected_surfaces: Vec<_> = affected_surfaces.into_iter().collect();
13
    affected_surfaces.sort_unstable();
13
    for surface in affected_surfaces.iter().copied() {
1533
        let arena = &surfaces[surface].arena;
1533
        scheduler.enqueue_subtree(arena, surface, arena.root(), Some(dirty_trace));
    }
13
    !affected_surfaces.is_empty()
1351498
}
25836572
fn deferred_nodes_in_bottom_up_order(
25836572
    nodes: impl IntoIterator<Item = (usize, ExpressionNodeId)>,
25836572
    surfaces: &[RewriteSurface],
25836572
) -> Vec<(usize, ExpressionNodeId, Option<Vec<usize>>)> {
    // Cache each rarely needed path once. Reconstructing paths inside the comparison closure would
    // turn sorting into O(n log n) parent-chain walks.
25836572
    let mut nodes: Vec<_> = nodes
25836572
        .into_iter()
92863543
        .map(|(surface, node_id)| {
92863543
            let path = surfaces
92863543
                .get(surface)
92863543
                .filter(|rewrite_surface| rewrite_surface.active)
92863543
                .filter(|rewrite_surface| rewrite_surface.arena.is_reachable(node_id))
92860751
                .map(|rewrite_surface| rewrite_surface.arena.preorder_path(node_id));
92863543
            (surface, node_id, path)
92863543
        })
        .collect();
25836572
    nodes.sort_by(
278230526
        |(left_surface, left_node, left_path), (right_surface, right_node, right_path)| {
278230526
            right_path
278230526
                .as_ref()
278230526
                .map_or(0, Vec::len)
278230526
                .cmp(&left_path.as_ref().map_or(0, Vec::len))
72754455
                .then_with(|| left_surface.cmp(right_surface))
72679787
                .then_with(|| match (left_path, right_path) {
72674827
                    (Some(left_path), Some(right_path)) => left_path.cmp(right_path),
4960
                    _ => left_node.cmp(right_node),
72679787
                })
278230526
        },
    );
25836572
    nodes
25836572
}
/// Applies evaluator normalisation throughout an existing arena surface.
///
/// This is the initial half of the evaluator normalisation hook. Evaluation is a privileged pure
/// simplification rather than an ordinary rule: before normal rule scheduling starts, each surface
/// is normalised bottom-up so parent evaluators can assume already-normal children. Comprehensions
/// remain atomic here for the same scoped-rewrite reason as normal scheduler traversal.
3466339
fn normalise_evaluators_bottom_up(arena: &mut ExpressionArena, dirty_trace: &mut DirtyTrace) {
3466339
    normalise_evaluators_subtree_bottom_up(arena, arena.root(), dirty_trace);
3466339
}
80026859
fn normalise_evaluators_subtree_bottom_up(
80026859
    arena: &mut ExpressionArena,
80026859
    subtree_root: ExpressionNodeId,
80026859
    dirty_trace: &mut DirtyTrace,
80026859
) -> bool {
80026859
    let nodes = rewriter_reachable_subtree_ids(arena, subtree_root);
80026859
    let mut changed = false;
80026859
    for node_id in nodes.into_iter().rev() {
1539603163
        if !arena.is_reachable(node_id) {
            continue;
1539603163
        }
1539603163
        changed |=
1539603163
            normalise_evaluator_node_to_fixpoint_without_ancestor_sync(arena, node_id, dirty_trace);
        // Copy the child's new value one level up. Going deepest first means each parent is up
        // to date before its own evaluator runs, without touching wider ancestors every time
        // something folds. Anything above the subtree root is the caller's job: the worklist
        // defers it, the non-worklist path updates it directly.
1539603163
        if node_id != subtree_root
1459576304
            && let Some(parent_id) = arena.parent(node_id)
1459576304
            && arena.is_reachable(parent_id)
        {
1459576304
            arena.sync_payload_for_changed_child(parent_id, node_id);
80026859
        }
    }
80026859
    changed
80026859
}
/// Applies the post-rewrite evaluator hook from `node_id` up to the root.
///
/// Ordinary rules still obey the rewriter invariant that higher priority wins and, within a
/// priority, enclosing expressions are tried before descendants. Evaluators are the explicit
/// exception: after an ordinary rule changes a subtree, local full/partial evaluation is allowed to
/// fire immediately at that node and then at each ancestor because such simplification is always
/// preferable to scheduling another ordinary rule on the unevaluated expression.
464
fn normalise_evaluators_from_node_to_root(
464
    arena: &mut ExpressionArena,
464
    node_id: ExpressionNodeId,
464
    dirty_trace: &mut DirtyTrace,
464
) -> (ExpressionNodeId, bool) {
    // Ordinary rewrites can introduce fresh nested arithmetic whose children were not previously
    // scheduled. Normalise the replacement subtree bottom-up first, then walk upward so evaluator
    // simplification remains privileged without running ordinary rules to a fixpoint.
464
    let subtree_changed = normalise_evaluators_subtree_bottom_up(arena, node_id, dirty_trace);
464
    let mut came_from = node_id;
464
    let mut current = arena.parent(node_id);
464
    let mut highest_rewritten = node_id;
464
    let mut changed = subtree_changed;
1922
    while let Some(current_id) = current {
1458
        if !arena.is_reachable(current_id) {
            break;
1458
        }
1458
        let rewritten = if current_id == arena.root() {
            // Only deep-normalise the root child that contains the rewrite. Full-root selective
            // deep on every upward walk re-traverses every non-flat sibling and dominates CPU on
            // large models (lee-distance profile).
440
            normalise_root_evaluator_for_child(arena, came_from, dirty_trace)
        } else {
1018
            normalise_evaluator_node_to_fixpoint(arena, current_id, dirty_trace)
        };
1458
        if rewritten {
            highest_rewritten = current_id;
            changed = true;
1458
        }
1458
        came_from = current_id;
1458
        current = arena.parent(current_id);
    }
464
    (highest_rewritten, changed)
464
}
/// Deep-normalises only the root constraint that `child_id` belongs to.
///
/// `child_id` must be a direct child of the arena root. Sibling constraints are left untouched.
///
/// The replacement is written to `child_id` rather than to the root. Rebuilding the root here
/// would clone every sibling constraint and re-import the whole model into the arena, which this
/// hook cannot afford: it runs after every rewrite.
440
fn normalise_root_evaluator_for_child(
440
    arena: &mut ExpressionArena,
440
    child_id: ExpressionNodeId,
440
    dirty_trace: &mut DirtyTrace,
440
) -> bool {
440
    let root_id = arena.root();
440
    if !arena.children(root_id).contains(&child_id) {
        return normalise_evaluator_node_to_fixpoint(arena, root_id, dirty_trace);
440
    }
440
    let Some(replacement) = normalise_root_constraint_deep(arena.expression(child_id)) else {
440
        return false;
    };
    dirty_trace.replacement_subtree_clears += 1;
    arena.replace_subtree(child_id, replacement);
    dirty_trace.record_rewrite("evaluator_normalisation_hook", false);
    sync_ancestor_payloads(arena, child_id);
    true
440
}
1018
fn normalise_evaluator_node_to_fixpoint(
1018
    arena: &mut ExpressionArena,
1018
    node_id: ExpressionNodeId,
1018
    dirty_trace: &mut DirtyTrace,
1018
) -> bool {
    let changed =
1018
        normalise_evaluator_node_to_fixpoint_without_ancestor_sync(arena, node_id, dirty_trace);
1018
    if changed {
        sync_ancestor_payloads(arena, node_id);
1018
    }
1018
    changed
1018
}
1663411287
fn normalise_evaluator_node_to_fixpoint_without_ancestor_sync(
1663411287
    arena: &mut ExpressionArena,
1663411287
    node_id: ExpressionNodeId,
1663411287
    dirty_trace: &mut DirtyTrace,
1663411287
) -> bool {
1663411287
    let mut changed = false;
1679842052
    while arena.is_reachable(node_id) {
1679842052
        let expr = arena.expression(node_id);
1679842052
        let Some(replacement) = normalise_evaluator_local(expr) else {
1663411287
            break;
        };
16430765
        dirty_trace.replacement_subtree_clears += 1;
16430765
        arena.replace_subtree(node_id, replacement);
16430765
        dirty_trace.record_rewrite("evaluator_normalisation_hook", false);
16430765
        changed = true;
    }
1663411287
    changed
1663411287
}
1351485
fn build_worklist_surfaces(
1351485
    submodel: &Model,
1351485
    root_arena: ExpressionArena,
1351485
) -> (Vec<RewriteSurface>, HashMap<Name, usize>) {
1351485
    let mut surfaces = vec![RewriteSurface::root(root_arena)];
1351485
    let mut value_letting_surfaces = HashMap::new();
1351485
    let symbols = submodel.symbols();
1351485
    for (name, decl) in symbols.iter_local() {
23008146
        let letting_expr = decl.as_value_letting().map(|expr| expr.clone());
23008146
        if let Some(expr) = letting_expr {
763262
            let surface = surfaces.len();
763262
            surfaces.push(RewriteSurface::value_letting(name.clone(), expr));
763262
            value_letting_surfaces.insert(name.clone(), surface);
22244884
        }
    }
1351485
    (surfaces, value_letting_surfaces)
1351485
}
65467621
fn value_letting_surface_name(kind: &RewriteSurfaceKind) -> Option<&Name> {
65467621
    match kind {
64694279
        RewriteSurfaceKind::Root => None,
773342
        RewriteSurfaceKind::ValueLetting { name } => Some(name),
    }
65467621
}
623474907
fn current_value_letting_expression(submodel: &Model, name: &Name) -> Option<Expr> {
    let declaration = {
623474907
        let symbols = submodel.symbols();
623474907
        symbols.lookup_local(name)
    }?;
623474907
    declaration.as_value_letting().map(|expr| expr.clone())
623474907
}
64704358
fn sync_value_letting_surfaces(
64704358
    submodel: &Model,
64704358
    surfaces: &mut Vec<RewriteSurface>,
64704358
    value_letting_surfaces: &mut HashMap<Name, usize>,
64704358
    names: &[Name],
64704358
) -> Vec<usize> {
64704358
    let mut synced_surfaces = Vec::new();
64704358
    let mut seen = HashSet::new();
64704358
    for name in names {
623474907
        if !seen.insert(name.clone()) {
            continue;
623474907
        }
623474907
        if let Some(expr) = current_value_letting_expression(submodel, name) {
523848
            if let Some(old_surface) = value_letting_surfaces.get(name).copied()
                && let Some(surface) = surfaces.get_mut(old_surface)
            {
                surface.active = false;
523848
            }
523848
            let new_surface = surfaces.len();
523848
            surfaces.push(RewriteSurface::value_letting(name.clone(), expr));
523848
            value_letting_surfaces.insert(name.clone(), new_surface);
523848
            synced_surfaces.push(new_surface);
622951059
        } else if let Some(old_surface) = value_letting_surfaces.remove(name)
            && let Some(surface) = surfaces.get_mut(old_surface)
        {
            surface.active = false;
622951059
        }
    }
64704358
    synced_surfaces
64704358
}
1351485
fn write_worklist_surfaces_to_model(submodel: &mut Model, surfaces: &[RewriteSurface]) {
1351485
    let root = surfaces[0]
1351485
        .arena
1351485
        .expression(surfaces[0].arena.root())
1351485
        .clone();
1351485
    submodel.replace_root(root);
1351485
    let mut wrote_value_letting = false;
1351485
    for surface in surfaces.iter().skip(1) {
763262
        if !surface.active {
            continue;
763262
        }
763262
        let Some(name) = value_letting_surface_name(&surface.kind) else {
            continue;
        };
763262
        let expression = surface.arena.expression(surface.arena.root()).clone();
763262
        wrote_value_letting |=
763262
            write_value_letting_expression_to_model_without_refresh(submodel, name, expression);
    }
1351485
    if wrote_value_letting {
290850
        submodel.symbols_mut().refresh_local_binding_hashes();
1060635
    }
1351485
}
1351485
fn move_worklist_surfaces_to_model(submodel: &mut Model, surfaces: Vec<RewriteSurface>) {
1351485
    let mut surfaces = surfaces.into_iter();
1351485
    let root = surfaces.next().expect("worklist must have a root surface");
1351485
    submodel.replace_root(root.arena.into_synced_root_expression());
1351485
    let mut wrote_value_letting = false;
1351485
    for surface in surfaces {
1287110
        if !surface.active {
            continue;
1287110
        }
1287110
        let RewriteSurfaceKind::ValueLetting { name } = surface.kind else {
            continue;
        };
1287110
        wrote_value_letting |= write_value_letting_expression_to_model_without_refresh(
1287110
            submodel,
1287110
            &name,
1287110
            surface.arena.into_synced_root_expression(),
1287110
        );
    }
1351485
    if wrote_value_letting {
290850
        submodel.symbols_mut().refresh_local_binding_hashes();
1060635
    }
1351485
}
10080
fn write_value_letting_surface_to_model(
10080
    submodel: &mut Model,
10080
    name: &Name,
10080
    arena: &ExpressionArena,
10080
) -> bool {
10080
    let written = write_value_letting_surface_to_model_without_refresh(submodel, name, arena);
10080
    if written {
10080
        submodel.symbols_mut().refresh_local_binding_hashes();
    }
10080
    written
10080
}
10080
fn write_value_letting_surface_to_model_without_refresh(
10080
    submodel: &mut Model,
10080
    name: &Name,
10080
    arena: &ExpressionArena,
10080
) -> bool {
10080
    write_value_letting_expression_to_model_without_refresh(
10080
        submodel,
10080
        name,
10080
        arena.expression_from(arena.root()),
10080
    )
10080
}
2060452
fn write_value_letting_expression_to_model_without_refresh(
2060452
    submodel: &mut Model,
2060452
    name: &Name,
2060452
    expression: Expr,
2060452
) -> bool {
    let declaration = {
2060452
        let symbols = submodel.symbols();
2060452
        symbols.lookup_local(name)
    };
2060452
    let Some(mut declaration) = declaration else {
        return false;
    };
    {
2060452
        let Some(mut letting) = declaration.as_value_letting_mut() else {
            return false;
        };
2060452
        *letting = expression;
    }
2060452
    true
2060452
}
64704358
fn enqueue_worklist_nodes_referencing_names(
64704358
    scheduler: &mut WorklistScheduler,
64704358
    surfaces: &[RewriteSurface],
64704358
    names: &[Name],
64704358
    dirty_trace: &mut DirtyTrace,
64704358
) {
64704358
    if names.is_empty() {
61021518
        return;
3682840
    }
    // A `BTreeSet` rather than a hash set: the scheduler breaks depth ties by enqueue order, so
    // the order these are inserted in has to be the same on every run.
3682840
    let mut affected_nodes = BTreeSet::new();
3682840
    for name in names {
7056278
        let Some(reference_nodes) = scheduler.reference_nodes_by_name.get(name) else {
3191851
            continue;
        };
3864427
        for &(surface_index, reference_node) in reference_nodes {
35939052
            let Some(surface) = surfaces.get(surface_index) else {
                continue;
            };
35939052
            if !surface.active
35939052
                || !surface.arena.is_reachable(reference_node)
10782152
                || !expression_directly_references_any(
10782152
                    surface.arena.expression(reference_node),
10782152
                    std::slice::from_ref(name),
10782152
                )
            {
25156900
                continue;
10782152
            }
10782152
            let mut current = Some(reference_node);
76030020
            while let Some(node_id) = current {
65247868
                affected_nodes.insert((surface_index, node_id));
65247868
                current = surface.arena.parent(node_id);
            }
        }
    }
3682840
    for (surface_index, node_id) in affected_nodes {
45582537
        let surface = &surfaces[surface_index];
45582537
        scheduler.enqueue_node_at_level(
45582537
            &surface.arena,
45582537
            surface_index,
45582537
            node_id,
45582537
            0,
45582537
            ScheduledMode::CheckNode,
45582537
            Some(dirty_trace),
45582537
        );
    }
64704358
}
#[cfg(test)]
5
fn collect_worklist_nodes_referencing_names(
5
    arena: &ExpressionArena,
5
    node_id: ExpressionNodeId,
5
    names: &[Name],
5
    affected_nodes: &mut Vec<ExpressionNodeId>,
5
) -> bool {
5
    if !arena.is_reachable(node_id) {
        return false;
5
    }
    let mut references_changed_name =
5
        expression_directly_references_any(arena.expression(node_id), names);
5
    if !matches!(arena.expression(node_id), Expr::Comprehension(_, _)) {
5
        for &child_id in arena.children(node_id) {
4
            references_changed_name |=
4
                collect_worklist_nodes_referencing_names(arena, child_id, names, affected_nodes);
        }
    }
5
    if references_changed_name {
3
        affected_nodes.push(node_id);
2
    }
5
    references_changed_name
5
}
10782157
fn expression_directly_references_any(expr: &Expr, names: &[Name]) -> bool {
10782157
    let Expr::Atomic(_, Atom::Reference(reference)) = expr else {
4
        return false;
    };
10782153
    names.iter().any(|name| &*reference.name() == name)
10782157
}
10080
fn increment_counter(counter: &mut Option<usize>) {
10080
    *counter = Some(counter.unwrap_or(0) + 1);
10080
}
/// Returns expression node ids in rewriter preorder, without entering comprehensions.
580
fn rewriter_preorder_ids(arena: &ExpressionArena) -> Vec<ExpressionNodeId> {
580
    rewriter_reachable_subtree_ids(arena, arena.root())
580
}
/// Returns reachable expression node ids under `node_id` in rewriter preorder.
///
/// The rewrite pass treats comprehensions as atomic here; comprehension expansion is responsible
/// for rewriting their bodies in the right scoped context.
159450386
fn rewriter_reachable_subtree_ids(
159450386
    arena: &ExpressionArena,
159450386
    node_id: ExpressionNodeId,
159450386
) -> Vec<ExpressionNodeId> {
3028408469
    fn collect(
3028408469
        arena: &ExpressionArena,
3028408469
        node_id: ExpressionNodeId,
3028408469
        nodes: &mut Vec<ExpressionNodeId>,
3028408469
    ) {
3028408469
        if !arena.is_reachable(node_id) {
            return;
3028408469
        }
3028408469
        nodes.push(node_id);
3028408469
        if matches!(arena.expression(node_id), Expr::Comprehension(_, _)) {
9631497
            return;
3018776972
        }
3018776972
        for child in arena.children(node_id) {
2868958083
            collect(arena, *child, nodes);
        }
3028408469
    }
159450386
    let mut nodes = Vec::new();
159450386
    collect(arena, node_id, &mut nodes);
159450386
    nodes
159450386
}
/// Replaces the focused expression and updates ancestor payloads to match it.
466
fn replace_focus_and_sync_ancestors(
466
    arena: &mut ExpressionArena,
466
    node_id: ExpressionNodeId,
466
    new_focus: Expr,
466
) {
466
    arena.replace_subtree(node_id, new_focus);
466
    sync_ancestor_payloads(arena, node_id);
466
}
466
fn sync_ancestor_payloads(arena: &mut ExpressionArena, node_id: ExpressionNodeId) {
466
    let mut child_id = node_id;
466
    let mut ancestor = arena.parent(node_id);
1928
    while let Some(ancestor_id) = ancestor {
        // Same-arity parent update: clone only the changed child into the parent payload.
1462
        arena.sync_payload_for_changed_child(ancestor_id, child_id);
1462
        child_id = ancestor_id;
1462
        ancestor = arena.parent(ancestor_id);
    }
466
}
1351592
fn take_model_root(model: &mut Model) -> Expr {
1351592
    model.replace_root(Expr::Root(Metadata::new(), Vec::new()))
1351592
}
71551639817
fn rule_is_universal(rule_data: &RuleData<'_>) -> bool {
71551639817
    rule_data.rule.prefilters.is_none()
71551639817
}
3506878723
fn rule_matches_self_discriminant(rule_data: &RuleData<'_>, expr_discriminant: usize) -> bool {
3506878723
    rule_data
3506878723
        .rule
3506878723
        .prefilters
3506878723
        .is_some_and(|prefilters| {
6119593885
            prefilters.iter().any(|prefilter| {
6119593885
                matches!(prefilter, RulePrefilter::Variant(discriminant) if *discriminant == expr_discriminant)
6119593885
            })
3506878723
        })
3506878723
}
55842356834
fn rule_matches_specific_prefilter(
55842356834
    rule_data: &RuleData<'_>,
55842356834
    expr: &Expr,
55842356834
    arena_node: Option<(&ExpressionArena, ExpressionNodeId)>,
55842356834
) -> bool {
55842356834
    if rule_is_universal(rule_data) {
        return false;
55842356834
    }
55842356834
    let expr_discriminant = discriminant_from_value(expr);
55842356834
    rule_data.rule.prefilters.is_some_and(|prefilters| {
65465212024
        prefilters.iter().any(|prefilter| match prefilter {
58750336857
            RulePrefilter::Variant(discriminant) => *discriminant == expr_discriminant,
            RulePrefilter::Child { child } => {
793271809
                has_direct_child_discriminant(expr, arena_node, *child)
            }
            RulePrefilter::VariantChild { variant, child } => {
3225984155
                *variant == expr_discriminant
133978313
                    && has_direct_child_discriminant(expr, arena_node, *child)
            }
2125099366
            RulePrefilter::Atom(atom_kind) => expr_atom_kind(expr) == Some(*atom_kind),
            RulePrefilter::ChildAtom(atom_kind) => {
570519837
                expr_has_direct_child_atom_kind(expr, *atom_kind)
            }
65465212024
        })
55842356834
    })
55842356834
}
927250122
fn has_direct_child_discriminant(
927250122
    expr: &Expr,
927250122
    arena_node: Option<(&ExpressionArena, ExpressionNodeId)>,
927250122
    target_discriminant: usize,
927250122
) -> bool {
927250122
    if let Some((arena, node_id)) = arena_node {
927250115
        arena.has_direct_child_discriminant(node_id, target_discriminant)
    } else {
7
        expr_has_direct_child_discriminant(expr, &[target_discriminant])
    }
927250122
}
3682023541
fn expr_atom_kind(expr: &Expr) -> Option<AtomKind> {
3682023541
    match expr {
270350525
        Expr::Atomic(_, Atom::Literal(_)) => Some(AtomKind::Literal),
1087253806
        Expr::Atomic(_, Atom::Reference(_)) => Some(AtomKind::Reference),
2324419210
        _ => None,
    }
3682023541
}
/// Returns whether `expr` has an immediate `Atomic` child of this kind.
///
/// Direct children are walked rather than read off the arena's cached child discriminants, which
/// record `Expression` variants only and so cannot tell a reference from a literal.
570519837
fn expr_has_direct_child_atom_kind(expr: &Expr, target: AtomKind) -> bool {
570519837
    let mut found = false;
1812704285
    expr.for_each_expr_child(&mut |child| {
1812704285
        if !found && expr_atom_kind(child) == Some(target) {
159722410
            found = true;
1652981875
        }
1812704285
    });
570519837
    found
570519837
}
7
fn expr_has_direct_child_discriminant(expr: &Expr, target_discriminants: &[usize]) -> bool {
7
    if target_discriminants.is_empty() {
        return false;
7
    }
7
    let mut found = false;
10
    expr.for_each_expr_child(&mut |child| {
10
        if !found && target_discriminants.contains(&discriminant_from_value(child)) {
4
            found = true;
6
        }
10
    });
7
    found
7
}
4320372
fn csv_escape(field: &str) -> String {
4320372
    if field.contains([',', '"', '\n', '\r']) {
659508
        format!("\"{}\"", field.replace('"', "\"\""))
    } else {
3660864
        field.to_string()
    }
4320372
}
#[cfg(test)]
mod tests {
    use crate::ast::comprehension::ComprehensionBuilder;
    use crate::ast::{Atom, DeclarationPtr, ExpressionArena, Literal, Moo, SymbolTablePtr};
    use crate::matrix_expr;
    use super::*;
36
    fn int_lit(value: i32) -> Expr {
36
        Expr::Atomic(Metadata::new(), Atom::Literal(Literal::Int(value)))
36
    }
17
    fn bool_lit(value: bool) -> Expr {
17
        Expr::Atomic(Metadata::new(), Atom::Literal(Literal::Bool(value)))
17
    }
10
    fn root(exprs: Vec<Expr>) -> Expr {
10
        Expr::Root(Metadata::new(), exprs)
10
    }
4
    fn comprehension(return_expression: Expr, guards: Vec<Expr>) -> Expr {
4
        let mut builder = ComprehensionBuilder::new(SymbolTablePtr::new());
4
        for guard in guards {
1
            builder = builder.guard(guard);
        }
4
        Expr::Comprehension(
4
            Metadata::new(),
4
            Moo::new(builder.with_return_value(return_expression)),
4
        )
4
    }
    #[test]
1
    fn dirty_trace_filename_replaces_path_separators_and_module_delimiters() {
1
        assert_eq!(
1
            sanitize_dirty_trace_filename("generated_tests::savilerow/quasiGroup6"),
1
            "generated_tests--savilerow-quasiGroup6"
        );
1
    }
    #[test]
1
    fn dirty_trace_bare_path_is_directory_destination() {
1
        assert_eq!(
1
            dirty_trace_destination_from_env_value("trace-dir".into()),
1
            DirtyTraceDestination::Directory(PathBuf::from("trace-dir"))
        );
1
    }
    #[test]
1
    fn dirty_trace_txt_path_is_file_destination() {
1
        assert_eq!(
1
            dirty_trace_destination_from_env_value("trace.txt".into()),
1
            DirtyTraceDestination::File(PathBuf::from("trace.txt"))
        );
1
    }
5
    fn reference_expr(name: &Name) -> Expr {
        use crate::ast::{Domain, Range, Reference};
5
        Expr::Atomic(
5
            Metadata::new(),
5
            Atom::Reference(Reference::new(DeclarationPtr::new_find(
5
                name.clone(),
5
                Domain::int(vec![Range::Bounded(1, 3)]),
5
            ))),
5
        )
5
    }
    fn test_rule_set_applies(_: &crate::settings::SolverFamily) -> bool {
        true
    }
    fn never_apply_test_rule(
        _: &Expr,
        _: &crate::ast::SymbolTable,
    ) -> crate::rule_engine::ApplicationResult {
        Err(crate::rule_engine::ApplicationError::RuleNotApplicable)
    }
    static TEST_RULE_SET: RuleSet<'static> =
        RuleSet::new("test-rule-set", &[], test_rule_set_applies);
    static TEST_RULE: crate::rule_engine::Rule<'static> = crate::rule_engine::Rule::new(
        "never-apply-test-rule",
        never_apply_test_rule,
        &[("test-rule-set", 1)],
    );
    static TEST_NO_TARGET_RULE: crate::rule_engine::Rule<'static> = crate::rule_engine::Rule {
        name: "no-target-test-rule",
        application: never_apply_test_rule,
        rule_sets: &[("test-rule-set", 1)],
        prefilters: Some(&[]),
        failure_invalidation: crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
    };
    static TEST_SYMBOLS_ONLY_RULE: crate::rule_engine::Rule<'static> = crate::rule_engine::Rule {
        name: "symbols-only-test-rule",
        application: never_apply_test_rule,
        rule_sets: &[("test-rule-set", 1)],
        prefilters: None,
        failure_invalidation: crate::rule_engine::RuleFailureInvalidation::SymbolsOnly,
    };
4
    fn test_rule_groups_at_priorities(priorities: &[u16]) -> Vec<RuleGroup<'static>> {
4
        priorities
4
            .iter()
6
            .map(|&priority| {
6
                RuleGroup::new(
6
                    priority,
6
                    vec![crate::rule_engine::RuleData {
6
                        rule: &TEST_RULE,
6
                        priority,
6
                        rule_set: &TEST_RULE_SET,
6
                    }],
6
                )
6
            })
            .collect()
4
    }
2
    fn test_rule_groups() -> Vec<RuleGroup<'static>> {
2
        test_rule_groups_at_priorities(&[1])
2
    }
2
    fn test_rule_groups_with_two_levels() -> Vec<RuleGroup<'static>> {
2
        test_rule_groups_at_priorities(&[1, 2])
2
    }
1
    fn test_rule_groups_with_no_candidate_middle_level() -> Vec<RuleGroup<'static>> {
1
        vec![
1
            RuleGroup::new(
1
                1,
1
                vec![crate::rule_engine::RuleData {
1
                    rule: &TEST_RULE,
1
                    priority: 1,
1
                    rule_set: &TEST_RULE_SET,
1
                }],
1
            ),
1
            RuleGroup::new(
1
                2,
1
                vec![crate::rule_engine::RuleData {
1
                    rule: &TEST_NO_TARGET_RULE,
1
                    priority: 2,
1
                    rule_set: &TEST_RULE_SET,
1
                }],
1
            ),
1
            RuleGroup::new(
1
                3,
1
                vec![crate::rule_engine::RuleData {
1
                    rule: &TEST_RULE,
1
                    priority: 3,
1
                    rule_set: &TEST_RULE_SET,
1
                }],
1
            ),
        ]
1
    }
1
    fn test_rule_groups_targeting_expr(expr: &Expr) -> Vec<RuleGroup<'static>> {
1
        let discriminant = discriminant_from_value(expr);
1
        let mut rules_by_discriminant = Vec::new();
1
        rules_by_discriminant.resize_with(discriminant + 1, || None);
1
        rules_by_discriminant[discriminant] = Some(vec![crate::rule_engine::RuleData {
1
            rule: &TEST_RULE,
1
            priority: 1,
1
            rule_set: &TEST_RULE_SET,
1
        }]);
1
        vec![RuleGroup {
1
            priority: 1,
1
            rules: vec![crate::rule_engine::RuleData {
1
                rule: &TEST_RULE,
1
                priority: 1,
1
                rule_set: &TEST_RULE_SET,
1
            }],
1
            rules_by_discriminant,
1
            universal_rules: Vec::new(),
1
            has_non_discriminant_filters: false,
1
        }]
1
    }
    #[test]
1
    fn rule_group_child_filter_matches_immediate_child_kind() {
1
        let bubble_discriminant = discriminant_from_value(&Expr::Bubble(
1
            Metadata::new(),
1
            Moo::new(int_lit(0)),
1
            Moo::new(bool_lit(true)),
1
        ));
1
        let child_prefilters: &'static [RulePrefilter] = Box::leak(
1
            vec![RulePrefilter::Child {
1
                child: bubble_discriminant,
1
            }]
1
            .into_boxed_slice(),
1
        );
        let child_bubble_rule: &'static crate::rule_engine::Rule<'static> =
1
            Box::leak(Box::new(crate::rule_engine::Rule {
1
                name: "child-bubble-test-rule",
1
                application: never_apply_test_rule,
1
                rule_sets: &[("test-rule-set", 1)],
1
                prefilters: Some(child_prefilters),
1
                failure_invalidation:
1
                    crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
1
            }));
1
        let rule_group = RuleGroup::new(
1
            1,
1
            vec![crate::rule_engine::RuleData {
1
                rule: child_bubble_rule,
1
                priority: 1,
1
                rule_set: &TEST_RULE_SET,
1
            }],
1
        );
1
        let config = RewriteConfig::optimised();
1
        let expr_with_bubble_child = Expr::Eq(
1
            Metadata::new(),
1
            Moo::new(Expr::Bubble(
1
                Metadata::new(),
1
                Moo::new(int_lit(1)),
1
                Moo::new(bool_lit(true)),
1
            )),
1
            Moo::new(int_lit(2)),
1
        );
        let expr_without_bubble_child =
1
            Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
1
        assert!(rule_group.has_candidates(config, &expr_with_bubble_child));
1
        assert_eq!(
1
            rule_group
1
                .candidates(config, &expr_with_bubble_child)
1
                .map(|rule_data| rule_data.rule.name)
                .collect_vec(),
1
            vec!["child-bubble-test-rule"]
        );
1
        assert!(!rule_group.has_candidates(config, &expr_without_bubble_child));
1
    }
    #[test]
1
    fn rule_group_atom_filter_matches_atomic_reference() {
        let atom_reference_rule: &'static crate::rule_engine::Rule<'static> =
1
            Box::leak(Box::new(crate::rule_engine::Rule {
1
                name: "atom-reference-test-rule",
1
                application: never_apply_test_rule,
1
                rule_sets: &[("test-rule-set", 1)],
1
                prefilters: Some(&[RulePrefilter::Atom(AtomKind::Reference)]),
1
                failure_invalidation:
1
                    crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
1
            }));
1
        let rule_group = RuleGroup::new(
1
            1,
1
            vec![crate::rule_engine::RuleData {
1
                rule: atom_reference_rule,
1
                priority: 1,
1
                rule_set: &TEST_RULE_SET,
1
            }],
1
        );
1
        let config = RewriteConfig::optimised();
1
        let reference = reference_expr(&Name::user("x"));
1
        let literal = int_lit(1);
1
        let composite = Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
1
        assert!(rule_group.has_candidates(config, &reference));
1
        assert_eq!(
1
            rule_group
1
                .candidates(config, &reference)
1
                .map(|rule_data| rule_data.rule.name)
                .collect_vec(),
1
            vec!["atom-reference-test-rule"]
        );
1
        assert!(!rule_group.has_candidates(config, &literal));
1
        assert!(!rule_group.has_candidates(config, &composite));
1
    }
    #[test]
1
    fn rule_group_child_atom_filter_matches_direct_atomic_reference_child() {
        let child_atom_rule: &'static crate::rule_engine::Rule<'static> =
1
            Box::leak(Box::new(crate::rule_engine::Rule {
1
                name: "child-atom-reference-test-rule",
1
                application: never_apply_test_rule,
1
                rule_sets: &[("test-rule-set", 1)],
1
                prefilters: Some(&[RulePrefilter::ChildAtom(AtomKind::Reference)]),
1
                failure_invalidation:
1
                    crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
1
            }));
1
        let rule_group = RuleGroup::new(
1
            1,
1
            vec![crate::rule_engine::RuleData {
1
                rule: child_atom_rule,
1
                priority: 1,
1
                rule_set: &TEST_RULE_SET,
1
            }],
1
        );
1
        let config = RewriteConfig::optimised();
1
        let with_reference_child = Expr::Eq(
1
            Metadata::new(),
1
            Moo::new(reference_expr(&Name::user("x"))),
1
            Moo::new(int_lit(2)),
1
        );
        // Literal children have the same `Atomic` variant, so only the atom kind separates them.
        let with_literal_children =
1
            Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
        // The reference itself is not a match: the filter is about children.
1
        let bare_reference = reference_expr(&Name::user("x"));
1
        assert!(rule_group.has_candidates(config, &with_reference_child));
1
        assert_eq!(
1
            rule_group
1
                .candidates(config, &with_reference_child)
1
                .map(|rule_data| rule_data.rule.name)
                .collect_vec(),
1
            vec!["child-atom-reference-test-rule"]
        );
1
        assert!(!rule_group.has_candidates(config, &with_literal_children));
1
        assert!(!rule_group.has_candidates(config, &bare_reference));
1
    }
    #[test]
1
    fn rule_group_includes_universal_rules_in_variant_buckets() {
1
        let lex_discriminant = discriminant_from_value(&Expr::LexLt(
1
            Metadata::new(),
1
            Moo::new(int_lit(1)),
1
            Moo::new(int_lit(2)),
1
        ));
        let lex_prefilters: &'static [RulePrefilter] =
1
            Box::leak(Box::new([RulePrefilter::Variant(lex_discriminant)]));
        let variant_rule: &'static crate::rule_engine::Rule<'static> =
1
            Box::leak(Box::new(crate::rule_engine::Rule {
1
                name: "variant-specific-test-rule",
1
                application: never_apply_test_rule,
1
                rule_sets: &[("test-rule-set", 1)],
1
                prefilters: Some(lex_prefilters),
1
                failure_invalidation:
1
                    crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
1
            }));
        let universal_rule: &'static crate::rule_engine::Rule<'static> =
1
            Box::leak(Box::new(crate::rule_engine::Rule {
1
                name: "universal-test-rule",
1
                application: never_apply_test_rule,
1
                rule_sets: &[("test-rule-set", 1)],
1
                prefilters: None,
1
                failure_invalidation:
1
                    crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
1
            }));
1
        let rule_group = RuleGroup::new(
1
            1,
1
            vec![
1
                crate::rule_engine::RuleData {
1
                    rule: variant_rule,
1
                    priority: 1,
1
                    rule_set: &TEST_RULE_SET,
1
                },
1
                crate::rule_engine::RuleData {
1
                    rule: universal_rule,
1
                    priority: 1,
1
                    rule_set: &TEST_RULE_SET,
1
                },
1
            ],
1
        );
1
        let config = RewriteConfig::optimised();
1
        let lex = Expr::LexLt(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
1
        assert!(rule_group.has_candidates(config, &lex));
1
        assert_eq!(
1
            rule_group
1
                .candidates(config, &lex)
2
                .map(|rule_data| rule_data.rule.name)
                .collect_vec(),
1
            vec!["variant-specific-test-rule", "universal-test-rule"]
        );
1
    }
    #[test]
1
    fn rule_group_variant_child_filter_does_not_cross_product_alternatives() {
1
        let and_discriminant = discriminant_from_value(&Expr::And(
1
            Metadata::new(),
1
            Moo::new(matrix_expr![bool_lit(true)]),
1
        ));
1
        let or_discriminant = discriminant_from_value(&Expr::Or(
1
            Metadata::new(),
1
            Moo::new(matrix_expr![bool_lit(true)]),
1
        ));
        let comprehension_discriminant =
1
            discriminant_from_value(&comprehension(bool_lit(true), vec![]));
1
        let atomic_discriminant = discriminant_from_value(&bool_lit(true));
1
        let paired_prefilters: &'static [RulePrefilter] = Box::leak(
1
            vec![
1
                RulePrefilter::VariantChild {
1
                    variant: and_discriminant,
1
                    child: comprehension_discriminant,
1
                },
1
                RulePrefilter::VariantChild {
1
                    variant: or_discriminant,
1
                    child: atomic_discriminant,
1
                },
1
            ]
1
            .into_boxed_slice(),
1
        );
        let paired_rule: &'static crate::rule_engine::Rule<'static> =
1
            Box::leak(Box::new(crate::rule_engine::Rule {
1
                name: "paired-prefilter-test-rule",
1
                application: never_apply_test_rule,
1
                rule_sets: &[("test-rule-set", 1)],
1
                prefilters: Some(paired_prefilters),
1
                failure_invalidation:
1
                    crate::rule_engine::RuleFailureInvalidation::ExpressionOrSymbols,
1
            }));
1
        let rule_group = RuleGroup::new(
1
            1,
1
            vec![crate::rule_engine::RuleData {
1
                rule: paired_rule,
1
                priority: 1,
1
                rule_set: &TEST_RULE_SET,
1
            }],
1
        );
1
        let config = RewriteConfig::optimised();
1
        let and_with_comprehension = Expr::And(
1
            Metadata::new(),
1
            Moo::new(comprehension(bool_lit(true), vec![])),
1
        );
1
        let or_with_atomic = Expr::Or(Metadata::new(), Moo::new(bool_lit(true)));
1
        let and_with_atomic = Expr::And(Metadata::new(), Moo::new(bool_lit(true)));
1
        let or_with_comprehension = Expr::Or(
1
            Metadata::new(),
1
            Moo::new(comprehension(bool_lit(true), vec![])),
1
        );
1
        assert!(rule_group.has_candidates(config, &and_with_comprehension));
1
        assert!(rule_group.has_candidates(config, &or_with_atomic));
1
        assert!(!rule_group.has_candidates(config, &and_with_atomic));
1
        assert!(!rule_group.has_candidates(config, &or_with_comprehension));
1
    }
    #[test]
1
    fn rewriter_subtree_preorder_does_not_enter_comprehensions() {
1
        let tree = root(vec![
1
            comprehension(int_lit(1), vec![int_lit(2)]),
1
            int_lit(3),
1
        ]);
1
        let arena = ExpressionArena::from_root(tree);
1
        let root_ids = rewriter_preorder_ids(&arena);
1
        let comp_id = arena.children(arena.root())[0];
1
        assert_eq!(root_ids.len(), 3);
1
        assert_eq!(
1
            rewriter_reachable_subtree_ids(&arena, comp_id),
1
            vec![comp_id]
        );
1
    }
    #[test]
1
    fn worklist_ancestor_enqueue_checks_enclosing_nodes_first() {
1
        let tree = root(vec![Expr::Eq(
1
            Metadata::new(),
1
            Moo::new(int_lit(1)),
1
            Moo::new(int_lit(2)),
1
        )]);
1
        let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
1
        let arena = &surfaces[0].arena;
1
        let ids = rewriter_preorder_ids(arena);
1
        let root_id = ids[0];
1
        let eq_id = ids[1];
1
        let left_leaf_id = ids[2];
1
        let rule_groups = test_rule_groups();
1
        let mut scheduler = WorklistScheduler::empty(&rule_groups);
1
        let mut dirty_trace = DirtyTrace::default();
1
        scheduler.enqueue_node_and_ancestors(arena, 0, left_leaf_id, &mut dirty_trace);
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((0, 0, root_id, ScheduledMode::CheckNode))
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((0, 0, eq_id, ScheduledMode::CheckNode))
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((0, 0, left_leaf_id, ScheduledMode::CheckNode))
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            None
        );
1
    }
    #[test]
1
    fn worklist_level_order_prefers_ancestors_over_earlier_descendants() {
1
        let tree = root(vec![Expr::Eq(
1
            Metadata::new(),
1
            Moo::new(int_lit(1)),
1
            Moo::new(int_lit(2)),
1
        )]);
1
        let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
1
        let arena = &surfaces[0].arena;
1
        let ids = rewriter_preorder_ids(arena);
1
        let eq_id = ids[1];
1
        let left_leaf_id = ids[2];
1
        let rule_groups = test_rule_groups();
1
        let mut scheduler = WorklistScheduler::empty(&rule_groups);
1
        let mut dirty_trace = DirtyTrace::default();
1
        scheduler.enqueue_node_at_level(
1
            arena,
1
            0,
1
            left_leaf_id,
1
            0,
1
            ScheduledMode::CheckNode,
1
            Some(&mut dirty_trace),
1
        );
1
        scheduler.enqueue_node_at_level(
1
            arena,
1
            0,
1
            eq_id,
1
            0,
1
            ScheduledMode::CheckNode,
1
            Some(&mut dirty_trace),
1
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((0, 0, eq_id, ScheduledMode::CheckNode))
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((0, 0, left_leaf_id, ScheduledMode::CheckNode))
        );
1
    }
    #[test]
1
    fn worklist_subtree_descends_lazily_in_breadth_first_order() {
1
        let tree = root(vec![
1
            Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2))),
1
            int_lit(3),
1
        ]);
1
        let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
1
        let arena = &surfaces[0].arena;
1
        let ids = rewriter_preorder_ids(arena);
1
        let root_id = ids[0];
1
        let eq_id = ids[1];
1
        let left_leaf_id = ids[2];
1
        let right_leaf_id = ids[3];
1
        let root_sibling_id = ids[4];
1
        let rule_groups = test_rule_groups_with_two_levels();
        let mut scheduler =
1
            WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
1
        let mut dirty_trace = DirtyTrace::default();
1
        let root_work = scheduler.pop_next(
1
            &surfaces,
1
            &rule_groups,
1
            RewriteConfig::optimised(),
1
            &mut dirty_trace,
1
        );
1
        assert_eq!(
1
            root_work,
1
            Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            None
        );
1
        scheduler.enqueue_after_no_rewrite(
1
            WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
1
            root_id,
1
            0,
1
            1,
1
            ScheduledMode::TraverseSubtreeRoot,
1
            Some(&mut dirty_trace),
1
        );
1
        let eq_work = scheduler.pop_next(
1
            &surfaces,
1
            &rule_groups,
1
            RewriteConfig::optimised(),
1
            &mut dirty_trace,
1
        );
1
        assert_eq!(
1
            eq_work,
1
            Some((0, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
        );
1
        scheduler.enqueue_after_no_rewrite(
1
            WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
1
            eq_id,
1
            0,
1
            1,
1
            ScheduledMode::TraverseSubtreeDescendant,
1
            Some(&mut dirty_trace),
1
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((
1
                0,
1
                0,
1
                root_sibling_id,
1
                ScheduledMode::TraverseSubtreeDescendant
1
            ))
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((0, 0, left_leaf_id, ScheduledMode::TraverseSubtreeDescendant))
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((
1
                0,
1
                0,
1
                right_leaf_id,
1
                ScheduledMode::TraverseSubtreeDescendant
1
            ))
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((1, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            None
        );
1
        scheduler.enqueue_after_no_rewrite(
1
            WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
1
            root_id,
1
            1,
1
            2,
1
            ScheduledMode::TraverseSubtreeRoot,
1
            Some(&mut dirty_trace),
1
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((1, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
        );
1
    }
    #[test]
1
    fn worklist_refreshes_stale_subtree_carrier_after_descendant_rewrite() {
1
        let tree = root(vec![
1
            Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2))),
1
            int_lit(3),
1
        ]);
1
        let mut surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
1
        let ids = rewriter_preorder_ids(&surfaces[0].arena);
1
        let root_id = ids[0];
1
        let eq_id = ids[1];
1
        let root_sibling_id = ids[4];
1
        let rule_groups = test_rule_groups_with_two_levels();
        let mut scheduler =
1
            WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
1
        let mut dirty_trace = DirtyTrace::default();
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
        );
1
        scheduler.enqueue_after_no_rewrite(
1
            WorklistSchedulingContext::new(
1
                &surfaces[0].arena,
1
                0,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
            ),
1
            root_id,
1
            0,
1
            1,
1
            ScheduledMode::TraverseSubtreeRoot,
1
            Some(&mut dirty_trace),
1
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((0, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
        );
1
        replace_focus_and_sync_ancestors(&mut surfaces[0].arena, eq_id, int_lit(10));
1
        enqueue_worklist_rewrite_impact(
1
            &mut scheduler,
1
            &surfaces[0].arena,
1
            0,
1
            eq_id,
1
            &mut dirty_trace,
1
        );
1
        let mut found_refreshed_root = false;
1
        for _ in 0..32 {
4
            let Some((level, surface, node_id, mode)) = scheduler.pop_next(
4
                &surfaces,
4
                &rule_groups,
4
                RewriteConfig::optimised(),
4
                &mut dirty_trace,
4
            ) else {
                break;
            };
4
            if (level, surface, node_id, mode)
4
                == (1, 0, root_id, ScheduledMode::TraverseSubtreeRoot)
            {
1
                found_refreshed_root = true;
1
                scheduler.enqueue_after_no_rewrite(
1
                    WorklistSchedulingContext::new(
1
                        &surfaces[0].arena,
1
                        0,
1
                        &rule_groups,
1
                        RewriteConfig::optimised(),
1
                    ),
1
                    root_id,
1
                    1,
1
                    2,
1
                    ScheduledMode::TraverseSubtreeRoot,
1
                    Some(&mut dirty_trace),
1
                );
1
                break;
3
            }
3
            scheduler.enqueue_after_no_rewrite(
3
                WorklistSchedulingContext::new(
3
                    &surfaces[surface].arena,
3
                    surface,
3
                    &rule_groups,
3
                    RewriteConfig::optimised(),
3
                ),
3
                node_id,
3
                level,
3
                level + 1,
3
                mode,
3
                Some(&mut dirty_trace),
3
            );
        }
1
        assert!(found_refreshed_root);
1
        let mut found_sibling_at_next_level = false;
1
        for _ in 0..32 {
2
            let Some((level, _surface, node_id, mode)) = scheduler.pop_next(
2
                &surfaces,
2
                &rule_groups,
2
                RewriteConfig::optimised(),
2
                &mut dirty_trace,
2
            ) else {
                break;
            };
2
            if (level, node_id, mode)
2
                == (1, root_sibling_id, ScheduledMode::TraverseSubtreeDescendant)
            {
1
                found_sibling_at_next_level = true;
1
                break;
1
            }
        }
1
        assert!(found_sibling_at_next_level);
1
    }
    #[test]
1
    fn worklist_prunes_child_subtrees_without_candidates_at_level() {
1
        let eq = Expr::Eq(Metadata::new(), Moo::new(int_lit(1)), Moo::new(int_lit(2)));
1
        let tree = root(vec![eq.clone(), int_lit(3)]);
1
        let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
1
        let arena = &surfaces[0].arena;
1
        let ids = rewriter_preorder_ids(arena);
1
        let root_id = ids[0];
1
        let eq_id = ids[1];
1
        let rule_groups = test_rule_groups_targeting_expr(&eq);
        let mut scheduler =
1
            WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
1
        let mut dirty_trace = DirtyTrace::default();
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
        );
1
        scheduler.enqueue_after_no_rewrite(
1
            WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
1
            root_id,
1
            0,
1
            1,
1
            ScheduledMode::TraverseSubtreeRoot,
1
            Some(&mut dirty_trace),
1
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((0, 0, eq_id, ScheduledMode::TraverseSubtreeDescendant))
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            None
        );
1
    }
    #[test]
1
    fn worklist_no_rewrite_skips_levels_without_candidates_for_subtree() {
1
        let tree = root(vec![int_lit(1)]);
1
        let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
1
        let arena = &surfaces[0].arena;
1
        let ids = rewriter_preorder_ids(arena);
1
        let root_id = ids[0];
1
        let child_id = ids[1];
1
        let rule_groups = test_rule_groups_with_no_candidate_middle_level();
        let mut scheduler =
1
            WorklistScheduler::new(&surfaces, &rule_groups, RewriteConfig::optimised());
1
        let mut dirty_trace = DirtyTrace::default();
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((0, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
        );
1
        scheduler.enqueue_after_no_rewrite(
1
            WorklistSchedulingContext::new(arena, 0, &rule_groups, RewriteConfig::optimised()),
1
            root_id,
1
            0,
1
            1,
1
            ScheduledMode::TraverseSubtreeRoot,
1
            Some(&mut dirty_trace),
1
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((0, 0, child_id, ScheduledMode::TraverseSubtreeDescendant))
        );
1
        assert_eq!(
1
            scheduler.pop_next(
1
                &surfaces,
1
                &rule_groups,
1
                RewriteConfig::optimised(),
1
                &mut dirty_trace
1
            ),
1
            Some((2, 0, root_id, ScheduledMode::TraverseSubtreeRoot))
        );
1
    }
    #[test]
1
    fn worklist_reference_invalidation_schedules_affected_path_once() {
1
        let x = Name::user("x");
1
        let tree = root(vec![
1
            Expr::Eq(
1
                Metadata::new(),
1
                Moo::new(reference_expr(&x)),
1
                Moo::new(int_lit(1)),
1
            ),
1
            int_lit(2),
1
        ]);
1
        let arena = ExpressionArena::from_root(tree);
1
        let ids = rewriter_preorder_ids(&arena);
1
        let root_id = ids[0];
1
        let eq_id = ids[1];
1
        let reference_id = ids[2];
1
        let mut affected_nodes = Vec::new();
1
        collect_worklist_nodes_referencing_names(
1
            &arena,
1
            arena.root(),
1
            std::slice::from_ref(&x),
1
            &mut affected_nodes,
1
        );
1
        assert_eq!(
1
            affected_nodes.into_iter().rev().collect_vec(),
1
            vec![root_id, eq_id, reference_id]
        );
1
    }
    #[test]
1
    fn worklist_caches_symbols_only_failures_until_symbols_change() {
1
        let tree = root(vec![int_lit(1)]);
1
        let surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(tree))];
1
        let node_id = surfaces[0].arena.root();
1
        let rule_groups = vec![RuleGroup::new(
1
            1,
1
            vec![crate::rule_engine::RuleData {
1
                rule: &TEST_SYMBOLS_ONLY_RULE,
1
                priority: 1,
1
                rule_set: &TEST_RULE_SET,
1
            }],
1
        )];
1
        let rule_data = &rule_groups[0].rules[0];
1
        let mut scheduler = WorklistScheduler::empty(&rule_groups);
1
        assert!(scheduler.should_attempt_rule(0, node_id, rule_data));
1
        scheduler.record_rule_failure(0, node_id, rule_data);
1
        assert!(!scheduler.should_attempt_rule(0, node_id, rule_data));
1
        scheduler.invalidate_symbol_rule_failures();
1
        assert!(scheduler.should_attempt_rule(0, node_id, rule_data));
1
    }
    #[test]
1
    fn evaluator_batches_ancestor_work_until_the_worklist_is_empty() {
1
        let x = Name::user("x");
1
        let and = Expr::And(
1
            Metadata::new(),
1
            Moo::new(matrix_expr![bool_lit(true), reference_expr(&x)]),
1
        );
1
        let mut surfaces = vec![RewriteSurface::root(ExpressionArena::from_root(root(
1
            vec![and],
1
        )))];
1
        let ids = rewriter_preorder_ids(&surfaces[0].arena);
1
        let and_id = ids
1
            .iter()
1
            .copied()
2
            .find(|&node_id| matches!(surfaces[0].arena.expression(node_id), Expr::And(..)))
1
            .expect("and node");
1
        let true_id = ids
1
            .iter()
1
            .copied()
4
            .find(|&node_id| surfaces[0].arena.expression(node_id) == &bool_lit(true))
1
            .expect("true literal");
1
        replace_focus_and_sync_ancestors(&mut surfaces[0].arena, true_id, bool_lit(false));
1
        let mut work = DeferredEvaluatorWork::default();
1
        work.defer_ancestors(&mut surfaces[0].arena, 0, true_id);
1
        work.defer_ancestors(&mut surfaces[0].arena, 0, true_id);
1
        assert_eq!(
1
            work.nodes.len(),
1
            3,
            "matrix, `and`, and root ancestors are unique"
        );
1
        assert!(matches!(
1
            surfaces[0].arena.expression(and_id),
            Expr::And(..)
        ));
1
        let rule_groups = Vec::new();
1
        let mut scheduler = WorklistScheduler::empty(&rule_groups);
1
        let mut dirty_trace = DirtyTrace::default();
1
        assert!(normalise_deferred_evaluators(
1
            &mut work,
1
            &mut Model::new(Default::default()),
1
            &mut surfaces,
1
            &mut scheduler,
1
            &mut dirty_trace,
1
        ));
1
        assert_eq!(surfaces[0].arena.expression(and_id), &bool_lit(false));
1
        assert!(work.is_empty());
1
    }
}