1use crate::wir;
9
10use crate::catalog::{Catalog, Locale};
11use crate::core::signatures::{ExpectedDomain, NoExpectedDomain};
12use crate::frontend::parser;
13use crate::output::emitter;
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct RoundTripRecord {
18 pub input_identity: String,
20 pub locale: Locale,
22 pub catalog_version: u32,
24 pub parse_ok: bool,
26 pub emit_ok: bool,
28 pub reparse_ok: bool,
30 pub equivalent: bool,
32 pub error: Option<String>,
34}
35
36pub fn round_trip(input: &str, catalog: &Catalog, locale: &Locale) -> RoundTripRecord {
40 round_trip_with_context(input, catalog, locale, &NoExpectedDomain)
41}
42
43pub fn round_trip_with_context(
48 input: &str,
49 catalog: &Catalog,
50 locale: &Locale,
51 context: &dyn ExpectedDomain,
52) -> RoundTripRecord {
53 let input_identity = sha256(input);
54 let mut record = RoundTripRecord {
55 input_identity,
56 locale: locale.clone(),
57 catalog_version: catalog.schema_version,
58 parse_ok: false,
59 emit_ok: false,
60 reparse_ok: false,
61 equivalent: false,
62 error: None,
63 };
64 let first = match parser::parse_with_context(input, catalog, locale, context) {
65 Ok(program) => program,
66 Err(error) => {
67 record.error = Some(error.to_string());
68 return record;
69 }
70 };
71 record.parse_ok = true;
72 let emitted = match emitter::emit(&first, catalog, locale) {
73 Ok(text) => text,
74 Err(error) => {
75 record.error = Some(error.to_string());
76 return record;
77 }
78 };
79 record.emit_ok = true;
80 let second = match parser::parse_with_context(&emitted, catalog, locale, context) {
81 Ok(program) => program,
82 Err(error) => {
83 record.error = Some(error.to_string());
84 return record;
85 }
86 };
87 record.reparse_ok = true;
88 record.equivalent = equivalent(&first, &second);
89 record
90}
91
92pub fn equivalent(a: &crate::Program, b: &crate::Program) -> bool {
95 let (Ok(a), Ok(b)) = (a.to_wir(), b.to_wir()) else {
96 return false;
97 };
98 equivalent_wir(&a, &b)
99}
100
101#[doc(hidden)]
102pub fn equivalent_wir(a: &wir::Program, b: &wir::Program) -> bool {
103 if !settings_equivalent(a.settings.as_ref(), b.settings.as_ref()) {
104 return false;
105 }
106 let globals_a: Vec<_> = a
107 .global_variables
108 .iter()
109 .map(|v| (v.name.as_str(), v.index))
110 .collect();
111 let globals_b: Vec<_> = b
112 .global_variables
113 .iter()
114 .map(|v| (v.name.as_str(), v.index))
115 .collect();
116 if globals_a != globals_b {
117 return false;
118 }
119 let players_a: Vec<_> = a
120 .player_variables
121 .iter()
122 .map(|v| (v.name.as_str(), v.index))
123 .collect();
124 let players_b: Vec<_> = b
125 .player_variables
126 .iter()
127 .map(|v| (v.name.as_str(), v.index))
128 .collect();
129 if players_a != players_b {
130 return false;
131 }
132 let subs_a: Vec<_> = a
133 .subroutines
134 .iter()
135 .map(|s| (s.name.as_str(), s.index))
136 .collect();
137 let subs_b: Vec<_> = b
138 .subroutines
139 .iter()
140 .map(|s| (s.name.as_str(), s.index))
141 .collect();
142 if subs_a != subs_b {
143 return false;
144 }
145 let rules_a: Vec<_> = a
149 .rules
150 .iter()
151 .filter(|rule| !rule.actions.is_empty())
152 .collect();
153 let rules_b: Vec<_> = b
154 .rules
155 .iter()
156 .filter(|rule| !rule.actions.is_empty())
157 .collect();
158 if rules_a.len() != rules_b.len() {
159 return false;
160 }
161 for (rule_a, rule_b) in rules_a.into_iter().zip(rules_b) {
162 if !rule_equivalent(a, b, rule_a, rule_b) {
163 return false;
164 }
165 }
166 true
167}
168
169fn settings_equivalent(
170 left: Option<&crate::settings::Settings>,
171 right: Option<&crate::settings::Settings>,
172) -> bool {
173 match (left, right) {
174 (None, None) => true,
175 (Some(left), Some(right)) => nodes_equivalent(&left.children, &right.children),
176 _ => false,
177 }
178}
179
180fn nodes_equivalent(
181 left: &[crate::settings::SettingsNode],
182 right: &[crate::settings::SettingsNode],
183) -> bool {
184 left.len() == right.len()
185 && left
186 .iter()
187 .zip(right)
188 .all(|(left, right)| match (left, right) {
189 (
190 crate::settings::SettingsNode::Workshop { children: left, .. },
191 crate::settings::SettingsNode::Workshop {
192 children: right, ..
193 },
194 ) => nodes_equivalent(left, right),
195 (
196 crate::settings::SettingsNode::Group {
197 name: left_name,
198 children: left_children,
199 ..
200 },
201 crate::settings::SettingsNode::Group {
202 name: right_name,
203 children: right_children,
204 ..
205 },
206 ) => left_name == right_name && nodes_equivalent(left_children, right_children),
207 (
208 crate::settings::SettingsNode::Number {
209 name: left_name,
210 value: left_value,
211 ..
212 },
213 crate::settings::SettingsNode::Number {
214 name: right_name,
215 value: right_value,
216 ..
217 },
218 ) => left_name == right_name && float_equivalent(*left_value, *right_value),
219 (
220 crate::settings::SettingsNode::Bool {
221 name: left_name,
222 value: left_value,
223 ..
224 },
225 crate::settings::SettingsNode::Bool {
226 name: right_name,
227 value: right_value,
228 ..
229 },
230 ) => left_name == right_name && left_value == right_value,
231 (
232 crate::settings::SettingsNode::Flag {
233 name: left_name, ..
234 },
235 crate::settings::SettingsNode::Flag {
236 name: right_name, ..
237 },
238 ) => left_name == right_name,
239 (
240 crate::settings::SettingsNode::String {
241 name: left_name,
242 value: left_value,
243 ..
244 },
245 crate::settings::SettingsNode::String {
246 name: right_name,
247 value: right_value,
248 ..
249 },
250 ) => left_name == right_name && left_value == right_value,
251 (
252 crate::settings::SettingsNode::List {
253 name: left_name,
254 elements: left_elements,
255 ..
256 },
257 crate::settings::SettingsNode::List {
258 name: right_name,
259 elements: right_elements,
260 ..
261 },
262 ) => {
263 left_name == right_name
264 && left_elements.len() == right_elements.len()
265 && left_elements
266 .iter()
267 .zip(right_elements)
268 .all(|(left, right)| left.value == right.value)
269 }
270 (
271 crate::settings::SettingsNode::Raw {
272 name: left_name,
273 value: left_value,
274 ..
275 },
276 crate::settings::SettingsNode::Raw {
277 name: right_name,
278 value: right_value,
279 ..
280 },
281 ) => left_name == right_name && left_value == right_value,
282 _ => false,
283 })
284}
285
286fn float_equivalent(left: f64, right: f64) -> bool {
287 if left == right {
288 return true;
289 }
290 let scale = left.abs().max(right.abs()).max(1.0);
291 (left - right).abs() <= f64::EPSILON * scale * 4.0
292}
293
294fn rule_equivalent(
295 a: &wir::Program,
296 b: &wir::Program,
297 left: &wir::Rule,
298 right: &wir::Rule,
299) -> bool {
300 if left.name != right.name || left.disabled != right.disabled {
301 return false;
302 }
303 let event_a = event_equivalent(a, b, &left.event, &right.event);
304 if !event_a {
305 return false;
306 }
307 if left.conditions.len() != right.conditions.len() {
308 return false;
309 }
310 for (ca, cb) in left.conditions.iter().zip(right.conditions.iter()) {
311 if !value_equivalent(a, b, *ca, *cb) {
312 return false;
313 }
314 }
315 if left.actions.len() != right.actions.len() {
316 return false;
317 }
318 for (aa, ab) in left.actions.iter().zip(right.actions.iter()) {
319 if !action_equivalent(a, b, *aa, *ab) {
320 return false;
321 }
322 }
323 true
324}
325
326fn event_equivalent(
327 a: &wir::Program,
328 b: &wir::Program,
329 left: &wir::Event,
330 right: &wir::Event,
331) -> bool {
332 match (left, right) {
333 (wir::Event::Global, wir::Event::Global) => true,
334 (wir::Event::EachPlayer, wir::Event::EachPlayer) => true,
335 (wir::Event::EachPlayer, wir::Event::EachPlayerWithFilters { team, target })
336 | (wir::Event::EachPlayerWithFilters { team, target }, wir::Event::EachPlayer) => {
337 *team == wir::EventTeam::All && *target == wir::EventTarget::All
338 }
339 (
340 wir::Event::EachPlayerWithFilters {
341 team: team_a,
342 target: target_a,
343 },
344 wir::Event::EachPlayerWithFilters {
345 team: team_b,
346 target: target_b,
347 },
348 ) => team_a == team_b && target_a == target_b,
349 (
350 wir::Event::Player {
351 kind: kind_a,
352 team: team_a,
353 target: target_a,
354 },
355 wir::Event::Player {
356 kind: kind_b,
357 team: team_b,
358 target: target_b,
359 },
360 ) => kind_a == kind_b && team_a == team_b && target_a == target_b,
361 (wir::Event::Subroutine(sa), wir::Event::Subroutine(sb)) => {
362 let name_a = a.subroutines.get(*sa).map(|s| s.name.as_str());
363 let name_b = b.subroutines.get(*sb).map(|s| s.name.as_str());
364 name_a == name_b
365 }
366 _ => false,
367 }
368}
369
370fn action_equivalent(
371 a: &wir::Program,
372 b: &wir::Program,
373 left: wir::ActionId,
374 right: wir::ActionId,
375) -> bool {
376 let (Some(la), Some(rb)) = (a.actions.get(left), b.actions.get(right)) else {
377 return false;
378 };
379 match (la, rb) {
380 (
381 wir::Action::SetGlobalVariable {
382 variable: va,
383 value: x,
384 ..
385 },
386 wir::Action::SetGlobalVariable {
387 variable: vb,
388 value: y,
389 ..
390 },
391 ) => {
392 name_eq(a.global_variables.get(*va), b.global_variables.get(*vb))
393 && value_equivalent(a, b, *x, *y)
394 }
395 (
396 wir::Action::ModifyGlobalVariable {
397 variable: va,
398 op: oa,
399 value: x,
400 ..
401 },
402 wir::Action::ModifyGlobalVariable {
403 variable: vb,
404 op: ob,
405 value: y,
406 ..
407 },
408 ) => {
409 name_eq(a.global_variables.get(*va), b.global_variables.get(*vb))
410 && oa == ob
411 && value_equivalent(a, b, *x, *y)
412 }
413 (
414 wir::Action::SetPlayerVariable {
415 player: pa,
416 variable: va,
417 value: x,
418 ..
419 },
420 wir::Action::SetPlayerVariable {
421 player: pb,
422 variable: vb,
423 value: y,
424 ..
425 },
426 ) => {
427 value_equivalent(a, b, *pa, *pb)
428 && name_eq(a.player_variables.get(*va), b.player_variables.get(*vb))
429 && value_equivalent(a, b, *x, *y)
430 }
431 (
432 wir::Action::ModifyPlayerVariable {
433 player: pa,
434 variable: va,
435 op: oa,
436 value: x,
437 ..
438 },
439 wir::Action::ModifyPlayerVariable {
440 player: pb,
441 variable: vb,
442 op: ob,
443 value: y,
444 ..
445 },
446 ) => {
447 value_equivalent(a, b, *pa, *pb)
448 && name_eq(a.player_variables.get(*va), b.player_variables.get(*vb))
449 && oa == ob
450 && value_equivalent(a, b, *x, *y)
451 }
452 (
453 wir::Action::AssignMember {
454 target: ta,
455 op: oa,
456 value: xa,
457 ..
458 },
459 wir::Action::AssignMember {
460 target: tb,
461 op: ob,
462 value: xb,
463 ..
464 },
465 ) => oa == ob && value_equivalent(a, b, *ta, *tb) && value_equivalent(a, b, *xa, *xb),
466 (
467 wir::Action::CallSubroutine { subroutine: sa, .. },
468 wir::Action::CallSubroutine { subroutine: sb, .. },
469 ) => name_eq(a.subroutines.get(*sa), b.subroutines.get(*sb)),
470 (
471 wir::Action::If {
472 branches: ba,
473 else_body: ea,
474 ..
475 },
476 wir::Action::If {
477 branches: bb,
478 else_body: eb,
479 ..
480 },
481 ) => branches_equivalent(a, b, ba, bb) && bodies_equivalent(a, b, ea, eb),
482 (
483 wir::Action::While {
484 condition: ca,
485 body: ba,
486 ..
487 },
488 wir::Action::While {
489 condition: cb,
490 body: bb,
491 ..
492 },
493 ) => value_equivalent(a, b, *ca, *cb) && actions_equivalent(a, b, ba, bb),
494 (
495 wir::Action::ForGlobalVariable {
496 variable: va,
497 start: sa,
498 stop: ea,
499 step: pa,
500 body: ba,
501 ..
502 },
503 wir::Action::ForGlobalVariable {
504 variable: vb,
505 start: sb,
506 stop: eb,
507 step: pb,
508 body: bb,
509 ..
510 },
511 ) => {
512 name_eq(a.global_variables.get(*va), b.global_variables.get(*vb))
513 && value_equivalent(a, b, *sa, *sb)
514 && value_equivalent(a, b, *ea, *eb)
515 && value_equivalent(a, b, *pa, *pb)
516 && actions_equivalent(a, b, ba, bb)
517 }
518 (
519 wir::Action::ForPlayerVariable {
520 player: pa,
521 variable: va,
522 start: sa,
523 stop: ea,
524 step: sta,
525 body: ba,
526 ..
527 },
528 wir::Action::ForPlayerVariable {
529 player: pb,
530 variable: vb,
531 start: sb,
532 stop: eb,
533 step: stb,
534 body: bb,
535 ..
536 },
537 ) => {
538 value_equivalent(a, b, *pa, *pb)
539 && name_eq(a.player_variables.get(*va), b.player_variables.get(*vb))
540 && value_equivalent(a, b, *sa, *sb)
541 && value_equivalent(a, b, *ea, *eb)
542 && value_equivalent(a, b, *sta, *stb)
543 && actions_equivalent(a, b, ba, bb)
544 }
545 (
546 wir::Action::Call {
547 name: na, args: xa, ..
548 },
549 wir::Action::Call {
550 name: nb, args: xb, ..
551 },
552 ) => na == nb && values_equivalent(a, b, xa, xb),
553 _ => false,
554 }
555}
556
557fn branches_equivalent(
558 a: &wir::Program,
559 b: &wir::Program,
560 left: &[wir::IfBranch],
561 right: &[wir::IfBranch],
562) -> bool {
563 left.len() == right.len()
564 && left.iter().zip(right.iter()).all(|(la, rb)| {
565 value_equivalent(a, b, la.condition, rb.condition)
566 && actions_equivalent(a, b, &la.body, &rb.body)
567 })
568}
569
570fn actions_equivalent(
571 a: &wir::Program,
572 b: &wir::Program,
573 left: &[wir::ActionId],
574 right: &[wir::ActionId],
575) -> bool {
576 left.len() == right.len()
577 && left
578 .iter()
579 .zip(right.iter())
580 .all(|(la, rb)| action_equivalent(a, b, *la, *rb))
581}
582
583fn bodies_equivalent(
584 a: &wir::Program,
585 b: &wir::Program,
586 left: &Option<Vec<wir::ActionId>>,
587 right: &Option<Vec<wir::ActionId>>,
588) -> bool {
589 match (left, right) {
590 (Some(la), Some(rb)) => actions_equivalent(a, b, la, rb),
591 (None, None) => true,
592 _ => false,
593 }
594}
595
596fn value_equivalent(
597 a: &wir::Program,
598 b: &wir::Program,
599 left: wir::ValueId,
600 right: wir::ValueId,
601) -> bool {
602 let (Some(la), Some(rb)) = (a.values.get(left), b.values.get(right)) else {
603 return false;
604 };
605 match (&la.value, &rb.value) {
606 (wir::Value::Number { value: x, .. }, wir::Value::Number { value: y, .. }) => x == y,
607 (wir::Value::String(x), wir::Value::String(y)) => x == y,
608 (wir::Value::LocalizedString(x), wir::Value::LocalizedString(y)) => x == y,
609 (wir::Value::Bool(x), wir::Value::Bool(y)) => x == y,
610 (wir::Value::Null, wir::Value::Null) => true,
611 (wir::Value::Array(xa), wir::Value::Array(xb)) => values_equivalent(a, b, xa, xb),
612 (
613 wir::Value::Vector {
614 x: x1,
615 y: y1,
616 z: z1,
617 },
618 wir::Value::Vector {
619 x: x2,
620 y: y2,
621 z: z2,
622 },
623 ) => {
624 value_equivalent(a, b, *x1, *x2)
625 && value_equivalent(a, b, *y1, *y2)
626 && value_equivalent(a, b, *z1, *z2)
627 }
628 (
629 wir::Value::Enum {
630 value_type: t1,
631 value: v1,
632 },
633 wir::Value::Enum {
634 value_type: t2,
635 value: v2,
636 },
637 ) => t1 == t2 && v1 == v2,
638 (wir::Value::GlobalVariable(v1), wir::Value::GlobalVariable(v2)) => {
639 name_eq(a.global_variables.get(*v1), b.global_variables.get(*v2))
640 }
641 (
642 wir::Value::PlayerVariable {
643 player: p1,
644 variable: v1,
645 },
646 wir::Value::PlayerVariable {
647 player: p2,
648 variable: v2,
649 },
650 ) => {
651 value_equivalent(a, b, *p1, *p2)
652 && name_eq(a.player_variables.get(*v1), b.player_variables.get(*v2))
653 }
654 (wir::Value::Subroutine(s1), wir::Value::Subroutine(s2)) => {
655 name_eq(a.subroutines.get(*s1), b.subroutines.get(*s2))
656 }
657 (wir::Value::EventPlayer, wir::Value::EventPlayer) => true,
658 (wir::Value::PlayerVariable { player, variable }, wir::Value::Call { name, args })
659 if name == "memberAccess" && args.len() == 2 =>
660 {
661 let Some(wir::ValueNode {
662 value: wir::Value::String(member),
663 ..
664 }) = b.values.get(args[1])
665 else {
666 return false;
667 };
668 value_equivalent(a, b, *player, args[0])
669 && a.player_variables
670 .get(*variable)
671 .is_some_and(|value| value.name == *member)
672 }
673 (wir::Value::Call { name, args }, wir::Value::PlayerVariable { player, variable })
674 if name == "memberAccess" && args.len() == 2 =>
675 {
676 let Some(wir::ValueNode {
677 value: wir::Value::String(member),
678 ..
679 }) = a.values.get(args[1])
680 else {
681 return false;
682 };
683 value_equivalent(a, b, args[0], *player)
684 && b.player_variables
685 .get(*variable)
686 .is_some_and(|value| value.name == *member)
687 }
688 (wir::Value::Call { name: n1, args: x1 }, wir::Value::Call { name: n2, args: x2 })
689 if n1 == wir::AMBIGUOUS_ENUM_CALL && n2 == wir::AMBIGUOUS_ENUM_CALL =>
690 {
691 ambiguous_enum_equivalent(a, b, x1, x2)
692 }
693 (wir::Value::Call { name: n1, args: x1 }, wir::Value::Call { name: n2, args: x2 }) => {
694 canonical_value_name(n1) == canonical_value_name(n2) && values_equivalent(a, b, x1, x2)
695 }
696 _ => false,
697 }
698}
699
700fn ambiguous_enum_equivalent(
701 a: &wir::Program,
702 b: &wir::Program,
703 left: &[wir::ValueId],
704 right: &[wir::ValueId],
705) -> bool {
706 let Some((_, left_candidates)) = wir::ambiguous_enum_parts_by_args(a, left) else {
707 return false;
708 };
709 let Some((_, right_candidates)) = wir::ambiguous_enum_parts_by_args(b, right) else {
710 return false;
711 };
712 left_candidates == right_candidates
713}
714
715fn canonical_value_name(name: &str) -> &str {
716 match name {
717 "+" => "add",
718 "-" => "subtract",
719 "*" => "multiply",
720 "/" => "divide",
721 "len" => "countOf",
722 "abs" => "absoluteValue",
723 "sqrt" => "squareRoot",
724 _ => name,
725 }
726}
727
728fn values_equivalent(
729 a: &wir::Program,
730 b: &wir::Program,
731 left: &[wir::ValueId],
732 right: &[wir::ValueId],
733) -> bool {
734 left.len() == right.len()
735 && left
736 .iter()
737 .zip(right.iter())
738 .all(|(la, rb)| value_equivalent(a, b, *la, *rb))
739}
740
741fn name_eq<T: Named>(left: Option<&T>, right: Option<&T>) -> bool {
742 match (left, right) {
743 (Some(left), Some(right)) => left.name() == right.name(),
744 (None, None) => true,
745 _ => false,
746 }
747}
748
749trait Named {
750 fn name(&self) -> &str;
751}
752
753impl Named for wir::WorkshopVariable {
754 fn name(&self) -> &str {
755 &self.name
756 }
757}
758
759impl Named for wir::WorkshopSubroutine {
760 fn name(&self) -> &str {
761 &self.name
762 }
763}
764
765fn sha256(input: &str) -> String {
766 use sha2::{Digest, Sha256};
767 let mut hasher = Sha256::new();
768 hasher.update(input.as_bytes());
769 format!("{:x}", hasher.finalize())
770}