1mod backend;
6mod instruction;
7mod program;
8mod runtime;
9
10use self::{backend::BytecodeBackend, program::BytecodeProgram, runtime::BytecodeRuntime};
11use crate::rotation::{
12 buffer::DenseBuffer, context::ContextSchema, decision_trace::DecisionTraceTarget,
13 prepared::PreparedRotation, reference, result::EvalResult,
14};
15
16#[derive(Debug)]
17struct PortableExecutable {
18 program: BytecodeProgram,
19 runtime: BytecodeRuntime,
20}
21
22#[derive(Debug)]
24pub struct PortableRotation {
25 prepared: PreparedRotation,
26 executable: PortableExecutable,
27 trace: Option<DecisionTraceTarget>,
28}
29
30impl PortableRotation {
31 pub(super) fn compile(prepared: PreparedRotation) -> Self {
32 let executable = PortableExecutable::compile(&prepared);
33
34 Self {
35 prepared,
36 executable,
37 trace: None,
38 }
39 }
40
41 pub(super) fn evaluate(&mut self, buffer: &mut DenseBuffer, now_secs: f64) -> EvalResult {
42 match &self.trace {
43 Some(sink) => reference::evaluate_trace(&self.prepared, buffer, now_secs, sink.clone()),
44 None => self.executable.evaluate(buffer, now_secs),
45 }
46 }
47
48 pub(super) fn schema(&self) -> &ContextSchema {
49 self.executable.schema()
50 }
51
52 pub(super) const fn prepared(&self) -> &PreparedRotation {
53 &self.prepared
54 }
55
56 pub(super) fn set_trace(&mut self, sink: DecisionTraceTarget) {
57 self.trace = Some(sink);
58 }
59
60 pub(super) const fn has_trace(&self) -> bool {
61 self.trace.is_some()
62 }
63}
64
65impl PortableExecutable {
66 pub(super) fn compile(prepared: &PreparedRotation) -> Self {
67 let mut backend = BytecodeBackend::new();
68
69 prepared.lower(&mut backend);
70
71 let program = backend.finish(std::sync::Arc::clone(prepared.schema()));
72 let runtime = BytecodeRuntime::new(&program);
73
74 Self { program, runtime }
75 }
76
77 pub(super) fn evaluate(&mut self, buffer: &mut DenseBuffer, now_secs: f64) -> EvalResult {
79 self.runtime.execute(&self.program, buffer, now_secs)
80 }
81 pub(super) fn schema(&self) -> &ContextSchema {
84 self.program.schema()
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use googletest::prelude::*;
91
92 #[cfg(feature = "rotation-optimizations")]
93 use super::instruction::{FloatComparison, Instruction};
94 use super::*;
95 use crate::rotation::{
96 BufferRequirements, RotationPreparation, SpecResolver, parse_and_validate,
97 };
98
99 fn prepared(json: &str) -> PreparedRotation {
100 let rotation = parse_and_validate(json).expect("parse test rotation");
101 let resolver = SpecResolver::new("bytecode-test")
102 .spell("primary", 1)
103 .spell("fallback", 2)
104 .aura("guard_aura", 40);
105
106 RotationPreparation::prepare(&rotation, &resolver)
107 .expect("prepare test rotation")
108 .bind(&BufferRequirements::new())
109 .expect("bind test rotation")
110 }
111
112 #[cfg(feature = "rotation-optimizations")]
113 fn prepared_with_combo_points(json: &str) -> PreparedRotation {
114 let rotation = parse_and_validate(json).expect("parse combo point rotation");
115 let resolver = SpecResolver::new("bytecode-combo-point-test")
116 .spell("primary", 1)
117 .spell("fallback", 2)
118 .resource("combo_points");
119
120 RotationPreparation::prepare(&rotation, &resolver)
121 .expect("prepare combo point rotation")
122 .bind(&BufferRequirements::new())
123 .expect("bind combo point rotation")
124 }
125
126 #[cfg(feature = "rotation-optimizations")]
127 fn prepared_with_cooldown_bypass(json: &str) -> PreparedRotation {
128 use wowlab_types::sim::{AuraIdx, AuraOn, SpellIdx};
129
130 let rotation = parse_and_validate(json).expect("parse cooldown bypass rotation");
131 let resolver = SpecResolver::new("bytecode-cooldown-bypass-test")
132 .spell("primary", 1)
133 .spell("fallback", 2)
134 .aura("bypass", 40)
135 .spell_cooldown_bypass(SpellIdx(1), AuraIdx(40), AuraOn::Player);
136
137 RotationPreparation::prepare(&rotation, &resolver)
138 .expect("prepare cooldown bypass rotation")
139 .bind(&BufferRequirements::new())
140 .expect("bind cooldown bypass rotation")
141 }
142
143 fn enable_spells(buffer: &mut DenseBuffer) {
144 let handles: Vec<_> = buffer
145 .layout()
146 .bindings()
147 .spells()
148 .map(|(_, handle)| handle)
149 .collect();
150
151 for handle in handles {
152 buffer.spell_mut_at(handle).is_enabled = 1;
153 }
154 }
155
156 #[derive(Clone, Copy)]
157 #[cfg(feature = "rotation-optimizations")]
158 enum DirectFloatGuardSource {
159 ResourceCurrent,
160 UnitDistance,
161 }
162
163 #[cfg(feature = "rotation-optimizations")]
164 impl DirectFloatGuardSource {
165 const fn read_json(self) -> &'static str {
166 match self {
167 Self::ResourceCurrent => {
168 r#"{"type":"read","domain":"resource","key":"combo_points","name":"current"}"#
169 }
170 Self::UnitDistance => {
171 r#"{"type":"read","domain":"unit","key":"target","name":"distance"}"#
172 }
173 }
174 }
175
176 const fn literal_json(self) -> &'static str {
177 match self {
178 Self::ResourceCurrent => r#"{"type":"int","value":0}"#,
179 Self::UnitDistance => r#"{"type":"float","value":0.0}"#,
180 }
181 }
182
183 fn set(self, buffer: &mut DenseBuffer, value: f64) {
184 match self {
185 Self::ResourceCurrent => {
186 use wowlab_types::combat::ResourceType;
187
188 buffer
189 .resource_mut(ResourceType::ComboPoints)
190 .expect("combo point resource slot")
191 .current = value;
192 }
193 Self::UnitDistance => {
194 buffer
195 .unit_mut("target")
196 .expect("target unit slot")
197 .distance = value;
198 }
199 }
200 }
201
202 fn prepare(self, json: &str) -> PreparedRotation {
203 match self {
204 Self::ResourceCurrent => prepared_with_combo_points(json),
205 Self::UnitDistance => prepared(json),
206 }
207 }
208 }
209
210 #[gtest]
211 fn alternating_nested_short_circuit_conditions_reuse_register_banks_safely() {
212 let json = r#"{
213 "version":1,
214 "name":"nested-short-circuit",
215 "variables":{},
216 "actions":[
217 {"type":"cast","spell":"primary","condition":{
218 "type":"and","operands":[
219 {"type":"or","operands":[
220 {"type":"read","domain":"player","name":"is_in_combat"},
221 {"type":"bool","value":false}
222 ]},
223 {"type":"or","operands":[
224 {"type":"bool","value":false},
225 {"type":"read","domain":"player","name":"is_alive"}
226 ]}
227 ]
228 }},
229 {"type":"cast","spell":"fallback"}
230 ],
231 "lists":{}
232 }"#;
233 let prepared = prepared(json);
234 let mut executable = PortableExecutable::compile(&prepared);
235 let mut buffer = prepared.schema().new_buffer();
236
237 enable_spells(&mut buffer);
238
239 for step in 0..64 {
240 let chooses_primary = step % 2 == 0;
241
242 buffer.player_mut().in_combat = i32::from(chooses_primary);
243 buffer.player_mut().is_alive = i32::from(chooses_primary);
244
245 let result = executable.evaluate(&mut buffer, f64::from(step));
246
247 expect_that!(result.spell_id, eq(if chooses_primary { 1 } else { 2 }));
248 }
249 }
250
251 #[gtest]
252 fn empty_and_is_true_and_empty_or_is_false() {
253 let json = r#"{
254 "version":1,
255 "name":"empty-booleans",
256 "variables":{},
257 "actions":[
258 {"type":"cast","spell":"primary","condition":{"type":"or","operands":[]}},
259 {"type":"cast","spell":"fallback","condition":{"type":"and","operands":[]}}
260 ],
261 "lists":{}
262 }"#;
263 let prepared = prepared(json);
264 let mut executable = PortableExecutable::compile(&prepared);
265 let mut buffer = prepared.schema().new_buffer();
266
267 enable_spells(&mut buffer);
268
269 expect_that!(executable.evaluate(&mut buffer, 0.0).spell_id, eq(2));
270 }
271
272 #[cfg(feature = "rotation-optimizations")]
273 #[gtest]
274 fn cast_lowering_deduplicates_direct_same_spell_usability_reads() {
275 let baseline = prepared(
276 r#"{
277 "version":1,
278 "name":"baseline",
279 "variables":{},
280 "actions":[{"type":"cast","spell":"primary","condition":{
281 "type":"read","domain":"player","name":"is_alive"
282 }}],
283 "lists":{}
284 }"#,
285 );
286 let duplicate = prepared(
287 r#"{
288 "version":1,
289 "name":"duplicate",
290 "variables":{},
291 "actions":[{"type":"cast","spell":"primary","condition":{
292 "type":"and","operands":[
293 {"type":"read","domain":"spell","key":"primary","name":"is_usable"},
294 {"type":"read","domain":"spell","key":"primary","name":"is_usable"},
295 {"type":"read","domain":"player","name":"is_alive"}
296 ]
297 }}],
298 "lists":{}
299 }"#,
300 );
301 let baseline = PortableExecutable::compile(&baseline);
302 let duplicate = PortableExecutable::compile(&duplicate);
303
304 expect_that!(
305 duplicate.program.instructions.len(),
306 eq(baseline.program.instructions.len())
307 );
308 }
309
310 #[cfg(feature = "rotation-optimizations")]
311 #[gtest]
312 fn cast_lowering_keeps_nested_usability_reads() {
313 let direct = prepared(
314 r#"{
315 "version":1,
316 "name":"direct",
317 "variables":{},
318 "actions":[{"type":"cast","spell":"primary","condition":{
319 "type":"read","domain":"spell","key":"primary","name":"is_usable"
320 }}],
321 "lists":{}
322 }"#,
323 );
324 let nested = prepared(
325 r#"{
326 "version":1,
327 "name":"nested",
328 "variables":{},
329 "actions":[{"type":"cast","spell":"primary","condition":{
330 "type":"or","operands":[
331 {"type":"read","domain":"spell","key":"primary","name":"is_usable"},
332 {"type":"bool","value":true}
333 ]
334 }}],
335 "lists":{}
336 }"#,
337 );
338 let direct = PortableExecutable::compile(&direct);
339 let nested = PortableExecutable::compile(&nested);
340
341 expect_that!(
342 nested.program.instructions.len(),
343 gt(direct.program.instructions.len())
344 );
345 }
346
347 #[cfg(feature = "rotation-optimizations")]
348 #[gtest]
349 fn condition_lowering_removes_neutral_boolean_literals() {
350 let baseline = prepared(
351 r#"{
352 "version":1,
353 "name":"baseline",
354 "variables":{},
355 "actions":[{"type":"cast","spell":"primary","condition":{
356 "type":"read","domain":"player","name":"is_alive"
357 }}],
358 "lists":{}
359 }"#,
360 );
361 let neutral = prepared(
362 r#"{
363 "version":1,
364 "name":"neutral",
365 "variables":{},
366 "actions":[{"type":"cast","spell":"primary","condition":{
367 "type":"and","operands":[
368 {"type":"bool","value":true},
369 {"type":"read","domain":"player","name":"is_alive"},
370 {"type":"bool","value":true}
371 ]
372 }}],
373 "lists":{}
374 }"#,
375 );
376 let baseline = PortableExecutable::compile(&baseline);
377 let neutral = PortableExecutable::compile(&neutral);
378
379 expect_that!(
380 neutral.program.instructions.len(),
381 eq(baseline.program.instructions.len())
382 );
383 }
384
385 #[cfg(feature = "rotation-optimizations")]
386 #[gtest]
387 fn cast_conjunction_branches_directly_to_the_next_action() -> Result<()> {
388 let prepared = prepared(
389 r#"{
390 "version":1,
391 "name":"cast-continuation",
392 "variables":{},
393 "actions":[
394 {"type":"cast","spell":"primary","condition":{
395 "type":"and","operands":[
396 {"type":"read","domain":"player","name":"is_in_combat"},
397 {"type":"read","domain":"player","name":"is_alive"}
398 ]
399 }},
400 {"type":"cast","spell":"fallback"}
401 ],
402 "lists":{}
403 }"#,
404 )
405 .with_prevalidated_spell_usability();
406 let mut executable = PortableExecutable::compile(&prepared);
407 let primary_return = executable
408 .program
409 .instructions
410 .iter()
411 .position(|instruction| {
412 matches!(instruction, Instruction::ReturnCast { spell_id: 1, .. })
413 })
414 .expect("optimized primary cast has an unconditional return");
415 let failure_target = primary_return + 1;
416 let guard_targets: Vec<_> = executable.program.instructions[..primary_return]
417 .iter()
418 .filter_map(|instruction| match instruction {
419 Instruction::JumpIf {
420 target,
421 when_true: false,
422 ..
423 }
424 | Instruction::JumpIfBufferBool {
425 target,
426 when_true: false,
427 ..
428 } => Some(target.as_usize()),
429 _ => None,
430 })
431 .collect();
432
433 verify_that!(guard_targets.len(), eq(3))?;
434 verify_that!(
435 guard_targets.iter().all(|&target| target == failure_target),
436 is_true()
437 )?;
438 verify_that!(
439 executable
440 .program
441 .instructions
442 .iter()
443 .any(|instruction| matches!(instruction, Instruction::SetBool { .. })),
444 is_false()
445 )?;
446 verify_that!(executable.program.register_counts().bools(), eq(4))?;
447
448 let mut buffer = prepared.schema().new_buffer();
449
450 enable_spells(&mut buffer);
451 buffer.player_mut().in_combat = 1;
452 buffer.player_mut().is_alive = 1;
453 verify_that!(executable.evaluate(&mut buffer, 0.0).spell_id, eq(1))?;
454
455 buffer.player_mut().in_combat = 0;
456
457 verify_that!(executable.evaluate(&mut buffer, 0.0).spell_id, eq(2))
458 }
459
460 #[cfg(feature = "rotation-optimizations")]
461 #[gtest]
462 fn cast_guard_continuation_matches_private_structural_reference() -> Result<()> {
463 let json = r#"{
464 "version":1,
465 "name":"cast-continuation-parity",
466 "variables":{},
467 "actions":[
468 {"type":"cast","spell":"primary","condition":{
469 "type":"and","operands":[
470 {"type":"read","domain":"player","name":"is_in_combat"},
471 {"type":"read","domain":"player","name":"is_alive"}
472 ]
473 }},
474 {"type":"cast","spell":"fallback"}
475 ],
476 "lists":{}
477 }"#;
478 let portable_prepared = prepared(json).with_prevalidated_spell_usability();
479 let reference_prepared = prepared(json).with_prevalidated_spell_usability();
480 #[cfg(feature = "jit")]
481 let jit_prepared = prepared(json).with_prevalidated_spell_usability();
482 let mut executable = PortableExecutable::compile(&portable_prepared);
483 #[cfg(feature = "jit")]
484 let mut jit = crate::rotation::RotationEngine::jit(jit_prepared)
485 .expect("compile cast continuation JIT fixture");
486 let mut portable_buffer = portable_prepared.schema().new_buffer();
487 let mut reference_buffer = reference_prepared.schema().new_buffer();
488 #[cfg(feature = "jit")]
489 let mut jit_buffer = jit.schema().new_buffer();
490
491 enable_spells(&mut portable_buffer);
492 enable_spells(&mut reference_buffer);
493 #[cfg(feature = "jit")]
494 enable_spells(&mut jit_buffer);
495
496 for (in_combat, is_alive) in [(false, false), (true, false), (false, true), (true, true)] {
497 for buffer in [&mut portable_buffer, &mut reference_buffer] {
498 buffer.player_mut().in_combat = i32::from(in_combat);
499 buffer.player_mut().is_alive = i32::from(is_alive);
500 }
501
502 #[cfg(feature = "jit")]
503 {
504 jit_buffer.player_mut().in_combat = i32::from(in_combat);
505 jit_buffer.player_mut().is_alive = i32::from(is_alive);
506 }
507
508 let portable_result = executable.evaluate(&mut portable_buffer, 0.0);
509 let reference_result =
510 reference::evaluate_oracle(&reference_prepared, &mut reference_buffer, 0.0);
511
512 verify_that!(portable_result, eq(reference_result))?;
513 verify_that!(portable_buffer.bytes(), eq(reference_buffer.bytes()))?;
514
515 #[cfg(feature = "jit")]
516 {
517 let jit_result = jit.evaluate(&mut jit_buffer, 0.0);
518
519 verify_that!(portable_result, eq(jit_result))?;
520 verify_that!(portable_buffer.bytes(), eq(jit_buffer.bytes()))?;
521 }
522 }
523
524 Ok(())
525 }
526
527 #[cfg(feature = "rotation-optimizations")]
528 #[gtest]
529 fn cooldown_ready_cast_guard_fuses_and_matches_all_backends() -> Result<()> {
530 use wowlab_types::sim::SpellIdx;
531
532 let json = r#"{
533 "version":1,
534 "name":"cooldown-ready-guard",
535 "variables":{},
536 "actions":[
537 {"type":"cast","spell":"primary","condition":{
538 "type":"read","domain":"cooldown","key":"primary","name":"is_ready"
539 }},
540 {"type":"cast","spell":"fallback"}
541 ],
542 "lists":{}
543 }"#;
544 let portable_prepared = prepared(json).with_prevalidated_spell_usability();
545 let reference_prepared = prepared(json).with_prevalidated_spell_usability();
546 #[cfg(feature = "jit")]
547 let jit_prepared = prepared(json).with_prevalidated_spell_usability();
548 let mut executable = PortableExecutable::compile(&portable_prepared);
549 #[cfg(feature = "jit")]
550 let mut jit = crate::rotation::RotationEngine::jit(jit_prepared)
551 .expect("compile cooldown-ready guard JIT fixture");
552 let primary_return = executable
553 .program
554 .instructions
555 .iter()
556 .position(|instruction| {
557 matches!(instruction, Instruction::ReturnCast { spell_id: 1, .. })
558 })
559 .expect("cooldown guard has an unconditional primary return");
560 let fused_guards: Vec<_> = executable.program.instructions[..primary_return]
561 .iter()
562 .filter_map(|instruction| match instruction {
563 Instruction::JumpIfCooldownNotReady {
564 bypass_aura_offset,
565 target,
566 ..
567 } => Some((*bypass_aura_offset, target.as_usize())),
568 _ => None,
569 })
570 .collect();
571
572 verify_that!(fused_guards.as_slice(), eq(&[(None, primary_return + 1)]))?;
573 verify_that!(
574 executable.program.instructions[..primary_return]
575 .iter()
576 .any(|instruction| matches!(
577 instruction,
578 Instruction::CmpGtI { .. }
579 | Instruction::CmpLteF { .. }
580 | Instruction::SelectB { .. }
581 | Instruction::OrB { .. }
582 )),
583 is_false()
584 )?;
585
586 let mut portable_buffer = portable_prepared.schema().new_buffer();
587 let mut reference_buffer = reference_prepared.schema().new_buffer();
588 #[cfg(feature = "jit")]
589 let mut jit_buffer = jit.schema().new_buffer();
590
591 enable_spells(&mut portable_buffer);
592 enable_spells(&mut reference_buffer);
593 #[cfg(feature = "jit")]
594 enable_spells(&mut jit_buffer);
595
596 for (ready_at, current_charges, max_charges, now, expected_spell) in [
597 (8.0, 0, 1, 10.0, 1),
598 (12.0, 0, 1, 10.0, 2),
599 (999.0, 1, 2, 10.0, 1),
600 (999.0, 0, 2, 10.0, 2),
601 ] {
602 for buffer in [&mut portable_buffer, &mut reference_buffer] {
603 let cooldown = buffer
604 .layout()
605 .cooldown_handle(SpellIdx(1))
606 .expect("primary cooldown handle");
607 let slot = buffer.cooldown_mut_at(cooldown);
608
609 slot.ready_at = ready_at;
610 slot.current_charges = current_charges;
611 slot.max_charges = max_charges;
612 }
613
614 #[cfg(feature = "jit")]
615 {
616 let cooldown = jit_buffer
617 .layout()
618 .cooldown_handle(SpellIdx(1))
619 .expect("primary JIT cooldown handle");
620 let slot = jit_buffer.cooldown_mut_at(cooldown);
621
622 slot.ready_at = ready_at;
623 slot.current_charges = current_charges;
624 slot.max_charges = max_charges;
625 }
626
627 let portable_result = executable.evaluate(&mut portable_buffer, now);
628 let reference_result =
629 reference::evaluate_oracle(&reference_prepared, &mut reference_buffer, now);
630
631 verify_that!(portable_result.spell_id, eq(expected_spell))?;
632 verify_that!(portable_result, eq(reference_result))?;
633 verify_that!(portable_buffer.bytes(), eq(reference_buffer.bytes()))?;
634
635 #[cfg(feature = "jit")]
636 {
637 let jit_result = jit.evaluate(&mut jit_buffer, now);
638
639 verify_that!(portable_result, eq(jit_result))?;
640 verify_that!(portable_buffer.bytes(), eq(jit_buffer.bytes()))?;
641 }
642 }
643
644 Ok(())
645 }
646
647 #[cfg(feature = "rotation-optimizations")]
648 fn direct_float_comparison_result(comparison: FloatComparison, lhs: f64) -> bool {
649 use wowlab_types::numeric::{float_eq, float_ne};
650
651 match comparison {
652 FloatComparison::Gt => lhs > 0.0,
653 FloatComparison::Gte => lhs >= 0.0,
654 FloatComparison::Lt => lhs < 0.0,
655 FloatComparison::Lte => lhs <= 0.0,
656 FloatComparison::Eq => float_eq(lhs, 0.0),
657 FloatComparison::Ne => float_ne(lhs, 0.0),
658 }
659 }
660
661 #[cfg(feature = "rotation-optimizations")]
662 #[gtest]
663 fn direct_float_literal_cast_guards_fuse_and_match_all_backends() -> Result<()> {
664 use wowlab_types::numeric::EPSILON;
665
666 let comparisons = [
667 ("gt", FloatComparison::Gt),
668 ("gte", FloatComparison::Gte),
669 ("lt", FloatComparison::Lt),
670 ("lte", FloatComparison::Lte),
671 ("eq", FloatComparison::Eq),
672 ("ne", FloatComparison::Ne),
673 ];
674 let values = [
675 f64::NEG_INFINITY,
676 -1.0,
677 -EPSILON,
678 -EPSILON / 2.0,
679 -0.0,
680 0.0,
681 EPSILON / 2.0,
682 EPSILON,
683 1.0,
684 f64::INFINITY,
685 f64::NAN,
686 ];
687 let sources = [
688 DirectFloatGuardSource::ResourceCurrent,
689 DirectFloatGuardSource::UnitDistance,
690 ];
691
692 for source in sources {
693 let source_comparisons = &comparisons;
694
695 for &(operator, expected_comparison) in source_comparisons {
696 let json = format!(
697 r#"{{
698 "version":1,
699 "name":"direct-float-literal-guard",
700 "variables":{{}},
701 "actions":[
702 {{"type":"cast","spell":"primary","condition":{{
703 "type":"and","operands":[
704 {{"type":"compare","op":"{operator}","left":{},"right":{}}},
705 {{"type":"read","domain":"player","name":"is_alive"}}
706 ]
707 }}}},
708 {{"type":"cast","spell":"fallback"}}
709 ],
710 "lists":{{}}
711 }}"#,
712 source.read_json(),
713 source.literal_json(),
714 );
715 let portable_prepared = source.prepare(&json).with_prevalidated_spell_usability();
716 let reference_prepared = source.prepare(&json).with_prevalidated_spell_usability();
717 #[cfg(feature = "jit")]
718 let jit_prepared = source.prepare(&json).with_prevalidated_spell_usability();
719 let mut executable = PortableExecutable::compile(&portable_prepared);
720 #[cfg(feature = "jit")]
721 let mut jit = crate::rotation::RotationEngine::jit(jit_prepared)
722 .expect("compile direct float literal guard JIT fixture");
723 let primary_return = executable
724 .program
725 .instructions
726 .iter()
727 .position(|instruction| {
728 matches!(instruction, Instruction::ReturnCast { spell_id: 1, .. })
729 })
730 .expect("direct float guard has an unconditional primary return");
731 let failure_target = primary_return + 1;
732 let fused_guards: Vec<_> = executable.program.instructions[..primary_return]
733 .iter()
734 .enumerate()
735 .filter_map(|(position, instruction)| match instruction {
736 Instruction::JumpIfBufferF64ComparisonFalse {
737 rhs,
738 comparison,
739 target,
740 ..
741 } => Some((position, *rhs, *comparison, target.as_usize())),
742 _ => None,
743 })
744 .collect();
745 let buffer_guard_positions: Vec<_> = executable.program.instructions
746 [..primary_return]
747 .iter()
748 .enumerate()
749 .filter_map(|(position, instruction)| match instruction {
750 Instruction::JumpIfBufferBool {
751 target,
752 when_true: false,
753 ..
754 } => Some((position, target.as_usize())),
755 _ => None,
756 })
757 .collect();
758
759 verify_that!(fused_guards.len(), eq(1))?;
760 let (fused_position, rhs, actual_comparison, target) = fused_guards[0];
761
762 verify_that!(rhs.to_bits(), eq(0.0_f64.to_bits()))?;
763 verify_that!(actual_comparison, eq(expected_comparison))?;
764 verify_that!(target, eq(failure_target))?;
765 verify_that!(buffer_guard_positions.len(), eq(2))?;
766 verify_that!(buffer_guard_positions[0].0, lt(fused_position))?;
767 verify_that!(buffer_guard_positions[1].0, gt(fused_position))?;
768 verify_that!(
769 buffer_guard_positions
770 .iter()
771 .all(|&(_, guard_target)| guard_target == failure_target),
772 is_true()
773 )?;
774 verify_that!(
775 executable.program.instructions[..primary_return]
776 .iter()
777 .any(|instruction| matches!(
778 instruction,
779 Instruction::LoadF64 { .. }
780 | Instruction::IntToFloat { .. }
781 | Instruction::CmpGtF { .. }
782 | Instruction::CmpGteF { .. }
783 | Instruction::CmpLtF { .. }
784 | Instruction::CmpLteF { .. }
785 | Instruction::CmpEqF { .. }
786 | Instruction::CmpNeF { .. }
787 )),
788 is_false()
789 )?;
790
791 let mut portable_buffer = portable_prepared.schema().new_buffer();
792 let mut reference_buffer = reference_prepared.schema().new_buffer();
793 #[cfg(feature = "jit")]
794 let mut jit_buffer = jit.schema().new_buffer();
795
796 enable_spells(&mut portable_buffer);
797 enable_spells(&mut reference_buffer);
798 portable_buffer.player_mut().is_alive = 1;
799 reference_buffer.player_mut().is_alive = 1;
800 #[cfg(feature = "jit")]
801 {
802 enable_spells(&mut jit_buffer);
803 jit_buffer.player_mut().is_alive = 1;
804 }
805
806 for lhs in values {
807 source.set(&mut portable_buffer, lhs);
808 source.set(&mut reference_buffer, lhs);
809 #[cfg(feature = "jit")]
810 source.set(&mut jit_buffer, lhs);
811
812 let portable_result = executable.evaluate(&mut portable_buffer, 10.0);
813 let reference_result = reference::evaluate_oracle(
814 &reference_prepared,
815 &mut reference_buffer,
816 10.0,
817 );
818 let expected_spell = if direct_float_comparison_result(expected_comparison, lhs)
819 {
820 1
821 } else {
822 2
823 };
824
825 verify_that!(portable_result.spell_id, eq(expected_spell))?;
826 verify_that!(portable_result, eq(reference_result))?;
827 verify_that!(portable_buffer.bytes(), eq(reference_buffer.bytes()))?;
828
829 #[cfg(feature = "jit")]
830 {
831 let jit_result = jit.evaluate(&mut jit_buffer, 10.0);
832
833 verify_that!(portable_result, eq(jit_result))?;
834 verify_that!(portable_buffer.bytes(), eq(jit_buffer.bytes()))?;
835 }
836 }
837 }
838 }
839
840 Ok(())
841 }
842
843 #[cfg(feature = "rotation-optimizations")]
844 #[gtest]
845 fn cooldown_ready_guard_fusion_preserves_bypass_semantics() -> Result<()> {
846 use wowlab_types::sim::{AuraIdx, AuraOn, AuraProjectionKey, SpellIdx};
847
848 fn set_cooldown_and_bypass(buffer: &mut DenseBuffer, bypass_expires_at: f64) {
849 let cooldown = buffer
850 .layout()
851 .cooldown_handle(SpellIdx(1))
852 .expect("primary cooldown handle");
853 let bypass = buffer
854 .layout()
855 .bindings()
856 .aura_projections()
857 .find_map(|(key, handle)| {
858 (key == AuraProjectionKey::new(AuraIdx(40), AuraOn::Player)).then_some(handle)
859 })
860 .expect("cooldown bypass projection handle");
861 let slot = buffer.cooldown_mut_at(cooldown);
862
863 slot.ready_at = 99.0;
864 slot.current_charges = 0;
865 slot.max_charges = 1;
866 buffer.aura_projection_mut_at(bypass).expires_at = bypass_expires_at;
867 }
868
869 let json = r#"{
870 "version":1,
871 "name":"cooldown-ready-bypass-guard",
872 "variables":{},
873 "actions":[
874 {"type":"cast","spell":"primary","condition":{
875 "type":"read","domain":"cooldown","key":"primary","name":"is_ready"
876 }},
877 {"type":"cast","spell":"fallback"}
878 ],
879 "lists":{}
880 }"#;
881 let portable_prepared =
882 prepared_with_cooldown_bypass(json).with_prevalidated_spell_usability();
883 let reference_prepared =
884 prepared_with_cooldown_bypass(json).with_prevalidated_spell_usability();
885 #[cfg(feature = "jit")]
886 let jit_prepared = prepared_with_cooldown_bypass(json).with_prevalidated_spell_usability();
887 let mut executable = PortableExecutable::compile(&portable_prepared);
888 #[cfg(feature = "jit")]
889 let mut jit = crate::rotation::RotationEngine::jit(jit_prepared)
890 .expect("compile cooldown bypass guard JIT fixture");
891 let primary_return = executable
892 .program
893 .instructions
894 .iter()
895 .position(|instruction| {
896 matches!(instruction, Instruction::ReturnCast { spell_id: 1, .. })
897 })
898 .expect("cooldown bypass guard has an unconditional primary return");
899 let fused_guards: Vec<_> = executable.program.instructions[..primary_return]
900 .iter()
901 .filter_map(|instruction| match instruction {
902 Instruction::JumpIfCooldownNotReady {
903 bypass_aura_offset: Some(bypass_aura_offset),
904 target,
905 ..
906 } => Some((*bypass_aura_offset, target.as_usize())),
907 _ => None,
908 })
909 .collect();
910
911 verify_that!(fused_guards.len(), eq(1))?;
912 verify_that!(fused_guards[0].1, eq(primary_return + 1))?;
913
914 let mut portable_buffer = portable_prepared.schema().new_buffer();
915 let mut reference_buffer = reference_prepared.schema().new_buffer();
916 #[cfg(feature = "jit")]
917 let mut jit_buffer = jit.schema().new_buffer();
918
919 enable_spells(&mut portable_buffer);
920 enable_spells(&mut reference_buffer);
921 #[cfg(feature = "jit")]
922 enable_spells(&mut jit_buffer);
923
924 for (bypass_expires_at, expected_spell) in [(20.0, 1), (10.0, 2), (0.0, 2)] {
925 set_cooldown_and_bypass(&mut portable_buffer, bypass_expires_at);
926 set_cooldown_and_bypass(&mut reference_buffer, bypass_expires_at);
927 #[cfg(feature = "jit")]
928 set_cooldown_and_bypass(&mut jit_buffer, bypass_expires_at);
929
930 let portable_result = executable.evaluate(&mut portable_buffer, 10.0);
931 let reference_result =
932 reference::evaluate_oracle(&reference_prepared, &mut reference_buffer, 10.0);
933
934 verify_that!(portable_result.spell_id, eq(expected_spell))?;
935 verify_that!(portable_result, eq(reference_result))?;
936 verify_that!(portable_buffer.bytes(), eq(reference_buffer.bytes()))?;
937
938 #[cfg(feature = "jit")]
939 {
940 let jit_result = jit.evaluate(&mut jit_buffer, 10.0);
941
942 verify_that!(portable_result, eq(jit_result))?;
943 verify_that!(portable_buffer.bytes(), eq(jit_buffer.bytes()))?;
944 }
945 }
946
947 Ok(())
948 }
949
950 #[cfg(feature = "rotation-optimizations")]
951 #[gtest]
952 fn timestamp_state_cast_guards_fuse_and_match_all_backends() -> Result<()> {
953 use wowlab_types::sim::{AuraIdx, AuraOn, AuraProjectionKey};
954
955 fn set_expiry(buffer: &mut DenseBuffer, expires_at: f64) {
956 let aura = buffer
957 .layout()
958 .bindings()
959 .aura_projections()
960 .find_map(|(key, handle)| {
961 (key == AuraProjectionKey::new(AuraIdx(40), AuraOn::Player)).then_some(handle)
962 })
963 .expect("guard aura projection handle");
964
965 buffer.aura_projection_mut_at(aura).expires_at = expires_at;
966 }
967
968 for (field_name, expected_active) in [("is_active", true), ("is_inactive", false)] {
969 let json = format!(
970 r#"{{
971 "version":1,
972 "name":"timestamp-state-guard",
973 "variables":{{}},
974 "actions":[
975 {{"type":"cast","spell":"primary","condition":{{
976 "type":"and","operands":[
977 {{"type":"read","domain":"aura","key":"guard_aura","name":"{field_name}"}},
978 {{"type":"read","domain":"player","name":"is_alive"}}
979 ]
980 }}}},
981 {{"type":"cast","spell":"fallback"}}
982 ],
983 "lists":{{}}
984 }}"#
985 );
986 let portable_prepared = prepared(&json).with_prevalidated_spell_usability();
987 let reference_prepared = prepared(&json).with_prevalidated_spell_usability();
988 #[cfg(feature = "jit")]
989 let jit_prepared = prepared(&json).with_prevalidated_spell_usability();
990 let mut executable = PortableExecutable::compile(&portable_prepared);
991 #[cfg(feature = "jit")]
992 let mut jit = crate::rotation::RotationEngine::jit(jit_prepared)
993 .expect("compile timestamp-state guard JIT fixture");
994 let primary_return = executable
995 .program
996 .instructions
997 .iter()
998 .position(|instruction| {
999 matches!(instruction, Instruction::ReturnCast { spell_id: 1, .. })
1000 })
1001 .expect("timestamp-state guard has an unconditional primary return");
1002 let failure_target = primary_return + 1;
1003 let (timestamp_position, timestamp_target) = executable.program.instructions
1004 [..primary_return]
1005 .iter()
1006 .enumerate()
1007 .find_map(|(position, instruction)| match instruction {
1008 Instruction::JumpIfTimestampStateMismatch {
1009 expected_active: actual_expected,
1010 target,
1011 ..
1012 } if *actual_expected == expected_active => Some((position, target.as_usize())),
1013 _ => None,
1014 })
1015 .expect("timestamp-state guard is fused");
1016 let buffer_guard_positions: Vec<_> = executable.program.instructions[..primary_return]
1017 .iter()
1018 .enumerate()
1019 .filter_map(|(position, instruction)| match instruction {
1020 Instruction::JumpIfBufferBool {
1021 target,
1022 when_true: false,
1023 ..
1024 } => Some((position, target.as_usize())),
1025 _ => None,
1026 })
1027 .collect();
1028
1029 verify_that!(timestamp_target, eq(failure_target))?;
1030 verify_that!(buffer_guard_positions.len(), eq(2))?;
1031 verify_that!(buffer_guard_positions[0].0, lt(timestamp_position))?;
1032 verify_that!(buffer_guard_positions[1].0, gt(timestamp_position))?;
1033 verify_that!(
1034 buffer_guard_positions
1035 .iter()
1036 .all(|&(_, target)| target == failure_target),
1037 is_true()
1038 )?;
1039 verify_that!(
1040 executable.program.instructions[..primary_return]
1041 .iter()
1042 .any(|instruction| matches!(
1043 instruction,
1044 Instruction::CmpGtF { .. }
1045 | Instruction::CmpLtF { .. }
1046 | Instruction::AndB { .. }
1047 | Instruction::NotB { .. }
1048 )),
1049 is_false()
1050 )?;
1051
1052 let mut portable_buffer = portable_prepared.schema().new_buffer();
1053 let mut reference_buffer = reference_prepared.schema().new_buffer();
1054 #[cfg(feature = "jit")]
1055 let mut jit_buffer = jit.schema().new_buffer();
1056
1057 enable_spells(&mut portable_buffer);
1058 enable_spells(&mut reference_buffer);
1059 portable_buffer.player_mut().is_alive = 1;
1060 reference_buffer.player_mut().is_alive = 1;
1061 #[cfg(feature = "jit")]
1062 {
1063 enable_spells(&mut jit_buffer);
1064 jit_buffer.player_mut().is_alive = 1;
1065 }
1066
1067 for (expires_at, now, semantically_active) in [
1068 (20.0, 10.0, true),
1069 (10.0, 10.0, false),
1070 (0.0, 10.0, false),
1071 (-0.0, 10.0, false),
1072 (-1.0, 10.0, false),
1073 (f64::NAN, 10.0, false),
1074 (20.0, f64::NAN, false),
1075 ] {
1076 set_expiry(&mut portable_buffer, expires_at);
1077 set_expiry(&mut reference_buffer, expires_at);
1078 #[cfg(feature = "jit")]
1079 set_expiry(&mut jit_buffer, expires_at);
1080
1081 let portable_result = executable.evaluate(&mut portable_buffer, now);
1082 let reference_result =
1083 reference::evaluate_oracle(&reference_prepared, &mut reference_buffer, now);
1084 let expected_spell = if semantically_active == expected_active {
1085 1
1086 } else {
1087 2
1088 };
1089
1090 verify_that!(portable_result.spell_id, eq(expected_spell))?;
1091 verify_that!(portable_result, eq(reference_result))?;
1092 verify_that!(portable_buffer.bytes(), eq(reference_buffer.bytes()))?;
1093
1094 #[cfg(feature = "jit")]
1095 {
1096 let jit_result = jit.evaluate(&mut jit_buffer, now);
1097
1098 verify_that!(portable_result, eq(jit_result))?;
1099 verify_that!(portable_buffer.bytes(), eq(jit_buffer.bytes()))?;
1100 }
1101 }
1102 }
1103
1104 Ok(())
1105 }
1106
1107 #[cfg(feature = "rotation-optimizations")]
1108 #[gtest]
1109 fn externally_prevalidated_usability_compiles_as_enabled_bit() {
1110 let prepared = prepared(
1111 r#"{
1112 "version":1,
1113 "name":"prevalidated",
1114 "variables":{},
1115 "actions":[{"type":"cast","spell":"primary"}],
1116 "lists":{}
1117 }"#,
1118 )
1119 .with_prevalidated_spell_usability();
1120 let mut executable = PortableExecutable::compile(&prepared);
1121 let mut buffer = prepared.schema().new_buffer();
1122 let primary = buffer
1123 .layout()
1124 .spell_handle(wowlab_types::sim::SpellIdx(1))
1125 .expect("primary spell handle");
1126
1127 buffer.spell_mut_at(primary).is_enabled = 1;
1128 buffer.spell_mut_at(primary).cost = f64::MAX;
1129
1130 expect_that!(
1131 executable
1132 .program
1133 .instructions
1134 .iter()
1135 .filter(|instruction| matches!(instruction, Instruction::LoadBool { .. }))
1136 .count(),
1137 eq(0)
1138 );
1139 expect_that!(
1140 executable
1141 .program
1142 .instructions
1143 .iter()
1144 .filter(|instruction| matches!(instruction, Instruction::JumpIfBufferBool { .. }))
1145 .count(),
1146 eq(1)
1147 );
1148 expect_that!(
1149 executable.program.instructions.iter().any(|instruction| {
1150 matches!(
1151 instruction,
1152 Instruction::LoadI32 { .. } | Instruction::CmpNeI { .. }
1153 )
1154 }),
1155 is_false()
1156 );
1157 expect_that!(executable.evaluate(&mut buffer, 0.0).spell_id, eq(1));
1158
1159 buffer.spell_mut_at(primary).is_enabled = 0;
1160
1161 expect_that!(executable.evaluate(&mut buffer, 0.0), eq(EvalResult::NONE));
1162 }
1163
1164 #[gtest]
1165 fn portable_program_matches_private_structural_reference() {
1166 let json = r#"{
1167 "version":1,
1168 "name":"private-reference-parity",
1169 "variables":{"gate":{"type":"bool","value":true}},
1170 "actions":[
1171 {"type":"set_var","name":"gate","value":{"type":"not","operand":{"type":"var","name":"gate"}}},
1172 {"type":"wait","seconds":0.5,"condition":{"type":"var","name":"gate"}},
1173 {"type":"cast","spell":"primary"},
1174 {"type":"cast","spell":"fallback"}
1175 ],
1176 "lists":{}
1177 }"#;
1178 let portable_prepared = prepared(json);
1179 let reference_prepared = prepared(json);
1180 let mut executable = PortableExecutable::compile(&portable_prepared);
1181 let mut portable_buffer = portable_prepared.schema().new_buffer();
1182 let mut reference_buffer = reference_prepared.schema().new_buffer();
1183
1184 enable_spells(&mut portable_buffer);
1185 enable_spells(&mut reference_buffer);
1186
1187 for step in 0..16 {
1188 let now = f64::from(step) * 0.25;
1189 let portable_result = executable.evaluate(&mut portable_buffer, now);
1190 let reference_result =
1191 reference::evaluate_oracle(&reference_prepared, &mut reference_buffer, now);
1192
1193 expect_that!(portable_result, eq(reference_result));
1194 expect_that!(portable_buffer.bytes(), eq(reference_buffer.bytes()));
1195 }
1196 }
1197
1198 #[gtest]
1199 fn ordinary_evaluation_does_not_traverse_or_lower_the_source() {
1200 let prepared = prepared(
1201 r#"{
1202 "version":1,
1203 "name":"lower-once",
1204 "variables":{},
1205 "actions":[{"type":"cast","spell":"primary"}],
1206 "lists":{}
1207 }"#,
1208 );
1209
1210 wowlab_types::bench_metrics::reset();
1211 wowlab_types::bench_metrics::record_rotation_evaluation();
1212 let metrics_enabled = wowlab_types::bench_metrics::snapshot().rotation_evaluations == 1;
1213
1214 crate::rotation::lower::reset_lowering_count();
1215 wowlab_types::bench_metrics::reset();
1216 let mut executable = PortableExecutable::compile(&prepared);
1217 let count_after_compile = crate::rotation::lower::lowering_count();
1218 let counters_after_compile = wowlab_types::bench_metrics::snapshot();
1219 let mut buffer = prepared.schema().new_buffer();
1220
1221 enable_spells(&mut buffer);
1222
1223 for step in 0..32 {
1224 let _ = executable.evaluate(&mut buffer, f64::from(step));
1225 }
1226
1227 expect_that!(count_after_compile, eq(1));
1228 expect_that!(
1229 crate::rotation::lower::lowering_count(),
1230 eq(count_after_compile)
1231 );
1232
1233 if metrics_enabled {
1234 expect_that!(counters_after_compile.rotation_ast_lowerings, eq(1));
1235 expect_that!(counters_after_compile.rotation_evaluations, eq(0));
1236 expect_that!(
1237 wowlab_types::bench_metrics::snapshot().rotation_ast_lowerings,
1238 eq(1)
1239 );
1240 } else {
1241 expect_that!(
1242 counters_after_compile,
1243 eq(wowlab_types::bench_metrics::BenchmarkCounters::default())
1244 );
1245 }
1246 }
1247
1248 #[cfg(feature = "jit")]
1249 #[gtest]
1250 fn portable_jit_and_private_reference_match_spell_usability() {
1251 use crate::rotation::RotationEngine;
1252
1253 let json = r#"{
1254 "version":1,
1255 "name":"three-way-parity",
1256 "variables":{},
1257 "actions":[
1258 {"type":"cast","spell":"primary"},
1259 {"type":"cast","spell":"fallback"}
1260 ],
1261 "lists":{}
1262 }"#;
1263 let portable_prepared = prepared(json);
1264 let reference_prepared = prepared(json);
1265 let jit_prepared = prepared(json);
1266 let mut portable = PortableExecutable::compile(&portable_prepared);
1267 let mut jit = RotationEngine::jit(jit_prepared).expect("compile JIT fixture");
1268 let mut portable_buffer = portable_prepared.schema().new_buffer();
1269 let mut reference_buffer = reference_prepared.schema().new_buffer();
1270 let mut jit_buffer = jit.schema().new_buffer();
1271
1272 enable_spells(&mut portable_buffer);
1273 enable_spells(&mut reference_buffer);
1274 enable_spells(&mut jit_buffer);
1275
1276 for primary_enabled in [true, false, true, false] {
1277 for buffer in [&mut portable_buffer, &mut reference_buffer, &mut jit_buffer] {
1278 let primary = buffer
1279 .layout()
1280 .spell_handle(wowlab_types::sim::SpellIdx(1))
1281 .expect("primary spell handle");
1282
1283 buffer.spell_mut_at(primary).is_enabled = i32::from(primary_enabled);
1284 }
1285
1286 let portable_result = portable.evaluate(&mut portable_buffer, 0.0);
1287 let reference_result =
1288 reference::evaluate_oracle(&reference_prepared, &mut reference_buffer, 0.0);
1289 let jit_result = jit.evaluate(&mut jit_buffer, 0.0);
1290
1291 expect_that!(portable_result, eq(reference_result));
1292 expect_that!(portable_result, eq(jit_result));
1293 expect_that!(
1294 portable_result.spell_id,
1295 eq(if primary_enabled { 1 } else { 2 })
1296 );
1297 }
1298 }
1299}