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)]
17#[non_exhaustive]
18pub struct RoundTripRecord {
19 pub input_identity: String,
21 pub locale: Locale,
23 pub catalog_version: u32,
25 pub parse_ok: bool,
27 pub emit_ok: bool,
29 pub reparse_ok: bool,
31 pub equivalent: bool,
33 pub error: Option<String>,
35}
36
37pub fn round_trip(input: &str, catalog: &Catalog, locale: &Locale) -> RoundTripRecord {
41 round_trip_with_context(input, catalog, locale, &NoExpectedDomain)
42}
43
44pub fn round_trip_with_context(
49 input: &str,
50 catalog: &Catalog,
51 locale: &Locale,
52 context: &dyn ExpectedDomain,
53) -> RoundTripRecord {
54 let input_identity = sha256(input);
55 let mut record = RoundTripRecord {
56 input_identity,
57 locale: locale.clone(),
58 catalog_version: catalog.schema_version(),
59 parse_ok: false,
60 emit_ok: false,
61 reparse_ok: false,
62 equivalent: false,
63 error: None,
64 };
65 let first = match parser::parse_with_context(input, catalog, locale, context) {
66 Ok(program) => program,
67 Err(error) => {
68 record.error = Some(error.to_string());
69 return record;
70 }
71 };
72 record.parse_ok = true;
73 let emitted = match emitter::emit(&first, catalog, locale) {
74 Ok(text) => text,
75 Err(error) => {
76 record.error = Some(error.to_string());
77 return record;
78 }
79 };
80 record.emit_ok = true;
81 let second = match parser::parse_with_context(&emitted, catalog, locale, context) {
82 Ok(program) => program,
83 Err(error) => {
84 record.error = Some(error.to_string());
85 return record;
86 }
87 };
88 record.reparse_ok = true;
89 record.equivalent = equivalent(&first, &second);
90 record
91}
92
93pub fn equivalent(a: &crate::Program, b: &crate::Program) -> bool {
96 let (Ok(a), Ok(b)) = (a.to_wir(), b.to_wir()) else {
97 return false;
98 };
99 equivalent_wir(&a, &b)
100}
101
102pub(crate) 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 ca.disabled != cb.disabled || !value_equivalent(a, b, ca.value, cb.value) {
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 (
362 wir::Event::Subroutine { subroutine: sa, .. },
363 wir::Event::Subroutine { subroutine: sb, .. },
364 ) => {
365 let name_a = a.subroutines.get(*sa).map(|s| s.name.as_str());
366 let name_b = b.subroutines.get(*sb).map(|s| s.name.as_str());
367 name_a == name_b
368 }
369 _ => false,
370 }
371}
372
373fn action_equivalent(
374 a: &wir::Program,
375 b: &wir::Program,
376 left: wir::ActionId,
377 right: wir::ActionId,
378) -> bool {
379 let (Some(la), Some(rb)) = (a.actions.get(left), b.actions.get(right)) else {
380 return false;
381 };
382 match (la, rb) {
383 (
384 wir::Action::SetGlobalVariable {
385 variable: va,
386 value: x,
387 ..
388 }
389 | wir::Action::ModifyGlobalVariable {
390 variable: va,
391 value: x,
392 ..
393 },
394 wir::Action::SetGlobalVariable {
395 variable: vb,
396 value: y,
397 ..
398 }
399 | wir::Action::ModifyGlobalVariable {
400 variable: vb,
401 value: y,
402 ..
403 },
404 ) => {
405 name_eq(a.global_variables.get(*va), b.global_variables.get(*vb))
406 && la.modify_op() == rb.modify_op()
407 && value_equivalent(a, b, *x, *y)
408 }
409 (
410 wir::Action::SetPlayerVariable {
411 player: pa,
412 variable: va,
413 value: x,
414 ..
415 }
416 | wir::Action::ModifyPlayerVariable {
417 player: pa,
418 variable: va,
419 value: x,
420 ..
421 },
422 wir::Action::SetPlayerVariable {
423 player: pb,
424 variable: vb,
425 value: y,
426 ..
427 }
428 | wir::Action::ModifyPlayerVariable {
429 player: pb,
430 variable: vb,
431 value: y,
432 ..
433 },
434 ) => {
435 value_equivalent(a, b, *pa, *pb)
436 && name_eq(a.player_variables.get(*va), b.player_variables.get(*vb))
437 && la.modify_op() == rb.modify_op()
438 && value_equivalent(a, b, *x, *y)
439 }
440 (
441 wir::Action::AssignMember {
442 target: ta,
443 op: oa,
444 value: xa,
445 ..
446 },
447 wir::Action::AssignMember {
448 target: tb,
449 op: ob,
450 value: xb,
451 ..
452 },
453 ) => oa == ob && value_equivalent(a, b, *ta, *tb) && value_equivalent(a, b, *xa, *xb),
454 (
455 wir::Action::CallSubroutine { subroutine: sa, .. },
456 wir::Action::CallSubroutine { subroutine: sb, .. },
457 ) => name_eq(a.subroutines.get(*sa), b.subroutines.get(*sb)),
458 (
459 wir::Action::If {
460 branches: ba,
461 else_body: ea,
462 ..
463 },
464 wir::Action::If {
465 branches: bb,
466 else_body: eb,
467 ..
468 },
469 ) => branches_equivalent(a, b, ba, bb) && bodies_equivalent(a, b, ea, eb),
470 (
471 wir::Action::While {
472 condition: ca,
473 body: ba,
474 ..
475 },
476 wir::Action::While {
477 condition: cb,
478 body: bb,
479 ..
480 },
481 ) => value_equivalent(a, b, *ca, *cb) && actions_equivalent(a, b, ba, bb),
482 (
483 wir::Action::ForGlobalVariable {
484 variable: va,
485 start: sa,
486 stop: ea,
487 step: pa,
488 body: ba,
489 ..
490 },
491 wir::Action::ForGlobalVariable {
492 variable: vb,
493 start: sb,
494 stop: eb,
495 step: pb,
496 body: bb,
497 ..
498 },
499 ) => {
500 name_eq(a.global_variables.get(*va), b.global_variables.get(*vb))
501 && value_equivalent(a, b, *sa, *sb)
502 && value_equivalent(a, b, *ea, *eb)
503 && value_equivalent(a, b, *pa, *pb)
504 && actions_equivalent(a, b, ba, bb)
505 }
506 (
507 wir::Action::ForPlayerVariable {
508 player: pa,
509 variable: va,
510 start: sa,
511 stop: ea,
512 step: sta,
513 body: ba,
514 ..
515 },
516 wir::Action::ForPlayerVariable {
517 player: pb,
518 variable: vb,
519 start: sb,
520 stop: eb,
521 step: stb,
522 body: bb,
523 ..
524 },
525 ) => {
526 value_equivalent(a, b, *pa, *pb)
527 && name_eq(a.player_variables.get(*va), b.player_variables.get(*vb))
528 && value_equivalent(a, b, *sa, *sb)
529 && value_equivalent(a, b, *ea, *eb)
530 && value_equivalent(a, b, *sta, *stb)
531 && actions_equivalent(a, b, ba, bb)
532 }
533 (wir::Action::Disabled { action: x, .. }, wir::Action::Disabled { action: y, .. }) => {
534 action_equivalent(a, b, *x, *y)
535 }
536 (
537 wir::Action::Call {
538 name: na, args: xa, ..
539 },
540 wir::Action::Call {
541 name: nb, args: xb, ..
542 },
543 ) => na == nb && values_equivalent(a, b, xa, xb),
544 _ => false,
545 }
546}
547
548fn branches_equivalent(
549 a: &wir::Program,
550 b: &wir::Program,
551 left: &[wir::IfBranch],
552 right: &[wir::IfBranch],
553) -> bool {
554 left.len() == right.len()
555 && left.iter().zip(right.iter()).all(|(la, rb)| {
556 value_equivalent(a, b, la.condition, rb.condition)
557 && actions_equivalent(a, b, &la.body, &rb.body)
558 })
559}
560
561fn actions_equivalent(
562 a: &wir::Program,
563 b: &wir::Program,
564 left: &[wir::ActionId],
565 right: &[wir::ActionId],
566) -> bool {
567 left.len() == right.len()
568 && left
569 .iter()
570 .zip(right.iter())
571 .all(|(la, rb)| action_equivalent(a, b, *la, *rb))
572}
573
574fn bodies_equivalent(
575 a: &wir::Program,
576 b: &wir::Program,
577 left: &Option<Vec<wir::ActionId>>,
578 right: &Option<Vec<wir::ActionId>>,
579) -> bool {
580 match (left, right) {
581 (Some(la), Some(rb)) => actions_equivalent(a, b, la, rb),
582 (None, None) => true,
583 _ => false,
584 }
585}
586
587fn value_equivalent(
588 a: &wir::Program,
589 b: &wir::Program,
590 left: wir::ValueId,
591 right: wir::ValueId,
592) -> bool {
593 let (Some(la), Some(rb)) = (a.values.get(left), b.values.get(right)) else {
594 return false;
595 };
596 match (&la.value, &rb.value) {
597 (wir::Value::Number { value: x, .. }, wir::Value::Number { value: y, .. }) => x == y,
598 (wir::Value::String(x), wir::Value::String(y)) => x == y,
599 (wir::Value::LocalizedString(x), wir::Value::LocalizedString(y)) => x == y,
600 (wir::Value::Bool(x), wir::Value::Bool(y)) => x == y,
601 (wir::Value::Null, wir::Value::Null) => true,
602 (wir::Value::Array(xa), wir::Value::Array(xb)) => values_equivalent(a, b, xa, xb),
603 (
604 wir::Value::Vector {
605 x: x1,
606 y: y1,
607 z: z1,
608 },
609 wir::Value::Vector {
610 x: x2,
611 y: y2,
612 z: z2,
613 },
614 ) => {
615 value_equivalent(a, b, *x1, *x2)
616 && value_equivalent(a, b, *y1, *y2)
617 && value_equivalent(a, b, *z1, *z2)
618 }
619 (
620 wir::Value::Enum {
621 value_type: t1,
622 value: v1,
623 },
624 wir::Value::Enum {
625 value_type: t2,
626 value: v2,
627 },
628 ) => t1 == t2 && v1 == v2,
629 (wir::Value::GlobalVariable(v1), wir::Value::GlobalVariable(v2)) => {
630 name_eq(a.global_variables.get(*v1), b.global_variables.get(*v2))
631 }
632 (
633 wir::Value::PlayerVariable {
634 player: p1,
635 variable: v1,
636 },
637 wir::Value::PlayerVariable {
638 player: p2,
639 variable: v2,
640 },
641 ) => {
642 value_equivalent(a, b, *p1, *p2)
643 && name_eq(a.player_variables.get(*v1), b.player_variables.get(*v2))
644 }
645 (wir::Value::Subroutine(s1), wir::Value::Subroutine(s2)) => {
646 name_eq(a.subroutines.get(*s1), b.subroutines.get(*s2))
647 }
648 (wir::Value::EventPlayer, wir::Value::EventPlayer) => true,
649 (wir::Value::PlayerVariable { player, variable }, wir::Value::Call { name, args })
650 if name == "memberAccess" && args.len() == 2 =>
651 {
652 let Some(wir::ValueNode {
653 value: wir::Value::String(member),
654 ..
655 }) = b.values.get(args[1])
656 else {
657 return false;
658 };
659 value_equivalent(a, b, *player, args[0])
660 && a.player_variables
661 .get(*variable)
662 .is_some_and(|value| value.name == *member)
663 }
664 (wir::Value::Call { name, args }, wir::Value::PlayerVariable { player, variable })
665 if name == "memberAccess" && args.len() == 2 =>
666 {
667 let Some(wir::ValueNode {
668 value: wir::Value::String(member),
669 ..
670 }) = a.values.get(args[1])
671 else {
672 return false;
673 };
674 value_equivalent(a, b, args[0], *player)
675 && b.player_variables
676 .get(*variable)
677 .is_some_and(|value| value.name == *member)
678 }
679 (wir::Value::Call { name: n1, args: x1 }, wir::Value::Call { name: n2, args: x2 })
680 if n1 == wir::AMBIGUOUS_ENUM_CALL && n2 == wir::AMBIGUOUS_ENUM_CALL =>
681 {
682 ambiguous_enum_equivalent(a, b, x1, x2)
683 }
684 (wir::Value::Call { name: n1, args: x1 }, wir::Value::Call { name: n2, args: x2 }) => {
685 canonical_value_name(n1) == canonical_value_name(n2) && values_equivalent(a, b, x1, x2)
686 }
687 _ => false,
688 }
689}
690
691fn ambiguous_enum_equivalent(
692 a: &wir::Program,
693 b: &wir::Program,
694 left: &[wir::ValueId],
695 right: &[wir::ValueId],
696) -> bool {
697 let Some((_, left_candidates)) = wir::ambiguous_enum_parts_by_args(a, left) else {
698 return false;
699 };
700 let Some((_, right_candidates)) = wir::ambiguous_enum_parts_by_args(b, right) else {
701 return false;
702 };
703 left_candidates == right_candidates
704}
705
706fn canonical_value_name(name: &str) -> &str {
707 match name {
708 "+" => "add",
709 "-" => "subtract",
710 "*" => "multiply",
711 "/" => "divide",
712 "len" => "countOf",
713 "abs" => "absoluteValue",
714 "sqrt" => "squareRoot",
715 _ => name,
716 }
717}
718
719fn values_equivalent(
720 a: &wir::Program,
721 b: &wir::Program,
722 left: &[wir::ValueId],
723 right: &[wir::ValueId],
724) -> bool {
725 left.len() == right.len()
726 && left
727 .iter()
728 .zip(right.iter())
729 .all(|(la, rb)| value_equivalent(a, b, *la, *rb))
730}
731
732fn name_eq<T: Named>(left: Option<&T>, right: Option<&T>) -> bool {
733 match (left, right) {
734 (Some(left), Some(right)) => left.name() == right.name(),
735 (None, None) => true,
736 _ => false,
737 }
738}
739
740trait Named {
741 fn name(&self) -> &str;
742}
743
744impl Named for wir::WorkshopVariable {
745 fn name(&self) -> &str {
746 &self.name
747 }
748}
749
750impl Named for wir::WorkshopSubroutine {
751 fn name(&self) -> &str {
752 &self.name
753 }
754}
755
756fn sha256(input: &str) -> String {
757 use sha2::{Digest, Sha256};
758 let mut hasher = Sha256::new();
759 hasher.update(input.as_bytes());
760 format!("{:x}", hasher.finalize())
761}