Skip to main content

workshop_rs/output/
compare.rs

1use crate::ModifyOp;
2use crate::core::format::format_number;
3use crate::wir;
4
5use super::roundtrip::{ambiguous_enum_equivalent, canonical_value_name, settings_equivalent};
6
7/// Which program of a [`difference`] pair a report refers to.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ProgramSide {
10    /// `difference`'s first argument.
11    First,
12    /// `difference`'s second argument.
13    Second,
14}
15
16/// A declaration table in a [`Program`](crate::Program).
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DeclarationTable {
19    /// `program.global_variables`
20    GlobalVariables,
21    /// `program.player_variables`
22    PlayerVariables,
23    /// `program.subroutines`
24    Subroutines,
25}
26
27/// One step from a compared element to the differing child element.
28///
29/// Indices count only compared elements: disabled conditions, disabled
30/// actions, and inert rules are skipped before comparison.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum PathStep {
34    /// The n-th enabled condition of the compared rules; a `Value(0)` step
35    /// after it enters the condition's value.
36    Condition(usize),
37    /// The n-th enabled action of the current action sequence — the rule's
38    /// actions with each control-flow group folded into its header action
39    /// (`If`, `While`, `For`), or a nested list entered through `Branch`,
40    /// `ElseBody`, or `Body`.
41    Action(usize),
42    /// The n-th value operand of an action — its value arguments in
43    /// declaration order (`If` carries one condition per branch) — or the
44    /// n-th child of a value: `Array` elements, `Call` arguments, `Vector`
45    /// x/y/z, or a `PlayerVariable`'s player. For the `Set`/`Modify` spelling
46    /// pair the steps index the `Set` value's call arguments; for a
47    /// `PlayerVariable`/`memberAccess` pair `Value(1)` is the memberAccess's
48    /// member operand.
49    Value(usize),
50    /// The body of the n-th `if`/`else if` branch of an `If` action.
51    Branch(usize),
52    /// The `else` body of an `If` action.
53    ElseBody,
54    /// The body of a `While` or `For` action.
55    Body,
56}
57
58/// What differs at a [`Difference`]'s location.
59#[derive(Debug, Clone, PartialEq)]
60#[non_exhaustive]
61pub enum DifferenceKind {
62    /// A program could not be materialized into canonical form, so the
63    /// comparison is undefined for it.
64    Malformed {
65        program: ProgramSide,
66        message: String,
67    },
68    /// The programs' `settings` differ.
69    Settings,
70    /// A declaration table declares a different index set.
71    DeclarationTable {
72        table: DeclarationTable,
73        first: Vec<u32>,
74        second: Vec<u32>,
75    },
76    /// Compared element counts differ; `element` names the compared list —
77    /// `"rules"`, `"subroutine rules"`, `"conditions"`, `"actions"`,
78    /// `"branches"`, `"arguments"`, or `"elements"`.
79    Count {
80        element: &'static str,
81        first: usize,
82        second: usize,
83    },
84    /// The compared rules' events differ; `first` and `second` describe the
85    /// two events.
86    Event { first: String, second: String },
87    /// The compared nodes have different kinds, e.g. `"setGlobalVariable"`
88    /// vs `"modifyGlobalVariable"` or `"number"` vs `"call"`.
89    Shape {
90        first: &'static str,
91        second: &'static str,
92    },
93    /// Variable or subroutine references resolve to different declaration
94    /// indices.
95    Reference {
96        table: DeclarationTable,
97        first: u32,
98        second: u32,
99    },
100    /// Modify operations differ (`"add"`, `"subtract"`, …).
101    Operation { first: String, second: String },
102    /// Scalar payloads differ — numbers, strings, enum members, call ids.
103    Scalar { first: String, second: String },
104}
105
106/// The first behavior-relevant difference [`difference`] found.
107#[derive(Debug, Clone, PartialEq)]
108#[non_exhaustive]
109pub struct Difference {
110    /// The compared rules' positions in each program's `rules`, or `None`
111    /// for differences above the rule level.
112    pub rules: Option<(usize, usize)>,
113    /// The path to the differing element inside the compared rules; empty
114    /// for rule-level differences (the event, or an element count).
115    pub path: Vec<PathStep>,
116    /// What differs at that location.
117    pub kind: DifferenceKind,
118}
119
120/// Whether two programs are behaviorally equivalent under this module's
121/// [comparison contract](self).
122pub fn equivalent(first: &crate::Program, second: &crate::Program) -> bool {
123    difference(first, second).is_none()
124}
125
126/// The first behavior-relevant difference between `first` and `second`
127/// under this module's [comparison contract](self), or `None` when the
128/// programs are equivalent.
129pub fn difference(first: &crate::Program, second: &crate::Program) -> Option<Difference> {
130    let a = match first.to_wir() {
131        Ok(program) => program,
132        Err(error) => return Some(malformed(ProgramSide::First, error)),
133    };
134    let b = match second.to_wir() {
135        Ok(program) => program,
136        Err(error) => return Some(malformed(ProgramSide::Second, error)),
137    };
138    compare(&a, &b)
139}
140
141fn malformed(program: ProgramSide, error: crate::WorkshopError) -> Difference {
142    Difference {
143        rules: None,
144        path: Vec::new(),
145        kind: DifferenceKind::Malformed {
146            program,
147            message: error.to_string(),
148        },
149    }
150}
151
152fn program_level(kind: DifferenceKind) -> Difference {
153    Difference {
154        rules: None,
155        path: Vec::new(),
156        kind,
157    }
158}
159
160fn compare(a: &wir::Program, b: &wir::Program) -> Option<Difference> {
161    if !settings_equivalent(a.settings.as_ref(), b.settings.as_ref()) {
162        return Some(program_level(DifferenceKind::Settings));
163    }
164    for (table, first, second) in [
165        (
166            DeclarationTable::GlobalVariables,
167            index_set(a.global_variables.iter().map(|variable| variable.index)),
168            index_set(b.global_variables.iter().map(|variable| variable.index)),
169        ),
170        (
171            DeclarationTable::PlayerVariables,
172            index_set(a.player_variables.iter().map(|variable| variable.index)),
173            index_set(b.player_variables.iter().map(|variable| variable.index)),
174        ),
175        (
176            DeclarationTable::Subroutines,
177            index_set(a.subroutines.iter().map(|subroutine| subroutine.index)),
178            index_set(b.subroutines.iter().map(|subroutine| subroutine.index)),
179        ),
180    ] {
181        if first != second {
182            return Some(program_level(DifferenceKind::DeclarationTable {
183                table,
184                first,
185                second,
186            }));
187        }
188    }
189    let first_rules = effective_rules(a);
190    let second_rules = effective_rules(b);
191    if first_rules.ordered.len() != second_rules.ordered.len() {
192        return Some(program_level(DifferenceKind::Count {
193            element: "rules",
194            first: first_rules.ordered.len(),
195            second: second_rules.ordered.len(),
196        }));
197    }
198    for (&(first, left), &(second, right)) in first_rules.ordered.iter().zip(&second_rules.ordered)
199    {
200        if let Some((path, kind)) = rule_difference(a, b, left, right) {
201            return Some(Difference {
202                rules: Some((first, second)),
203                path,
204                kind,
205            });
206        }
207    }
208    if first_rules.subroutines.len() != second_rules.subroutines.len() {
209        return Some(program_level(DifferenceKind::Count {
210            element: "subroutine rules",
211            first: first_rules.subroutines.len(),
212            second: second_rules.subroutines.len(),
213        }));
214    }
215    for (&(_, first, left), &(_, second, right)) in first_rules
216        .subroutines
217        .iter()
218        .zip(&second_rules.subroutines)
219    {
220        if let Some((path, kind)) = rule_difference(a, b, left, right) {
221            return Some(Difference {
222                rules: Some((first, second)),
223                path,
224                kind,
225            });
226        }
227    }
228    None
229}
230
231fn index_set(indices: impl Iterator<Item = u32>) -> Vec<u32> {
232    let mut indices: Vec<u32> = indices.collect();
233    indices.sort_unstable();
234    indices
235}
236
237struct EffectiveRules<'p> {
238    ordered: Vec<(usize, &'p wir::Rule)>,
239    subroutines: Vec<(u32, usize, &'p wir::Rule)>,
240}
241
242fn effective_rules(program: &wir::Program) -> EffectiveRules<'_> {
243    let mut ordered = Vec::new();
244    let mut subroutines = Vec::new();
245    for (position, rule) in program.rules.iter().enumerate() {
246        if rule.disabled
247            || !rule
248                .actions
249                .iter()
250                .any(|action| !is_disabled(program, *action))
251        {
252            continue;
253        }
254        if let wir::Event::Subroutine { subroutine, .. } = &rule.event {
255            subroutines.push((subroutine_index(program, *subroutine), position, rule));
256        } else {
257            ordered.push((position, rule));
258        }
259    }
260    subroutines.sort_by_key(|(index, ..)| *index);
261    EffectiveRules {
262        ordered,
263        subroutines,
264    }
265}
266
267fn is_disabled(program: &wir::Program, action: wir::ActionId) -> bool {
268    matches!(
269        program.actions.get(action),
270        Some(wir::Action::Disabled { .. })
271    )
272}
273
274fn enabled_actions(program: &wir::Program, actions: &[wir::ActionId]) -> Vec<wir::ActionId> {
275    actions
276        .iter()
277        .copied()
278        .filter(|action| !is_disabled(program, *action))
279        .collect()
280}
281
282fn global_index(program: &wir::Program, variable: wir::GlobalVarId) -> u32 {
283    program
284        .global_variables
285        .get(variable)
286        .map_or(u32::MAX, |variable| variable.index)
287}
288
289fn player_index(program: &wir::Program, variable: wir::PlayerVarId) -> u32 {
290    program
291        .player_variables
292        .get(variable)
293        .map_or(u32::MAX, |variable| variable.index)
294}
295
296fn subroutine_index(program: &wir::Program, subroutine: wir::SubroutineId) -> u32 {
297    program
298        .subroutines
299        .get(subroutine)
300        .map_or(u32::MAX, |subroutine| subroutine.index)
301}
302
303fn rule_difference(
304    a: &wir::Program,
305    b: &wir::Program,
306    left: &wir::Rule,
307    right: &wir::Rule,
308) -> Option<(Vec<PathStep>, DifferenceKind)> {
309    let mut path = Vec::new();
310    let kind = rule_kind(a, b, left, right, &mut path)?;
311    Some((path, kind))
312}
313
314fn rule_kind(
315    a: &wir::Program,
316    b: &wir::Program,
317    left: &wir::Rule,
318    right: &wir::Rule,
319    path: &mut Vec<PathStep>,
320) -> Option<DifferenceKind> {
321    if !event_equivalent(a, &left.event, b, &right.event) {
322        return Some(DifferenceKind::Event {
323            first: describe_event(a, &left.event),
324            second: describe_event(b, &right.event),
325        });
326    }
327    let left_conditions: Vec<wir::Condition> = left
328        .conditions
329        .iter()
330        .copied()
331        .filter(|condition| !condition.disabled)
332        .collect();
333    let right_conditions: Vec<wir::Condition> = right
334        .conditions
335        .iter()
336        .copied()
337        .filter(|condition| !condition.disabled)
338        .collect();
339    if left_conditions.len() != right_conditions.len() {
340        return Some(DifferenceKind::Count {
341            element: "conditions",
342            first: left_conditions.len(),
343            second: right_conditions.len(),
344        });
345    }
346    for (index, (left, right)) in left_conditions.iter().zip(&right_conditions).enumerate() {
347        path.push(PathStep::Condition(index));
348        if let Some(kind) = value_difference(a, b, left.value, right.value, path) {
349            return Some(kind);
350        }
351        path.pop();
352    }
353    action_list_difference(a, b, &left.actions, &right.actions, path)
354}
355
356fn event_equivalent(
357    a: &wir::Program,
358    left: &wir::Event,
359    b: &wir::Program,
360    right: &wir::Event,
361) -> bool {
362    match (left, right) {
363        (wir::Event::Global, wir::Event::Global) => true,
364        (wir::Event::EachPlayer, wir::Event::EachPlayer) => true,
365        (wir::Event::EachPlayer, wir::Event::EachPlayerWithFilters { team, target })
366        | (wir::Event::EachPlayerWithFilters { team, target }, wir::Event::EachPlayer) => {
367            *team == wir::EventTeam::All && *target == wir::EventTarget::All
368        }
369        (
370            wir::Event::EachPlayerWithFilters {
371                team: first_team,
372                target: first_target,
373            },
374            wir::Event::EachPlayerWithFilters {
375                team: second_team,
376                target: second_target,
377            },
378        ) => first_team == second_team && first_target == second_target,
379        (
380            wir::Event::Player {
381                kind: first_kind,
382                team: first_team,
383                target: first_target,
384            },
385            wir::Event::Player {
386                kind: second_kind,
387                team: second_team,
388                target: second_target,
389            },
390        ) => {
391            first_kind == second_kind && first_team == second_team && first_target == second_target
392        }
393        (
394            wir::Event::Subroutine {
395                subroutine: first, ..
396            },
397            wir::Event::Subroutine {
398                subroutine: second, ..
399            },
400        ) => subroutine_index(a, *first) == subroutine_index(b, *second),
401        _ => false,
402    }
403}
404
405fn describe_event(program: &wir::Program, event: &wir::Event) -> String {
406    match event {
407        wir::Event::Global => "global".to_string(),
408        wir::Event::EachPlayer => "eachPlayer".to_string(),
409        wir::Event::EachPlayerWithFilters { team, target } => {
410            format!("eachPlayer(team={team:?}, target={target:?})")
411        }
412        wir::Event::Player { kind, team, target } => {
413            format!("{}(team={team:?}, target={target:?})", kind.catalog_id())
414        }
415        wir::Event::Subroutine { subroutine, .. } => {
416            format!(
417                "subroutine(index={})",
418                subroutine_index(program, *subroutine)
419            )
420        }
421    }
422}
423
424fn action_list_difference(
425    a: &wir::Program,
426    b: &wir::Program,
427    left: &[wir::ActionId],
428    right: &[wir::ActionId],
429    path: &mut Vec<PathStep>,
430) -> Option<DifferenceKind> {
431    let left = enabled_actions(a, left);
432    let right = enabled_actions(b, right);
433    if left.len() != right.len() {
434        return Some(DifferenceKind::Count {
435            element: "actions",
436            first: left.len(),
437            second: right.len(),
438        });
439    }
440    for (index, (&left, &right)) in left.iter().zip(&right).enumerate() {
441        path.push(PathStep::Action(index));
442        if let Some(kind) = action_difference(a, b, left, right, path) {
443            return Some(kind);
444        }
445        path.pop();
446    }
447    None
448}
449
450fn action_difference(
451    a: &wir::Program,
452    b: &wir::Program,
453    left: wir::ActionId,
454    right: wir::ActionId,
455    path: &mut Vec<PathStep>,
456) -> Option<DifferenceKind> {
457    let (Some(first), Some(second)) = (a.actions.get(left), b.actions.get(right)) else {
458        return Some(DifferenceKind::Shape {
459            first: "action",
460            second: "missing",
461        });
462    };
463    match (first, second) {
464        (
465            wir::Action::SetGlobalVariable {
466                variable: first_variable,
467                value: first_value,
468                ..
469            },
470            wir::Action::SetGlobalVariable {
471                variable: second_variable,
472                value: second_value,
473                ..
474            },
475        ) => {
476            let difference = reference_difference(
477                global_index(a, *first_variable),
478                global_index(b, *second_variable),
479                DeclarationTable::GlobalVariables,
480            );
481            if difference.is_some() {
482                return difference;
483            }
484            argument_difference(a, b, path, 0, *first_value, *second_value)
485        }
486        (
487            wir::Action::ModifyGlobalVariable {
488                variable: first_variable,
489                op: first_op,
490                value: first_value,
491                ..
492            },
493            wir::Action::ModifyGlobalVariable {
494                variable: second_variable,
495                op: second_op,
496                value: second_value,
497                ..
498            },
499        ) => {
500            let difference = reference_difference(
501                global_index(a, *first_variable),
502                global_index(b, *second_variable),
503                DeclarationTable::GlobalVariables,
504            );
505            if difference.is_some() {
506                return difference;
507            }
508            if first_op != second_op {
509                return Some(DifferenceKind::Operation {
510                    first: first_op.catalog_id().to_string(),
511                    second: second_op.catalog_id().to_string(),
512                });
513            }
514            argument_difference(a, b, path, 0, *first_value, *second_value)
515        }
516        (
517            wir::Action::SetGlobalVariable {
518                variable: first_variable,
519                value: first_value,
520                ..
521            },
522            wir::Action::ModifyGlobalVariable {
523                variable: second_variable,
524                op: second_op,
525                value: second_value,
526                ..
527            },
528        ) => {
529            let first_index = global_index(a, *first_variable);
530            let second_index = global_index(b, *second_variable);
531            let difference =
532                reference_difference(first_index, second_index, DeclarationTable::GlobalVariables);
533            if difference.is_some() {
534                return difference;
535            }
536            self_modify_difference(
537                SelfModify {
538                    set_program: a,
539                    modify_program: b,
540                    set_is_first: true,
541                    target: ModifyTarget::Global { index: first_index },
542                    set_value: *first_value,
543                    op: *second_op,
544                    modify_value: *second_value,
545                },
546                path,
547            )
548        }
549        (
550            wir::Action::ModifyGlobalVariable {
551                variable: first_variable,
552                op: first_op,
553                value: first_value,
554                ..
555            },
556            wir::Action::SetGlobalVariable {
557                variable: second_variable,
558                value: second_value,
559                ..
560            },
561        ) => {
562            let first_index = global_index(a, *first_variable);
563            let second_index = global_index(b, *second_variable);
564            let difference =
565                reference_difference(first_index, second_index, DeclarationTable::GlobalVariables);
566            if difference.is_some() {
567                return difference;
568            }
569            self_modify_difference(
570                SelfModify {
571                    set_program: b,
572                    modify_program: a,
573                    set_is_first: false,
574                    target: ModifyTarget::Global { index: first_index },
575                    set_value: *second_value,
576                    op: *first_op,
577                    modify_value: *first_value,
578                },
579                path,
580            )
581        }
582        (
583            wir::Action::SetPlayerVariable {
584                player: first_player,
585                variable: first_variable,
586                value: first_value,
587                ..
588            },
589            wir::Action::SetPlayerVariable {
590                player: second_player,
591                variable: second_variable,
592                value: second_value,
593                ..
594            },
595        ) => {
596            if let Some(kind) = argument_difference(a, b, path, 0, *first_player, *second_player) {
597                return Some(kind);
598            }
599            let difference = reference_difference(
600                player_index(a, *first_variable),
601                player_index(b, *second_variable),
602                DeclarationTable::PlayerVariables,
603            );
604            if difference.is_some() {
605                return difference;
606            }
607            argument_difference(a, b, path, 1, *first_value, *second_value)
608        }
609        (
610            wir::Action::ModifyPlayerVariable {
611                player: first_player,
612                variable: first_variable,
613                op: first_op,
614                value: first_value,
615                ..
616            },
617            wir::Action::ModifyPlayerVariable {
618                player: second_player,
619                variable: second_variable,
620                op: second_op,
621                value: second_value,
622                ..
623            },
624        ) => {
625            if let Some(kind) = argument_difference(a, b, path, 0, *first_player, *second_player) {
626                return Some(kind);
627            }
628            let difference = reference_difference(
629                player_index(a, *first_variable),
630                player_index(b, *second_variable),
631                DeclarationTable::PlayerVariables,
632            );
633            if difference.is_some() {
634                return difference;
635            }
636            if first_op != second_op {
637                return Some(DifferenceKind::Operation {
638                    first: first_op.catalog_id().to_string(),
639                    second: second_op.catalog_id().to_string(),
640                });
641            }
642            argument_difference(a, b, path, 1, *first_value, *second_value)
643        }
644        (
645            wir::Action::SetPlayerVariable {
646                player: first_player,
647                variable: first_variable,
648                value: first_value,
649                ..
650            },
651            wir::Action::ModifyPlayerVariable {
652                player: second_player,
653                variable: second_variable,
654                op: second_op,
655                value: second_value,
656                ..
657            },
658        ) => {
659            if let Some(kind) = argument_difference(a, b, path, 0, *first_player, *second_player) {
660                return Some(kind);
661            }
662            let first_index = player_index(a, *first_variable);
663            let second_index = player_index(b, *second_variable);
664            let difference =
665                reference_difference(first_index, second_index, DeclarationTable::PlayerVariables);
666            if difference.is_some() {
667                return difference;
668            }
669            self_modify_difference(
670                SelfModify {
671                    set_program: a,
672                    modify_program: b,
673                    set_is_first: true,
674                    target: ModifyTarget::Player {
675                        index: first_index,
676                        player: *first_player,
677                    },
678                    set_value: *first_value,
679                    op: *second_op,
680                    modify_value: *second_value,
681                },
682                path,
683            )
684        }
685        (
686            wir::Action::ModifyPlayerVariable {
687                player: first_player,
688                variable: first_variable,
689                op: first_op,
690                value: first_value,
691                ..
692            },
693            wir::Action::SetPlayerVariable {
694                player: second_player,
695                variable: second_variable,
696                value: second_value,
697                ..
698            },
699        ) => {
700            if let Some(kind) = argument_difference(a, b, path, 0, *first_player, *second_player) {
701                return Some(kind);
702            }
703            let first_index = player_index(a, *first_variable);
704            let second_index = player_index(b, *second_variable);
705            let difference =
706                reference_difference(first_index, second_index, DeclarationTable::PlayerVariables);
707            if difference.is_some() {
708                return difference;
709            }
710            self_modify_difference(
711                SelfModify {
712                    set_program: b,
713                    modify_program: a,
714                    set_is_first: false,
715                    target: ModifyTarget::Player {
716                        index: first_index,
717                        player: *second_player,
718                    },
719                    set_value: *second_value,
720                    op: *first_op,
721                    modify_value: *first_value,
722                },
723                path,
724            )
725        }
726        (
727            wir::Action::AssignMember {
728                target: first_target,
729                op: first_op,
730                value: first_value,
731                ..
732            },
733            wir::Action::AssignMember {
734                target: second_target,
735                op: second_op,
736                value: second_value,
737                ..
738            },
739        ) => {
740            if first_op != second_op {
741                return Some(DifferenceKind::Operation {
742                    first: first_op.map_or("assign", ModifyOp::catalog_id).to_string(),
743                    second: second_op.map_or("assign", ModifyOp::catalog_id).to_string(),
744                });
745            }
746            if let Some(kind) = argument_difference(a, b, path, 0, *first_target, *second_target) {
747                return Some(kind);
748            }
749            argument_difference(a, b, path, 1, *first_value, *second_value)
750        }
751        (
752            wir::Action::CallSubroutine {
753                subroutine: first, ..
754            },
755            wir::Action::CallSubroutine {
756                subroutine: second, ..
757            },
758        ) => reference_difference(
759            subroutine_index(a, *first),
760            subroutine_index(b, *second),
761            DeclarationTable::Subroutines,
762        ),
763        (
764            wir::Action::If {
765                branches: first_branches,
766                else_body: first_else,
767                ..
768            },
769            wir::Action::If {
770                branches: second_branches,
771                else_body: second_else,
772                ..
773            },
774        ) => {
775            if first_branches.len() != second_branches.len() {
776                return Some(DifferenceKind::Count {
777                    element: "branches",
778                    first: first_branches.len(),
779                    second: second_branches.len(),
780                });
781            }
782            for (index, (first, second)) in first_branches.iter().zip(second_branches).enumerate() {
783                path.push(PathStep::Value(index));
784                if let Some(kind) = value_difference(a, b, first.condition, second.condition, path)
785                {
786                    return Some(kind);
787                }
788                path.pop();
789                path.push(PathStep::Branch(index));
790                if let Some(kind) = action_list_difference(a, b, &first.body, &second.body, path) {
791                    return Some(kind);
792                }
793                path.pop();
794            }
795            match (first_else, second_else) {
796                (None, None) => None,
797                (Some(first), Some(second)) => {
798                    path.push(PathStep::ElseBody);
799                    let difference = action_list_difference(a, b, first, second, path);
800                    if difference.is_none() {
801                        path.pop();
802                    }
803                    difference
804                }
805                (first, second) => Some(DifferenceKind::Shape {
806                    first: if first.is_some() { "if/else" } else { "if" },
807                    second: if second.is_some() { "if/else" } else { "if" },
808                }),
809            }
810        }
811        (
812            wir::Action::While {
813                condition: first_condition,
814                body: first_body,
815                ..
816            },
817            wir::Action::While {
818                condition: second_condition,
819                body: second_body,
820                ..
821            },
822        ) => {
823            if let Some(kind) =
824                argument_difference(a, b, path, 0, *first_condition, *second_condition)
825            {
826                return Some(kind);
827            }
828            path.push(PathStep::Body);
829            let difference = action_list_difference(a, b, first_body, second_body, path);
830            if difference.is_none() {
831                path.pop();
832            }
833            difference
834        }
835        (
836            wir::Action::ForGlobalVariable {
837                variable: first_variable,
838                start: first_start,
839                stop: first_stop,
840                step: first_step,
841                body: first_body,
842                ..
843            },
844            wir::Action::ForGlobalVariable {
845                variable: second_variable,
846                start: second_start,
847                stop: second_stop,
848                step: second_step,
849                body: second_body,
850                ..
851            },
852        ) => {
853            let difference = reference_difference(
854                global_index(a, *first_variable),
855                global_index(b, *second_variable),
856                DeclarationTable::GlobalVariables,
857            );
858            if difference.is_some() {
859                return difference;
860            }
861            for (index, (&first, &second)) in [
862                (first_start, second_start),
863                (first_stop, second_stop),
864                (first_step, second_step),
865            ]
866            .into_iter()
867            .enumerate()
868            {
869                if let Some(kind) = argument_difference(a, b, path, index, first, second) {
870                    return Some(kind);
871                }
872            }
873            path.push(PathStep::Body);
874            let difference = action_list_difference(a, b, first_body, second_body, path);
875            if difference.is_none() {
876                path.pop();
877            }
878            difference
879        }
880        (
881            wir::Action::ForPlayerVariable {
882                player: first_player,
883                variable: first_variable,
884                start: first_start,
885                stop: first_stop,
886                step: first_step,
887                body: first_body,
888                ..
889            },
890            wir::Action::ForPlayerVariable {
891                player: second_player,
892                variable: second_variable,
893                start: second_start,
894                stop: second_stop,
895                step: second_step,
896                body: second_body,
897                ..
898            },
899        ) => {
900            if let Some(kind) = argument_difference(a, b, path, 0, *first_player, *second_player) {
901                return Some(kind);
902            }
903            let difference = reference_difference(
904                player_index(a, *first_variable),
905                player_index(b, *second_variable),
906                DeclarationTable::PlayerVariables,
907            );
908            if difference.is_some() {
909                return difference;
910            }
911            for (index, (&first, &second)) in [
912                (first_start, second_start),
913                (first_stop, second_stop),
914                (first_step, second_step),
915            ]
916            .into_iter()
917            .enumerate()
918            {
919                if let Some(kind) = argument_difference(a, b, path, index + 1, first, second) {
920                    return Some(kind);
921                }
922            }
923            path.push(PathStep::Body);
924            let difference = action_list_difference(a, b, first_body, second_body, path);
925            if difference.is_none() {
926                path.pop();
927            }
928            difference
929        }
930        (
931            wir::Action::Call {
932                name: first_name,
933                args: first_args,
934                ..
935            },
936            wir::Action::Call {
937                name: second_name,
938                args: second_args,
939                ..
940            },
941        ) => {
942            if first_name != second_name {
943                return Some(DifferenceKind::Scalar {
944                    first: first_name.clone(),
945                    second: second_name.clone(),
946                });
947            }
948            if first_args.len() != second_args.len() {
949                return Some(DifferenceKind::Count {
950                    element: "arguments",
951                    first: first_args.len(),
952                    second: second_args.len(),
953                });
954            }
955            for (index, (&first, &second)) in first_args.iter().zip(second_args).enumerate() {
956                if let Some(kind) = argument_difference(a, b, path, index, first, second) {
957                    return Some(kind);
958                }
959            }
960            None
961        }
962        _ => Some(DifferenceKind::Shape {
963            first: action_shape(first),
964            second: action_shape(second),
965        }),
966    }
967}
968
969fn reference_difference(
970    first: u32,
971    second: u32,
972    table: DeclarationTable,
973) -> Option<DifferenceKind> {
974    (first != second).then_some(DifferenceKind::Reference {
975        table,
976        first,
977        second,
978    })
979}
980
981fn argument_difference(
982    a: &wir::Program,
983    b: &wir::Program,
984    path: &mut Vec<PathStep>,
985    index: usize,
986    first: wir::ValueId,
987    second: wir::ValueId,
988) -> Option<DifferenceKind> {
989    path.push(PathStep::Value(index));
990    let difference = value_difference(a, b, first, second, path);
991    if difference.is_none() {
992        path.pop();
993    }
994    difference
995}
996
997struct SelfModify<'p> {
998    set_program: &'p wir::Program,
999    modify_program: &'p wir::Program,
1000    set_is_first: bool,
1001    target: ModifyTarget,
1002    set_value: wir::ValueId,
1003    op: ModifyOp,
1004    modify_value: wir::ValueId,
1005}
1006
1007#[derive(Clone, Copy)]
1008enum ModifyTarget {
1009    Global { index: u32 },
1010    Player { index: u32, player: wir::ValueId },
1011}
1012
1013impl ModifyTarget {
1014    fn table(&self) -> DeclarationTable {
1015        match self {
1016            Self::Global { .. } => DeclarationTable::GlobalVariables,
1017            Self::Player { .. } => DeclarationTable::PlayerVariables,
1018        }
1019    }
1020
1021    fn index(&self) -> u32 {
1022        match self {
1023            Self::Global { index } | Self::Player { index, .. } => *index,
1024        }
1025    }
1026
1027    fn shapes(&self) -> (&'static str, &'static str) {
1028        match self {
1029            Self::Global { .. } => ("setGlobalVariable", "modifyGlobalVariable"),
1030            Self::Player { .. } => ("setPlayerVariable", "modifyPlayerVariable"),
1031        }
1032    }
1033
1034    fn value_step(&self) -> usize {
1035        match self {
1036            Self::Global { .. } => 0,
1037            Self::Player { .. } => 1,
1038        }
1039    }
1040}
1041
1042fn variable_read(
1043    program: &wir::Program,
1044    value: wir::ValueId,
1045    target: ModifyTarget,
1046) -> Option<(u32, Option<wir::ValueId>)> {
1047    match (target, program.values.get(value).map(|node| &node.value)) {
1048        (ModifyTarget::Global { .. }, Some(wir::Value::GlobalVariable(variable))) => {
1049            Some((global_index(program, *variable), None))
1050        }
1051        (
1052            ModifyTarget::Player { .. },
1053            Some(wir::Value::PlayerVariable {
1054                player, variable, ..
1055            }),
1056        ) => Some((player_index(program, *variable), Some(*player))),
1057        (ModifyTarget::Player { .. }, Some(wir::Value::Call { name, args }))
1058            if name == "memberAccess" && args.len() == 2 =>
1059        {
1060            let Some(wir::Value::String(member)) =
1061                program.values.get(args[1]).map(|node| &node.value)
1062            else {
1063                return None;
1064            };
1065            Some((member_binding(program, member), Some(args[0])))
1066        }
1067        _ => None,
1068    }
1069}
1070
1071fn self_modify_difference(
1072    pair: SelfModify<'_>,
1073    path: &mut Vec<PathStep>,
1074) -> Option<DifferenceKind> {
1075    let base = path.len();
1076    let difference = self_modify_inner(pair, path);
1077    if difference.is_none() {
1078        path.truncate(base);
1079    }
1080    difference
1081}
1082
1083fn self_modify_inner(pair: SelfModify<'_>, path: &mut Vec<PathStep>) -> Option<DifferenceKind> {
1084    let (set_shape, modify_shape) = pair.target.shapes();
1085    let (first_shape, second_shape) = if pair.set_is_first {
1086        (set_shape, modify_shape)
1087    } else {
1088        (modify_shape, set_shape)
1089    };
1090    let Some(wir::Value::Call { name, args }) = pair
1091        .set_program
1092        .values
1093        .get(pair.set_value)
1094        .map(|node| &node.value)
1095    else {
1096        return Some(DifferenceKind::Shape {
1097            first: first_shape,
1098            second: second_shape,
1099        });
1100    };
1101    if args.len() != 2 {
1102        return Some(DifferenceKind::Shape {
1103            first: first_shape,
1104            second: second_shape,
1105        });
1106    }
1107    path.push(PathStep::Value(pair.target.value_step()));
1108    let Some((found, read_player)) = variable_read(pair.set_program, args[0], pair.target) else {
1109        path.pop();
1110        return Some(DifferenceKind::Shape {
1111            first: first_shape,
1112            second: second_shape,
1113        });
1114    };
1115    if found != pair.target.index() {
1116        path.push(PathStep::Value(0));
1117        let (first, second) = if pair.set_is_first {
1118            (found, pair.target.index())
1119        } else {
1120            (pair.target.index(), found)
1121        };
1122        return Some(DifferenceKind::Reference {
1123            table: pair.target.table(),
1124            first,
1125            second,
1126        });
1127    }
1128    if let ModifyTarget::Player { player, .. } = pair.target
1129        && let Some(read_player) = read_player
1130    {
1131        path.push(PathStep::Value(0));
1132        let difference = argument_difference(
1133            pair.set_program,
1134            pair.set_program,
1135            path,
1136            0,
1137            read_player,
1138            player,
1139        );
1140        match difference {
1141            Some(difference) => return Some(difference),
1142            None => {
1143                path.pop();
1144            }
1145        }
1146    }
1147    if canonical_value_name(name) != pair.op.catalog_id() {
1148        let (first, second) = if pair.set_is_first {
1149            (
1150                canonical_value_name(name).to_string(),
1151                pair.op.catalog_id().to_string(),
1152            )
1153        } else {
1154            (
1155                pair.op.catalog_id().to_string(),
1156                canonical_value_name(name).to_string(),
1157            )
1158        };
1159        return Some(DifferenceKind::Operation { first, second });
1160    }
1161    path.push(PathStep::Value(1));
1162    let (a, b, left, right) = if pair.set_is_first {
1163        (
1164            pair.set_program,
1165            pair.modify_program,
1166            args[1],
1167            pair.modify_value,
1168        )
1169    } else {
1170        (
1171            pair.modify_program,
1172            pair.set_program,
1173            pair.modify_value,
1174            args[1],
1175        )
1176    };
1177    value_difference(a, b, left, right, path)
1178}
1179
1180fn value_difference(
1181    a: &wir::Program,
1182    b: &wir::Program,
1183    left: wir::ValueId,
1184    right: wir::ValueId,
1185    path: &mut Vec<PathStep>,
1186) -> Option<DifferenceKind> {
1187    let (Some(first), Some(second)) = (
1188        a.values.get(left).map(|node| &node.value),
1189        b.values.get(right).map(|node| &node.value),
1190    ) else {
1191        return Some(DifferenceKind::Shape {
1192            first: "value",
1193            second: "missing",
1194        });
1195    };
1196    match (first, second) {
1197        (wir::Value::Number { value: first, .. }, wir::Value::Number { value: second, .. }) => {
1198            if *first == *second || (first.is_nan() && second.is_nan()) {
1199                None
1200            } else {
1201                Some(DifferenceKind::Scalar {
1202                    first: format_number(*first),
1203                    second: format_number(*second),
1204                })
1205            }
1206        }
1207        (wir::Value::String(first), wir::Value::String(second)) => {
1208            if first == second {
1209                None
1210            } else {
1211                Some(DifferenceKind::Scalar {
1212                    first: first.clone(),
1213                    second: second.clone(),
1214                })
1215            }
1216        }
1217        (wir::Value::LocalizedString(first), wir::Value::LocalizedString(second)) => {
1218            if first == second {
1219                None
1220            } else {
1221                Some(DifferenceKind::Scalar {
1222                    first: first.clone(),
1223                    second: second.clone(),
1224                })
1225            }
1226        }
1227        (wir::Value::Bool(first), wir::Value::Bool(second)) => {
1228            if first == second {
1229                None
1230            } else {
1231                Some(DifferenceKind::Scalar {
1232                    first: first.to_string(),
1233                    second: second.to_string(),
1234                })
1235            }
1236        }
1237        (wir::Value::Null, wir::Value::Null) => None,
1238        (wir::Value::Array(first), wir::Value::Array(second)) => {
1239            if first.len() != second.len() {
1240                return Some(DifferenceKind::Count {
1241                    element: "elements",
1242                    first: first.len(),
1243                    second: second.len(),
1244                });
1245            }
1246            for (index, (&first, &second)) in first.iter().zip(second).enumerate() {
1247                if let Some(kind) = argument_difference(a, b, path, index, first, second) {
1248                    return Some(kind);
1249                }
1250            }
1251            None
1252        }
1253        (
1254            wir::Value::Vector {
1255                x: first_x,
1256                y: first_y,
1257                z: first_z,
1258            },
1259            wir::Value::Vector {
1260                x: second_x,
1261                y: second_y,
1262                z: second_z,
1263            },
1264        ) => {
1265            for (index, (&first, &second)) in [
1266                (first_x, second_x),
1267                (first_y, second_y),
1268                (first_z, second_z),
1269            ]
1270            .into_iter()
1271            .enumerate()
1272            {
1273                if let Some(kind) = argument_difference(a, b, path, index, first, second) {
1274                    return Some(kind);
1275                }
1276            }
1277            None
1278        }
1279        (
1280            wir::Value::Enum {
1281                value_type: first_type,
1282                value: first_value,
1283            },
1284            wir::Value::Enum {
1285                value_type: second_type,
1286                value: second_value,
1287            },
1288        ) => {
1289            if first_type == second_type && first_value == second_value {
1290                None
1291            } else {
1292                Some(DifferenceKind::Scalar {
1293                    first: format!("{first_type}.{first_value}"),
1294                    second: format!("{second_type}.{second_value}"),
1295                })
1296            }
1297        }
1298        (wir::Value::GlobalVariable(first), wir::Value::GlobalVariable(second)) => {
1299            reference_difference(
1300                global_index(a, *first),
1301                global_index(b, *second),
1302                DeclarationTable::GlobalVariables,
1303            )
1304        }
1305        (
1306            wir::Value::PlayerVariable {
1307                player: first_player,
1308                variable: first_variable,
1309            },
1310            wir::Value::PlayerVariable {
1311                player: second_player,
1312                variable: second_variable,
1313            },
1314        ) => {
1315            if let Some(kind) = argument_difference(a, b, path, 0, *first_player, *second_player) {
1316                return Some(kind);
1317            }
1318            reference_difference(
1319                player_index(a, *first_variable),
1320                player_index(b, *second_variable),
1321                DeclarationTable::PlayerVariables,
1322            )
1323        }
1324        (wir::Value::Subroutine(first), wir::Value::Subroutine(second)) => reference_difference(
1325            subroutine_index(a, *first),
1326            subroutine_index(b, *second),
1327            DeclarationTable::Subroutines,
1328        ),
1329        (wir::Value::EventPlayer, wir::Value::EventPlayer) => None,
1330        (wir::Value::PlayerVariable { player, variable }, wir::Value::Call { name, args })
1331        | (wir::Value::Call { name, args }, wir::Value::PlayerVariable { player, variable })
1332            if name == "memberAccess" && args.len() == 2 =>
1333        {
1334            member_access_difference(
1335                a,
1336                b,
1337                matches!(first, wir::Value::PlayerVariable { .. }),
1338                *player,
1339                *variable,
1340                args,
1341                path,
1342            )
1343        }
1344        (
1345            wir::Value::Call {
1346                name: first_name,
1347                args: first_args,
1348            },
1349            wir::Value::Call {
1350                name: second_name,
1351                args: second_args,
1352            },
1353        ) => {
1354            if first_name == wir::AMBIGUOUS_ENUM_CALL && second_name == wir::AMBIGUOUS_ENUM_CALL {
1355                return (!ambiguous_enum_equivalent(a, b, first_args, second_args)).then_some(
1356                    DifferenceKind::Scalar {
1357                        first: describe_ambiguous_enum(a, first_args),
1358                        second: describe_ambiguous_enum(b, second_args),
1359                    },
1360                );
1361            }
1362            if first_name == "memberAccess"
1363                && second_name == "memberAccess"
1364                && first_args.len() == 2
1365                && second_args.len() == 2
1366            {
1367                return member_access_pair_difference(a, b, first_args, second_args, path);
1368            }
1369            if canonical_value_name(first_name) != canonical_value_name(second_name) {
1370                return Some(DifferenceKind::Scalar {
1371                    first: first_name.clone(),
1372                    second: second_name.clone(),
1373                });
1374            }
1375            if first_args.len() != second_args.len() {
1376                return Some(DifferenceKind::Count {
1377                    element: "arguments",
1378                    first: first_args.len(),
1379                    second: second_args.len(),
1380                });
1381            }
1382            for (index, (&first, &second)) in first_args.iter().zip(second_args).enumerate() {
1383                if let Some(kind) = argument_difference(a, b, path, index, first, second) {
1384                    return Some(kind);
1385                }
1386            }
1387            None
1388        }
1389        _ => Some(DifferenceKind::Shape {
1390            first: value_shape(first),
1391            second: value_shape(second),
1392        }),
1393    }
1394}
1395
1396fn member_access_difference(
1397    a: &wir::Program,
1398    b: &wir::Program,
1399    variable_is_first: bool,
1400    player: wir::ValueId,
1401    variable: wir::PlayerVarId,
1402    args: &[wir::ValueId],
1403    path: &mut Vec<PathStep>,
1404) -> Option<DifferenceKind> {
1405    let base = path.len();
1406    let difference = member_access_inner(a, b, variable_is_first, player, variable, args, path);
1407    if difference.is_none() {
1408        path.truncate(base);
1409    }
1410    difference
1411}
1412
1413fn member_access_inner(
1414    a: &wir::Program,
1415    b: &wir::Program,
1416    variable_is_first: bool,
1417    player: wir::ValueId,
1418    variable: wir::PlayerVarId,
1419    args: &[wir::ValueId],
1420    path: &mut Vec<PathStep>,
1421) -> Option<DifferenceKind> {
1422    let (variable_program, member_program) = if variable_is_first { (a, b) } else { (b, a) };
1423    let difference = if variable_is_first {
1424        argument_difference(a, b, path, 0, player, args[0])
1425    } else {
1426        argument_difference(a, b, path, 0, args[0], player)
1427    };
1428    if difference.is_some() {
1429        return difference;
1430    }
1431    let member_node = member_program.values.get(args[1]).map(|node| &node.value);
1432    let Some(wir::Value::String(member)) = member_node else {
1433        let (first, second) = if variable_is_first {
1434            ("playerVariable", "call")
1435        } else {
1436            ("call", "playerVariable")
1437        };
1438        return Some(DifferenceKind::Shape { first, second });
1439    };
1440    path.push(PathStep::Value(1));
1441    let member_index = member_binding(member_program, member);
1442    let variable_index = player_index(variable_program, variable);
1443    let (first, second) = if variable_is_first {
1444        (variable_index, member_index)
1445    } else {
1446        (member_index, variable_index)
1447    };
1448    reference_difference(first, second, DeclarationTable::PlayerVariables)
1449}
1450
1451fn member_binding(program: &wir::Program, member: &str) -> u32 {
1452    program
1453        .player_variables
1454        .iter()
1455        .find(|variable| variable.name == member)
1456        .map_or(u32::MAX, |variable| variable.index)
1457}
1458
1459fn member_access_pair_difference(
1460    a: &wir::Program,
1461    b: &wir::Program,
1462    first_args: &[wir::ValueId],
1463    second_args: &[wir::ValueId],
1464    path: &mut Vec<PathStep>,
1465) -> Option<DifferenceKind> {
1466    let base = path.len();
1467    let difference = member_access_pair_inner(a, b, first_args, second_args, path);
1468    if difference.is_none() {
1469        path.truncate(base);
1470    }
1471    difference
1472}
1473
1474fn member_access_pair_inner(
1475    a: &wir::Program,
1476    b: &wir::Program,
1477    first_args: &[wir::ValueId],
1478    second_args: &[wir::ValueId],
1479    path: &mut Vec<PathStep>,
1480) -> Option<DifferenceKind> {
1481    if let Some(kind) = argument_difference(a, b, path, 0, first_args[0], second_args[0]) {
1482        return Some(kind);
1483    }
1484    let members = (
1485        a.values.get(first_args[1]).map(|node| &node.value),
1486        b.values.get(second_args[1]).map(|node| &node.value),
1487    );
1488    let (Some(wir::Value::String(first_member)), Some(wir::Value::String(second_member))) = members
1489    else {
1490        return argument_difference(a, b, path, 1, first_args[1], second_args[1]);
1491    };
1492    path.push(PathStep::Value(1));
1493    let first_index = member_binding(a, first_member);
1494    let second_index = member_binding(b, second_member);
1495    if first_index != second_index {
1496        return Some(DifferenceKind::Reference {
1497            table: DeclarationTable::PlayerVariables,
1498            first: first_index,
1499            second: second_index,
1500        });
1501    }
1502    if first_index == u32::MAX && first_member != second_member {
1503        return Some(DifferenceKind::Scalar {
1504            first: first_member.clone(),
1505            second: second_member.clone(),
1506        });
1507    }
1508    None
1509}
1510
1511fn describe_ambiguous_enum(program: &wir::Program, args: &[wir::ValueId]) -> String {
1512    match wir::ambiguous_enum_parts_by_args(program, args) {
1513        Some((spelling, candidates)) => format!("{spelling} over {candidates:?}"),
1514        None => "ambiguous enum".to_string(),
1515    }
1516}
1517
1518fn action_shape(action: &wir::Action) -> &'static str {
1519    match action {
1520        wir::Action::SetGlobalVariable { .. } => "setGlobalVariable",
1521        wir::Action::ModifyGlobalVariable { .. } => "modifyGlobalVariable",
1522        wir::Action::SetPlayerVariable { .. } => "setPlayerVariable",
1523        wir::Action::ModifyPlayerVariable { .. } => "modifyPlayerVariable",
1524        wir::Action::AssignMember { op, .. } => {
1525            if op.is_some() {
1526                "modifyMember"
1527            } else {
1528                "assignMember"
1529            }
1530        }
1531        wir::Action::CallSubroutine { .. } => "callSubroutine",
1532        wir::Action::If { .. } => "if",
1533        wir::Action::While { .. } => "while",
1534        wir::Action::ForGlobalVariable { .. } => "forGlobalVariable",
1535        wir::Action::ForPlayerVariable { .. } => "forPlayerVariable",
1536        wir::Action::Disabled { .. } => "disabled",
1537        wir::Action::Call { .. } => "call",
1538    }
1539}
1540
1541fn value_shape(value: &wir::Value) -> &'static str {
1542    match value {
1543        wir::Value::Number { .. } => "number",
1544        wir::Value::String(_) => "string",
1545        wir::Value::LocalizedString(_) => "localizedString",
1546        wir::Value::Bool(_) => "bool",
1547        wir::Value::Null => "null",
1548        wir::Value::Array(_) => "array",
1549        wir::Value::Vector { .. } => "vector",
1550        wir::Value::Enum { .. } => "enum",
1551        wir::Value::GlobalVariable(_) => "globalVariable",
1552        wir::Value::PlayerVariable { .. } => "playerVariable",
1553        wir::Value::Subroutine(_) => "subroutine",
1554        wir::Value::EventPlayer => "eventPlayer",
1555        wir::Value::Call { .. } => "call",
1556    }
1557}