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