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