Skip to main content

wowlab_engine_domain/rotation/bytecode/
mod.rs

1// #t(file: rust_inline_test_module_size) private compiler/reference tests need bytecode internals and cannot be integration tests.
2
3//! Portable rotation bytecode compilation and execution.
4
5mod 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/// Portable bytecode rotation and its reusable register runtime.
23#[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    // docref:start rotation-compiler-portable-evaluate
78    pub(super) fn evaluate(&mut self, buffer: &mut DenseBuffer, now_secs: f64) -> EvalResult {
79        self.runtime.execute(&self.program, buffer, now_secs)
80    }
81    // docref:end rotation-compiler-portable-evaluate
82
83    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}