Skip to main content

wowlab_engine_domain/rotation/bytecode/
runtime.rs

1// #t(file: rust_unchecked_indexing) program construction validates all register identifiers and buffer coordinates.
2// #t(file: rust_floating_point_eq) exact-zero truthiness is the documented backend contract.
3
4// #t(file: rust_inline_test_module_size) exhaustive instruction coverage requires private register and program construction.
5
6//! Reusable register banks and allocation-free portable bytecode dispatch.
7
8use wowlab_types::{
9    numeric::{float_eq, float_ne, safe_div, true_mod},
10    sim::SpellIdx,
11};
12
13#[cfg(feature = "rotation-optimizations")]
14use super::instruction::FloatComparison;
15use super::{
16    instruction::{BoolRegister, BoolValue, FloatValue, Instruction, IntValue},
17    program::BytecodeProgram,
18};
19#[cfg(feature = "rotation-optimizations")]
20use crate::rotation::{
21    buffer::CooldownSlot,
22    lower::ops::{ScalarEvalOps, eval_cooldown_ready},
23};
24use crate::rotation::{buffer::DenseBuffer, result::EvalResult};
25
26#[derive(Debug)]
27#[expect(
28    clippy::struct_field_names,
29    reason = "the suffix identifies each typed register bank consistently"
30)]
31pub(super) struct BytecodeRuntime {
32    bool_registers: Box<[u8]>,
33    int_registers: Box<[i64]>,
34    float_registers: Box<[f64]>,
35}
36
37impl BytecodeRuntime {
38    pub(super) fn new(program: &BytecodeProgram) -> Self {
39        let counts = program.register_counts();
40
41        Self {
42            bool_registers: vec![0; counts.bools()].into_boxed_slice(),
43            int_registers: vec![0; counts.ints()].into_boxed_slice(),
44            float_registers: vec![0.0; counts.floats()].into_boxed_slice(),
45        }
46    }
47
48    // #t(fn: rust_cyclomatic_complexity) the bytecode dispatcher is intentionally exhaustive so every backend primitive has one auditable runtime arm.
49    // #t(fn: rust_max_fn_lines) keeping the exhaustive instruction-to-operation mapping together preserves dispatch locality.
50    #[inline]
51    pub(super) fn execute(
52        &mut self,
53        program: &BytecodeProgram,
54        buffer: &mut DenseBuffer,
55        now: f64,
56    ) -> EvalResult {
57        if let Some(register) = program.now_register() {
58            self.float_registers[register.as_usize()] = now;
59        }
60
61        let mut instruction_pointer = 0;
62
63        while instruction_pointer < program.instructions.len() {
64            let instruction = program.instructions[instruction_pointer];
65
66            instruction_pointer += 1;
67
68            match instruction {
69                Instruction::LoadBool { target, offset } => {
70                    let value = bytemuck::pod_read_unaligned::<i32>(
71                        &buffer.bytes()[offset..offset + size_of::<i32>()],
72                    ) != 0;
73
74                    self.store_bool_register(target, value);
75                }
76                Instruction::LoadI32 { target, offset } => {
77                    self.int_registers[target.as_usize()] =
78                        i64::from(bytemuck::pod_read_unaligned::<i32>(
79                            &buffer.bytes()[offset..offset + size_of::<i32>()],
80                        ));
81                }
82                Instruction::LoadF64 { target, offset } => {
83                    self.float_registers[target.as_usize()] = bytemuck::pod_read_unaligned::<f64>(
84                        &buffer.bytes()[offset..offset + size_of::<f64>()],
85                    );
86                }
87                Instruction::StoreBool { offset, value } => {
88                    let bytes = i32::from(self.bool_value(value)).to_ne_bytes();
89
90                    buffer.bytes_mut()[offset..offset + bytes.len()].copy_from_slice(&bytes);
91                }
92                Instruction::StoreI32 { offset, value } => {
93                    let bytes =
94                        crate::numeric::wrapping_i64_to_i32(self.int_value(value)).to_ne_bytes();
95
96                    buffer.bytes_mut()[offset..offset + bytes.len()].copy_from_slice(&bytes);
97                }
98                Instruction::StoreF64 { offset, value } => {
99                    let bytes = self.float_value(value).to_ne_bytes();
100
101                    buffer.bytes_mut()[offset..offset + bytes.len()].copy_from_slice(&bytes);
102                }
103                #[cfg(not(feature = "rotation-optimizations"))]
104                Instruction::Now { target } => self.float_registers[target.as_usize()] = now,
105                Instruction::AddF { target, lhs, rhs } => {
106                    self.float_registers[target.as_usize()] =
107                        self.float_value(lhs) + self.float_value(rhs);
108                }
109                Instruction::SubF { target, lhs, rhs } => {
110                    self.float_registers[target.as_usize()] =
111                        self.float_value(lhs) - self.float_value(rhs);
112                }
113                Instruction::MulF { target, lhs, rhs } => {
114                    self.float_registers[target.as_usize()] =
115                        self.float_value(lhs) * self.float_value(rhs);
116                }
117                Instruction::SafeDivF { target, lhs, rhs } => {
118                    self.float_registers[target.as_usize()] =
119                        safe_div(self.float_value(lhs), self.float_value(rhs));
120                }
121                Instruction::TrueModF { target, lhs, rhs } => {
122                    self.float_registers[target.as_usize()] =
123                        true_mod(self.float_value(lhs), self.float_value(rhs));
124                }
125                Instruction::MinF { target, lhs, rhs } => {
126                    self.float_registers[target.as_usize()] =
127                        self.float_value(lhs).min(self.float_value(rhs));
128                }
129                Instruction::MaxF { target, lhs, rhs } => {
130                    self.float_registers[target.as_usize()] =
131                        self.float_value(lhs).max(self.float_value(rhs));
132                }
133                Instruction::FloorF { target, value } => {
134                    self.float_registers[target.as_usize()] = self.float_value(value).floor();
135                }
136                Instruction::CeilF { target, value } => {
137                    self.float_registers[target.as_usize()] = self.float_value(value).ceil();
138                }
139                Instruction::AbsF { target, value } => {
140                    self.float_registers[target.as_usize()] = self.float_value(value).abs();
141                }
142                Instruction::CmpGtF { target, lhs, rhs } => {
143                    let value = self.float_value(lhs) > self.float_value(rhs);
144
145                    self.store_bool_register(target, value);
146                }
147                Instruction::CmpGteF { target, lhs, rhs } => {
148                    let value = self.float_value(lhs) >= self.float_value(rhs);
149
150                    self.store_bool_register(target, value);
151                }
152                Instruction::CmpLtF { target, lhs, rhs } => {
153                    let value = self.float_value(lhs) < self.float_value(rhs);
154
155                    self.store_bool_register(target, value);
156                }
157                Instruction::CmpLteF { target, lhs, rhs } => {
158                    let value = self.float_value(lhs) <= self.float_value(rhs);
159
160                    self.store_bool_register(target, value);
161                }
162                Instruction::CmpEqF { target, lhs, rhs } => {
163                    let value = float_eq(self.float_value(lhs), self.float_value(rhs));
164
165                    self.store_bool_register(target, value);
166                }
167                Instruction::CmpNeF { target, lhs, rhs } => {
168                    let value = float_ne(self.float_value(lhs), self.float_value(rhs));
169
170                    self.store_bool_register(target, value);
171                }
172                Instruction::CmpEqI { target, lhs, rhs } => {
173                    let value = self.int_value(lhs) == self.int_value(rhs);
174
175                    self.store_bool_register(target, value);
176                }
177                Instruction::CmpNeI { target, lhs, rhs } => {
178                    let value = self.int_value(lhs) != self.int_value(rhs);
179
180                    self.store_bool_register(target, value);
181                }
182                Instruction::CmpGtI { target, lhs, rhs } => {
183                    let value = self.int_value(lhs) > self.int_value(rhs);
184
185                    self.store_bool_register(target, value);
186                }
187                Instruction::CmpGteI { target, lhs, rhs } => {
188                    let value = self.int_value(lhs) >= self.int_value(rhs);
189
190                    self.store_bool_register(target, value);
191                }
192                Instruction::CmpLtI { target, lhs, rhs } => {
193                    let value = self.int_value(lhs) < self.int_value(rhs);
194
195                    self.store_bool_register(target, value);
196                }
197                Instruction::CmpLteI { target, lhs, rhs } => {
198                    let value = self.int_value(lhs) <= self.int_value(rhs);
199
200                    self.store_bool_register(target, value);
201                }
202                Instruction::AndB { target, lhs, rhs } => {
203                    let value = self.bool_value(lhs) && self.bool_value(rhs);
204
205                    self.store_bool_register(target, value);
206                }
207                Instruction::OrB { target, lhs, rhs } => {
208                    let value = self.bool_value(lhs) || self.bool_value(rhs);
209
210                    self.store_bool_register(target, value);
211                }
212                Instruction::SetBool { target, value } => {
213                    self.store_bool_register(target, value);
214                }
215                Instruction::JumpIf {
216                    condition,
217                    target,
218                    when_true,
219                } => {
220                    if self.bool_value(condition) == when_true {
221                        instruction_pointer = target.as_usize();
222                    }
223                }
224                #[cfg(feature = "rotation-optimizations")]
225                Instruction::JumpIfBufferBool {
226                    offset,
227                    target,
228                    when_true,
229                } => {
230                    let value = bytemuck::pod_read_unaligned::<i32>(
231                        &buffer.bytes()[offset..offset + size_of::<i32>()],
232                    ) != 0;
233
234                    if value == when_true {
235                        instruction_pointer = target.as_usize();
236                    }
237                }
238                #[cfg(feature = "rotation-optimizations")]
239                Instruction::JumpIfCooldownNotReady {
240                    cooldown_offset,
241                    bypass_aura_offset,
242                    target,
243                } => {
244                    let bytes = buffer.bytes();
245                    let ready_at = bytemuck::pod_read_unaligned::<f64>(
246                        &bytes[cooldown_offset..cooldown_offset + size_of::<f64>()],
247                    );
248                    let current_charges_offset = cooldown_offset
249                        + CooldownSlot::OFF_CURRENT_CHARGES
250                        - CooldownSlot::OFF_READY_AT;
251                    let current_charges = bytemuck::pod_read_unaligned::<i32>(
252                        &bytes[current_charges_offset..current_charges_offset + size_of::<i32>()],
253                    );
254                    let max_charges_offset = cooldown_offset + CooldownSlot::OFF_MAX_CHARGES
255                        - CooldownSlot::OFF_READY_AT;
256                    let max_charges = bytemuck::pod_read_unaligned::<i32>(
257                        &bytes[max_charges_offset..max_charges_offset + size_of::<i32>()],
258                    );
259                    let bypass_active = bypass_aura_offset.is_some_and(|offset| {
260                        bytemuck::pod_read_unaligned::<f64>(
261                            &bytes[offset..offset + size_of::<f64>()],
262                        ) > now
263                    });
264                    let ready = eval_cooldown_ready(
265                        &mut ScalarEvalOps,
266                        ready_at,
267                        current_charges,
268                        max_charges,
269                        now,
270                        bypass_active,
271                    );
272
273                    if !ready {
274                        instruction_pointer = target.as_usize();
275                    }
276                }
277                #[cfg(feature = "rotation-optimizations")]
278                Instruction::JumpIfTimestampStateMismatch {
279                    offset,
280                    expected_active,
281                    target,
282                } => {
283                    let expires_at = bytemuck::pod_read_unaligned::<f64>(
284                        &buffer.bytes()[offset..offset + size_of::<f64>()],
285                    );
286                    let positive = expires_at > 0.0;
287                    let now_before_expiry = now < expires_at;
288                    let active = positive & now_before_expiry;
289
290                    if active != expected_active {
291                        instruction_pointer = target.as_usize();
292                    }
293                }
294                #[cfg(feature = "rotation-optimizations")]
295                Instruction::JumpIfBufferF64ComparisonFalse {
296                    offset,
297                    rhs,
298                    comparison,
299                    target,
300                } => {
301                    let lhs = bytemuck::pod_read_unaligned::<f64>(
302                        &buffer.bytes()[offset..offset + size_of::<f64>()],
303                    );
304                    let result = match comparison {
305                        FloatComparison::Gt => lhs > rhs,
306                        FloatComparison::Gte => lhs >= rhs,
307                        FloatComparison::Lt => lhs < rhs,
308                        FloatComparison::Lte => lhs <= rhs,
309                        FloatComparison::Eq => float_eq(lhs, rhs),
310                        FloatComparison::Ne => float_ne(lhs, rhs),
311                    };
312
313                    if !result {
314                        instruction_pointer = target.as_usize();
315                    }
316                }
317                Instruction::NotB { target, value } => {
318                    let value = !self.bool_value(value);
319
320                    self.store_bool_register(target, value);
321                }
322                Instruction::BoolToFloat { target, value } => {
323                    self.float_registers[target.as_usize()] =
324                        if self.bool_value(value) { 1.0 } else { 0.0 };
325                }
326                Instruction::BoolToInt { target, value } => {
327                    self.int_registers[target.as_usize()] = i64::from(self.bool_value(value));
328                }
329                Instruction::IntToFloat { target, value } => {
330                    self.float_registers[target.as_usize()] =
331                        wowlab_types::numeric::i64_to_f64(self.int_value(value));
332                }
333                Instruction::IntToBool { target, value } => {
334                    let value = self.int_value(value) != 0;
335
336                    self.store_bool_register(target, value);
337                }
338                Instruction::FloatToBool { target, value } => {
339                    let value = self.float_value(value) != 0.0;
340
341                    self.store_bool_register(target, value);
342                }
343                Instruction::FloatToInt { target, value } => {
344                    self.int_registers[target.as_usize()] =
345                        wowlab_types::numeric::f64_to_i64_saturating_trunc(self.float_value(value));
346                }
347                Instruction::SelectF {
348                    target,
349                    condition,
350                    if_true,
351                    if_false,
352                } => {
353                    self.float_registers[target.as_usize()] = if self.bool_value(condition) {
354                        self.float_value(if_true)
355                    } else {
356                        self.float_value(if_false)
357                    };
358                }
359                Instruction::SelectI {
360                    target,
361                    condition,
362                    if_true,
363                    if_false,
364                } => {
365                    self.int_registers[target.as_usize()] = if self.bool_value(condition) {
366                        self.int_value(if_true)
367                    } else {
368                        self.int_value(if_false)
369                    };
370                }
371                Instruction::SelectB {
372                    target,
373                    condition,
374                    if_true,
375                    if_false,
376                } => {
377                    let value = if self.bool_value(condition) {
378                        self.bool_value(if_true)
379                    } else {
380                        self.bool_value(if_false)
381                    };
382
383                    self.store_bool_register(target, value);
384                }
385                #[cfg(feature = "rotation-optimizations")]
386                Instruction::ReturnCast {
387                    spell_id,
388                    empower_rank,
389                } => return EvalResult::cast(SpellIdx(spell_id), empower_rank),
390                Instruction::ReturnCastIf {
391                    condition,
392                    spell_id,
393                    empower_rank,
394                } => {
395                    if self.bool_value(condition) {
396                        return EvalResult::cast(SpellIdx(spell_id), empower_rank);
397                    }
398                }
399                Instruction::ReturnWaitIf { condition, seconds } => {
400                    if self.bool_value(condition) {
401                        return EvalResult::wait(seconds);
402                    }
403                }
404                Instruction::ReturnPoolIf { condition, target } => {
405                    if self.bool_value(condition) {
406                        return EvalResult::pool(target);
407                    }
408                }
409                Instruction::ReturnUseItemIf {
410                    condition,
411                    gear_slot,
412                    empower_rank,
413                } => {
414                    if self.bool_value(condition) {
415                        return EvalResult::use_item_raw(gear_slot, empower_rank);
416                    }
417                }
418                Instruction::ReturnNone => return EvalResult::NONE,
419            }
420        }
421
422        EvalResult::NONE
423    }
424
425    #[inline]
426    fn bool_value(&self, value: BoolValue) -> bool {
427        match value {
428            BoolValue::Constant(value) => value,
429            BoolValue::Register(register) => self.bool_registers[register.as_usize()] != 0,
430        }
431    }
432
433    #[inline]
434    fn int_value(&self, value: IntValue) -> i64 {
435        match value {
436            IntValue::Constant(value) => value,
437            IntValue::Register(register) => self.int_registers[register.as_usize()],
438        }
439    }
440
441    #[inline]
442    fn float_value(&self, value: FloatValue) -> f64 {
443        match value {
444            FloatValue::Constant(value) => value,
445            FloatValue::Register(register) => self.float_registers[register.as_usize()],
446        }
447    }
448
449    #[inline]
450    fn store_bool_register(&mut self, target: BoolRegister, value: bool) {
451        self.bool_registers[target.as_usize()] = u8::from(value);
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use std::sync::Arc;
458
459    use googletest::prelude::*;
460    use wowlab_types::sim::Rotation;
461
462    use super::*;
463    use crate::rotation::{
464        BufferRequirements, RotationPreparation, SpecResolver,
465        bytecode::{
466            instruction::{FloatRegister, InstructionAddress, IntRegister},
467            program::RegisterCounts,
468        },
469    };
470
471    fn test_program(instructions: Vec<Instruction>, counts: RegisterCounts) -> BytecodeProgram {
472        let prepared = RotationPreparation::prepare(
473            &Rotation::empty(),
474            &SpecResolver::new("bytecode-runtime"),
475        )
476        .expect("prepare empty rotation")
477        .bind(&BufferRequirements::new())
478        .expect("bind empty schema");
479
480        BytecodeProgram::new(instructions, counts, Arc::clone(prepared.schema()))
481    }
482
483    #[cfg(feature = "rotation-optimizations")]
484    fn test_program_with_now(
485        instructions: Vec<Instruction>,
486        counts: RegisterCounts,
487        now_register: FloatRegister,
488    ) -> BytecodeProgram {
489        test_program(instructions, counts).with_optional_now_register(Some(now_register))
490    }
491
492    #[gtest]
493    fn every_instruction_executes_with_typed_register_banks() {
494        let b = |index| BoolRegister(index);
495        let i = |index| IntRegister(index);
496        let f = |index| FloatRegister(index);
497        let instructions = vec![
498            Instruction::StoreBool {
499                offset: 0,
500                value: BoolValue::Constant(true),
501            },
502            Instruction::LoadBool {
503                target: b(0),
504                offset: 0,
505            },
506            Instruction::StoreI32 {
507                offset: 8,
508                value: IntValue::Constant(7),
509            },
510            Instruction::LoadI32 {
511                target: i(0),
512                offset: 8,
513            },
514            Instruction::StoreF64 {
515                offset: 16,
516                value: FloatValue::Constant(-2.5),
517            },
518            Instruction::LoadF64 {
519                target: f(0),
520                offset: 16,
521            },
522            Instruction::AddF {
523                target: f(2),
524                lhs: FloatValue::Constant(2.0),
525                rhs: FloatValue::Constant(3.0),
526            },
527            Instruction::SubF {
528                target: f(3),
529                lhs: FloatValue::Constant(2.0),
530                rhs: FloatValue::Constant(3.0),
531            },
532            Instruction::MulF {
533                target: f(4),
534                lhs: FloatValue::Constant(2.0),
535                rhs: FloatValue::Constant(3.0),
536            },
537            Instruction::SafeDivF {
538                target: f(5),
539                lhs: FloatValue::Constant(9.0),
540                rhs: FloatValue::Constant(0.0),
541            },
542            Instruction::TrueModF {
543                target: f(6),
544                lhs: FloatValue::Constant(-7.0),
545                rhs: FloatValue::Constant(3.0),
546            },
547            Instruction::MinF {
548                target: f(7),
549                lhs: FloatValue::Constant(2.0),
550                rhs: FloatValue::Constant(3.0),
551            },
552            Instruction::MaxF {
553                target: f(8),
554                lhs: FloatValue::Constant(2.0),
555                rhs: FloatValue::Constant(3.0),
556            },
557            Instruction::FloorF {
558                target: f(9),
559                value: FloatValue::Constant(2.9),
560            },
561            Instruction::CeilF {
562                target: f(10),
563                value: FloatValue::Constant(2.1),
564            },
565            Instruction::AbsF {
566                target: f(11),
567                value: FloatValue::Constant(-2.0),
568            },
569            Instruction::CmpGtF {
570                target: b(1),
571                lhs: FloatValue::Constant(2.0),
572                rhs: FloatValue::Constant(1.0),
573            },
574            Instruction::CmpGteF {
575                target: b(2),
576                lhs: FloatValue::Constant(2.0),
577                rhs: FloatValue::Constant(2.0),
578            },
579            Instruction::CmpLtF {
580                target: b(3),
581                lhs: FloatValue::Constant(1.0),
582                rhs: FloatValue::Constant(2.0),
583            },
584            Instruction::CmpLteF {
585                target: b(4),
586                lhs: FloatValue::Constant(2.0),
587                rhs: FloatValue::Constant(2.0),
588            },
589            Instruction::CmpEqF {
590                target: b(5),
591                lhs: FloatValue::Constant(2.0),
592                rhs: FloatValue::Constant(2.0),
593            },
594            Instruction::CmpNeF {
595                target: b(6),
596                lhs: FloatValue::Constant(2.0),
597                rhs: FloatValue::Constant(3.0),
598            },
599            Instruction::CmpEqI {
600                target: b(7),
601                lhs: IntValue::Constant(2),
602                rhs: IntValue::Constant(2),
603            },
604            Instruction::CmpNeI {
605                target: b(8),
606                lhs: IntValue::Constant(2),
607                rhs: IntValue::Constant(3),
608            },
609            Instruction::CmpGtI {
610                target: b(9),
611                lhs: IntValue::Constant(3),
612                rhs: IntValue::Constant(2),
613            },
614            Instruction::CmpGteI {
615                target: b(10),
616                lhs: IntValue::Constant(2),
617                rhs: IntValue::Constant(2),
618            },
619            Instruction::CmpLtI {
620                target: b(11),
621                lhs: IntValue::Constant(2),
622                rhs: IntValue::Constant(3),
623            },
624            Instruction::CmpLteI {
625                target: b(12),
626                lhs: IntValue::Constant(2),
627                rhs: IntValue::Constant(2),
628            },
629            Instruction::AndB {
630                target: b(13),
631                lhs: BoolValue::Constant(true),
632                rhs: BoolValue::Constant(true),
633            },
634            Instruction::OrB {
635                target: b(14),
636                lhs: BoolValue::Constant(false),
637                rhs: BoolValue::Constant(true),
638            },
639            Instruction::SetBool {
640                target: b(15),
641                value: true,
642            },
643            Instruction::JumpIf {
644                condition: BoolValue::Constant(false),
645                target: InstructionAddress(34),
646                when_true: true,
647            },
648            Instruction::NotB {
649                target: b(16),
650                value: BoolValue::Constant(false),
651            },
652            Instruction::BoolToFloat {
653                target: f(12),
654                value: BoolValue::Register(b(16)),
655            },
656            Instruction::BoolToInt {
657                target: i(2),
658                value: BoolValue::Register(b(16)),
659            },
660            Instruction::IntToFloat {
661                target: f(13),
662                value: IntValue::Constant(9),
663            },
664            Instruction::IntToBool {
665                target: b(17),
666                value: IntValue::Constant(-1),
667            },
668            Instruction::FloatToBool {
669                target: b(18),
670                value: FloatValue::Constant(-0.0),
671            },
672            Instruction::FloatToInt {
673                target: i(3),
674                value: FloatValue::Constant(-2.9),
675            },
676            Instruction::SelectF {
677                target: f(14),
678                condition: BoolValue::Constant(true),
679                if_true: FloatValue::Constant(4.0),
680                if_false: FloatValue::Constant(5.0),
681            },
682            Instruction::SelectI {
683                target: i(4),
684                condition: BoolValue::Constant(false),
685                if_true: IntValue::Constant(4),
686                if_false: IntValue::Constant(5),
687            },
688            Instruction::SelectB {
689                target: b(19),
690                condition: BoolValue::Constant(true),
691                if_true: BoolValue::Constant(true),
692                if_false: BoolValue::Constant(false),
693            },
694            Instruction::StoreBool {
695                offset: 32,
696                value: BoolValue::Register(b(19)),
697            },
698            Instruction::StoreI32 {
699                offset: 40,
700                value: IntValue::Register(i(3)),
701            },
702            Instruction::StoreF64 {
703                offset: 48,
704                value: FloatValue::Register(f(14)),
705            },
706            Instruction::ReturnCastIf {
707                condition: BoolValue::Constant(false),
708                spell_id: 1,
709                empower_rank: 2,
710            },
711            Instruction::ReturnWaitIf {
712                condition: BoolValue::Constant(false),
713                seconds: 1.0,
714            },
715            Instruction::ReturnUseItemIf {
716                condition: BoolValue::Constant(false),
717                gear_slot: 13,
718                empower_rank: 1,
719            },
720            Instruction::ReturnPoolIf {
721                condition: BoolValue::Constant(true),
722                target: 42.0,
723            },
724            Instruction::ReturnNone,
725        ];
726        let program = test_program(
727            instructions,
728            RegisterCounts {
729                bools: 20,
730                ints: 5,
731                floats: 15,
732            },
733        );
734        let mut buffer = program.schema().new_buffer();
735
736        buffer.bytes_mut()[24..32].copy_from_slice(&(-9_i64).to_ne_bytes());
737        let mut runtime = BytecodeRuntime::new(&program);
738        let result = runtime.execute(&program, &mut buffer, 7.5);
739
740        expect_that!(result, eq(EvalResult::pool(42.0)));
741        expect_that!(
742            bytemuck::pod_read_unaligned::<i32>(&buffer.bytes()[32..36]),
743            eq(1)
744        );
745        expect_that!(
746            bytemuck::pod_read_unaligned::<i32>(&buffer.bytes()[40..44]),
747            eq(-2)
748        );
749        expect_that!(
750            bytemuck::pod_read_unaligned::<f64>(&buffer.bytes()[48..56]),
751            eq(4.0)
752        );
753    }
754
755    #[cfg(feature = "rotation-optimizations")]
756    #[gtest]
757    fn shared_now_register_is_initialized_before_dispatch() {
758        let register = FloatRegister(0);
759        let program = test_program_with_now(
760            vec![
761                Instruction::StoreF64 {
762                    offset: 0,
763                    value: FloatValue::Register(register),
764                },
765                Instruction::ReturnNone,
766            ],
767            RegisterCounts {
768                bools: 0,
769                ints: 0,
770                floats: 1,
771            },
772            register,
773        );
774        let mut buffer = program.schema().new_buffer();
775        let mut runtime = BytecodeRuntime::new(&program);
776
777        let result = runtime.execute(&program, &mut buffer, 7.5);
778
779        expect_that!(result, eq(EvalResult::NONE));
780        expect_that!(
781            bytemuck::pod_read_unaligned::<f64>(&buffer.bytes()[0..8]),
782            eq(7.5)
783        );
784    }
785
786    #[gtest]
787    fn conversions_define_zero_nan_infinity_and_truncation_exactly() {
788        let instructions = vec![
789            Instruction::FloatToBool {
790                target: BoolRegister(0),
791                value: FloatValue::Constant(0.0),
792            },
793            Instruction::FloatToBool {
794                target: BoolRegister(1),
795                value: FloatValue::Constant(-0.0),
796            },
797            Instruction::FloatToBool {
798                target: BoolRegister(2),
799                value: FloatValue::Constant(f64::NAN),
800            },
801            Instruction::FloatToInt {
802                target: IntRegister(0),
803                value: FloatValue::Constant(f64::NAN),
804            },
805            Instruction::FloatToInt {
806                target: IntRegister(1),
807                value: FloatValue::Constant(f64::NEG_INFINITY),
808            },
809            Instruction::FloatToInt {
810                target: IntRegister(2),
811                value: FloatValue::Constant(f64::INFINITY),
812            },
813            Instruction::FloatToInt {
814                target: IntRegister(3),
815                value: FloatValue::Constant(-9.75),
816            },
817            Instruction::StoreBool {
818                offset: 0,
819                value: BoolValue::Register(BoolRegister(0)),
820            },
821            Instruction::StoreBool {
822                offset: 8,
823                value: BoolValue::Register(BoolRegister(1)),
824            },
825            Instruction::StoreBool {
826                offset: 16,
827                value: BoolValue::Register(BoolRegister(2)),
828            },
829            Instruction::StoreI32 {
830                offset: 24,
831                value: IntValue::Register(IntRegister(0)),
832            },
833            Instruction::StoreI32 {
834                offset: 32,
835                value: IntValue::Register(IntRegister(1)),
836            },
837            Instruction::StoreI32 {
838                offset: 40,
839                value: IntValue::Register(IntRegister(2)),
840            },
841            Instruction::StoreI32 {
842                offset: 48,
843                value: IntValue::Register(IntRegister(3)),
844            },
845            Instruction::ReturnNone,
846        ];
847        let program = test_program(
848            instructions,
849            RegisterCounts {
850                bools: 3,
851                ints: 4,
852                floats: 0,
853            },
854        );
855        let mut buffer = program.schema().new_buffer();
856        let mut runtime = BytecodeRuntime::new(&program);
857
858        expect_that!(
859            runtime.execute(&program, &mut buffer, 0.0).is_none(),
860            is_true()
861        );
862        let read_i32 =
863            |offset| bytemuck::pod_read_unaligned::<i32>(&buffer.bytes()[offset..offset + 4]);
864
865        expect_that!(read_i32(0), eq(0));
866        expect_that!(read_i32(8), eq(0));
867        expect_that!(read_i32(16), eq(1));
868        expect_that!(read_i32(24), eq(0));
869        expect_that!(
870            &runtime.int_registers,
871            elements_are![eq(&0), eq(&i64::MIN), eq(&i64::MAX), eq(&-9)]
872        );
873    }
874
875    #[gtest]
876    fn first_fired_terminator_returns_immediately_and_later_state_is_untouched() {
877        let program = test_program(
878            vec![
879                Instruction::ReturnCastIf {
880                    condition: BoolValue::Constant(true),
881                    spell_id: 77,
882                    empower_rank: 3,
883                },
884                Instruction::StoreI32 {
885                    offset: 0,
886                    value: IntValue::Constant(99),
887                },
888                Instruction::ReturnUseItemIf {
889                    condition: BoolValue::Constant(true),
890                    gear_slot: 13,
891                    empower_rank: 2,
892                },
893                Instruction::ReturnNone,
894            ],
895            RegisterCounts {
896                bools: 0,
897                ints: 0,
898                floats: 0,
899            },
900        );
901        let mut buffer = program.schema().new_buffer();
902        let mut runtime = BytecodeRuntime::new(&program);
903
904        let result = runtime.execute(&program, &mut buffer, 0.0);
905
906        expect_that!(result, eq(EvalResult::cast(SpellIdx(77), 3)));
907        expect_that!(
908            bytemuck::pod_read_unaligned::<i32>(&buffer.bytes()[0..4]),
909            eq(0)
910        );
911    }
912
913    #[gtest]
914    fn steady_state_dispatch_allocates_nothing() {
915        let program = test_program(
916            vec![Instruction::ReturnWaitIf {
917                condition: BoolValue::Constant(true),
918                seconds: 0.25,
919            }],
920            RegisterCounts {
921                bools: 0,
922                ints: 0,
923                floats: 0,
924            },
925        );
926        let mut buffer = program.schema().new_buffer();
927        let mut runtime = BytecodeRuntime::new(&program);
928
929        let (result, allocations) =
930            crate::test_alloc::allocations_during(|| runtime.execute(&program, &mut buffer, 0.0));
931
932        expect_that!(result, eq(EvalResult::wait(0.25)));
933        expect_that!(allocations, eq(0));
934    }
935}