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