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