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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ProgramSide {
10 First,
12 Second,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DeclarationTable {
19 GlobalVariables,
21 PlayerVariables,
23 Subroutines,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum PathStep {
34 Condition(usize),
37 Action(usize),
42 Value(usize),
50 Branch(usize),
52 ElseBody,
54 Body,
56}
57
58#[derive(Debug, Clone, PartialEq)]
60#[non_exhaustive]
61pub enum DifferenceKind {
62 Malformed {
65 program: ProgramSide,
66 message: String,
67 },
68 Settings,
70 DeclarationTable {
72 table: DeclarationTable,
73 first: Vec<u32>,
74 second: Vec<u32>,
75 },
76 Count {
80 element: &'static str,
81 first: usize,
82 second: usize,
83 },
84 Event { first: String, second: String },
87 Shape {
90 first: &'static str,
91 second: &'static str,
92 },
93 Reference {
96 table: DeclarationTable,
97 first: u32,
98 second: u32,
99 },
100 Operation { first: String, second: String },
102 Scalar { first: String, second: String },
104}
105
106#[derive(Debug, Clone, PartialEq)]
108#[non_exhaustive]
109pub struct Difference {
110 pub rules: Option<(usize, usize)>,
113 pub path: Vec<PathStep>,
116 pub kind: DifferenceKind,
118}
119
120pub fn equivalent(first: &crate::Program, second: &crate::Program) -> bool {
123 difference(first, second).is_none()
124}
125
126pub 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}