Skip to main content

luau_bytecode/graph/
sccp.rs

1use super::*;
2use crate::function::{BytecodeFunction, BytecodeFunctionConstant};
3use std::collections::{HashMap, HashSet, VecDeque};
4
5#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
6pub enum ConstantLattice {
7    #[default]
8    Undetermined,
9    NotConstant,
10    VmConstant(ConstantIndex),
11    Immediate(BytecodeImmediate),
12}
13
14impl ConstantLattice {
15    fn merge(self, other: Self) -> Self {
16        match (self, other) {
17            (Self::Undetermined, value) | (value, Self::Undetermined) => value,
18            (left, right) if left == right => left,
19            _ => Self::NotConstant,
20        }
21    }
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25enum ConditionState {
26    AlwaysFalse,
27    AlwaysTrue,
28    Unknown,
29}
30
31#[derive(Debug, Clone, Copy)]
32struct JumpTarget {
33    dead: bool,
34    block: BytecodeBlockId,
35}
36
37pub struct Sccp<'function, 'table> {
38    function: &'function mut BytecodeFunction<'table>,
39    lattice: HashMap<BytecodeOperand, ConstantLattice>,
40    block_uses: HashMap<BytecodeBlockId, HashSet<BytecodeBlockId>>,
41    flow_worklist: VecDeque<BytecodeBlockId>,
42    visited_blocks: HashSet<BytecodeBlockId>,
43    ssa_worklist: VecDeque<BytecodeOperand>,
44}
45
46impl<'function, 'table> Sccp<'function, 'table> {
47    pub fn new(function: &'function mut BytecodeFunction<'table>) -> Self {
48        Self {
49            function,
50            lattice: HashMap::new(),
51            block_uses: HashMap::new(),
52            flow_worklist: VecDeque::new(),
53            visited_blocks: HashSet::new(),
54            ssa_worklist: VecDeque::new(),
55        }
56    }
57
58    pub fn lattice(&self, operand: BytecodeOperand) -> ConstantLattice {
59        self.operand_lattice(operand)
60    }
61
62    pub fn propagate(&mut self) {
63        let entry = self.function.entry_block;
64        let exit = self.function.exit_block;
65        self.block_uses.entry(entry).or_default().insert(entry);
66        self.block_uses.entry(exit).or_default().insert(exit);
67        self.flow_worklist.push_back(entry);
68
69        while !self.flow_worklist.is_empty() || !self.ssa_worklist.is_empty() {
70            while let Some(block) = self.flow_worklist.pop_front() {
71                if !self.visited_blocks.insert(block) {
72                    continue;
73                }
74
75                let phis = self.function.block(block).phis.clone();
76                for phi in phis {
77                    self.visit_phi(phi);
78                }
79
80                let instructions = self.function.block(block).instruction_ids.clone();
81                for instruction in instructions.iter().copied() {
82                    self.visit_instruction(instruction);
83                }
84
85                let ends_with_branch = instructions
86                    .last()
87                    .is_some_and(|id| !self.jump_targets(*id).is_empty());
88                let successors = self.function.block(block).successors.clone();
89                for edge in successors {
90                    if edge.kind != BytecodeEdgeKind::Fallthrough {
91                        continue;
92                    }
93                    if ends_with_branch
94                        && !self
95                            .block_uses
96                            .get(&edge.target)
97                            .is_some_and(|uses| uses.contains(&block))
98                    {
99                        continue;
100                    }
101                    self.mark_flow_edge(block, edge.target);
102                }
103            }
104
105            while let Some(operand) = self.ssa_worklist.pop_front() {
106                match operand {
107                    BytecodeOperand::Instruction(id) => self.visit_instruction(id),
108                    BytecodeOperand::Phi(id) => self.visit_phi(id),
109                    _ => {}
110                }
111            }
112        }
113    }
114
115    pub fn rewrite(&mut self) {
116        self.rewrite_arithmetic_constants();
117        self.replace_folded_instructions();
118        self.simplify_phis();
119        self.update_block_reachability();
120        rebuild_uses(&mut self.function.instructions, &mut self.function.phis);
121    }
122
123    fn operand_lattice(&self, operand: BytecodeOperand) -> ConstantLattice {
124        if matches!(
125            operand,
126            BytecodeOperand::Projection(_)
127                | BytecodeOperand::VmRegister(_)
128                | BytecodeOperand::VmUpvalue(_)
129        ) {
130            ConstantLattice::NotConstant
131        } else {
132            self.lattice.get(&operand).copied().unwrap_or_default()
133        }
134    }
135
136    fn unknown_condition(&self, operands: &[BytecodeOperand]) -> ConstantLattice {
137        if operands
138            .iter()
139            .any(|operand| self.operand_lattice(*operand) == ConstantLattice::NotConstant)
140        {
141            ConstantLattice::NotConstant
142        } else {
143            ConstantLattice::Undetermined
144        }
145    }
146
147    fn visit_phi(&mut self, id: BytecodePhiId) {
148        let mut value = ConstantLattice::Undetermined;
149        for operand in self.function.phi(id).operands.iter().copied() {
150            value = self.operand_lattice(operand).merge(value);
151        }
152
153        let operand = BytecodeOperand::Phi(id);
154        let previous = self.lattice.get(&operand).copied().unwrap_or_default();
155        if value != previous {
156            self.ssa_worklist
157                .extend(self.function.phi(id).users.iter().copied());
158            self.lattice.insert(operand, value);
159        }
160    }
161
162    fn visit_instruction(&mut self, id: BytecodeInstructionId) {
163        let instruction = self.function.graph_instruction(id).clone();
164
165        if instruction.opcode == Opcode::Capture
166            && instruction.operands.len() >= 2
167            && self.immediate(instruction.operands[0]) == Some(BytecodeImmediate::Int(1))
168        {
169            let source = instruction.operands[1];
170            if matches!(
171                source,
172                BytecodeOperand::Instruction(_) | BytecodeOperand::Phi(_)
173            ) && self.operand_lattice(source) != ConstantLattice::NotConstant
174            {
175                self.lattice.insert(source, ConstantLattice::NotConstant);
176                self.ssa_worklist.extend(self.users(source));
177            }
178        }
179
180        let operand = BytecodeOperand::Instruction(id);
181        let value = self.evaluate_instruction(&instruction);
182        let previous = self.lattice.get(&operand).copied().unwrap_or_default();
183        let merged = value.merge(previous);
184        if merged != previous {
185            self.ssa_worklist.extend(instruction.users.iter().copied());
186        }
187
188        for target in self.jump_targets(id) {
189            if !target.dead {
190                self.mark_flow_edge(instruction.block, target.block);
191            }
192        }
193        self.lattice.insert(operand, merged);
194    }
195
196    fn mark_flow_edge(&mut self, source: BytecodeBlockId, target: BytecodeBlockId) {
197        self.block_uses.entry(target).or_default().insert(source);
198        if !self.visited_blocks.contains(&target) {
199            self.flow_worklist.push_back(target);
200        }
201    }
202
203    fn evaluate_instruction(&mut self, instruction: &BytecodeInstruction) -> ConstantLattice {
204        match instruction.opcode {
205            Opcode::LoadK | Opcode::LoadKx => match instruction.operands.first().copied() {
206                Some(BytecodeOperand::VmConstant(index)) => ConstantLattice::VmConstant(index),
207                _ => ConstantLattice::NotConstant,
208            },
209            Opcode::LoadB | Opcode::LoadN => instruction
210                .operands
211                .first()
212                .and_then(|operand| self.immediate(*operand))
213                .map(ConstantLattice::Immediate)
214                .unwrap_or(ConstantLattice::NotConstant),
215            Opcode::LoadNil => ConstantLattice::VmConstant(
216                self.find_or_add_constant(BytecodeFunctionConstant::Nil),
217            ),
218            Opcode::Add
219            | Opcode::Sub
220            | Opcode::Mul
221            | Opcode::Div
222            | Opcode::Mod
223            | Opcode::Pow
224            | Opcode::IDiv => self.evaluate_arithmetic(instruction),
225            Opcode::Move => self.operand_lattice(instruction.operands[0]),
226            Opcode::JumpIf | Opcode::JumpIfNot => {
227                let condition = self.evaluate_condition(instruction.operands[0]);
228                if condition == ConditionState::Unknown {
229                    return self.unknown_condition(&instruction.operands[..1]);
230                }
231                let jumps_on_true = instruction.opcode == Opcode::JumpIf;
232                ConstantLattice::Immediate(BytecodeImmediate::Boolean(
233                    (condition == ConditionState::AlwaysTrue) == jumps_on_true,
234                ))
235            }
236            Opcode::JumpIfEq
237            | Opcode::JumpIfLe
238            | Opcode::JumpIfLt
239            | Opcode::JumpIfNotEq
240            | Opcode::JumpIfNotLe
241            | Opcode::JumpIfNotLt => {
242                let condition = self.evaluate_comparison(
243                    instruction.opcode,
244                    instruction.operands[0],
245                    instruction.operands[1],
246                );
247                if condition == ConditionState::Unknown {
248                    return self.unknown_condition(&instruction.operands[..2]);
249                }
250                let negated = matches!(
251                    instruction.opcode,
252                    Opcode::JumpIfNotEq | Opcode::JumpIfNotLe | Opcode::JumpIfNotLt
253                );
254                ConstantLattice::Immediate(BytecodeImmediate::Boolean(
255                    (condition == ConditionState::AlwaysTrue) != negated,
256                ))
257            }
258            Opcode::JumpXEqKNil | Opcode::JumpXEqKB | Opcode::JumpXEqKN | Opcode::JumpXEqKS => {
259                let condition = self.evaluate_constant_comparison(instruction);
260                if condition == ConditionState::Unknown {
261                    return self.unknown_condition(&instruction.operands[..1]);
262                }
263                let negated = self
264                    .immediate(instruction.operands[1])
265                    .is_some_and(|value| value == BytecodeImmediate::Boolean(true));
266                ConstantLattice::Immediate(BytecodeImmediate::Boolean(
267                    (condition == ConditionState::AlwaysTrue) != negated,
268                ))
269            }
270            _ => ConstantLattice::NotConstant,
271        }
272    }
273
274    fn evaluate_arithmetic(&mut self, instruction: &BytecodeInstruction) -> ConstantLattice {
275        let left = self.operand_lattice(instruction.operands[0]);
276        let right = self.operand_lattice(instruction.operands[1]);
277
278        match (left, right) {
279            (
280                ConstantLattice::Immediate(BytecodeImmediate::Int(left)),
281                ConstantLattice::Immediate(BytecodeImmediate::Int(right)),
282            ) => {
283                if right == 0
284                    && matches!(instruction.opcode, Opcode::Div | Opcode::Mod | Opcode::IDiv)
285                {
286                    return ConstantLattice::NotConstant;
287                }
288                if matches!(instruction.opcode, Opcode::Div | Opcode::Pow) {
289                    return ConstantLattice::NotConstant;
290                }
291                let value = match instruction.opcode {
292                    Opcode::Add => i64::from(left) + i64::from(right),
293                    Opcode::Sub => i64::from(left) - i64::from(right),
294                    Opcode::Mul => i64::from(left) * i64::from(right),
295                    Opcode::Mod => {
296                        let mut value = i64::from(left) % i64::from(right);
297                        if value != 0 && (left < 0) != (right < 0) {
298                            value += i64::from(right);
299                        }
300                        value
301                    }
302                    Opcode::IDiv => i64::from(left).div_euclid(i64::from(right)),
303                    _ => return ConstantLattice::NotConstant,
304                };
305                i16::try_from(value)
306                    .ok()
307                    .map(|value| {
308                        ConstantLattice::Immediate(BytecodeImmediate::Int(i32::from(value)))
309                    })
310                    .unwrap_or(ConstantLattice::NotConstant)
311            }
312            (ConstantLattice::VmConstant(left), ConstantLattice::VmConstant(right)) => {
313                let (
314                    Some(BytecodeFunctionConstant::Number(left)),
315                    Some(BytecodeFunctionConstant::Number(right)),
316                ) = (self.constant(left), self.constant(right))
317                else {
318                    return ConstantLattice::NotConstant;
319                };
320                let value = match instruction.opcode {
321                    Opcode::Add => left + right,
322                    Opcode::Sub => left - right,
323                    Opcode::Mul => left * right,
324                    Opcode::Div => left / right,
325                    Opcode::Mod if *right != 0.0 => left - (left / right).floor() * right,
326                    Opcode::Pow => left.powf(*right),
327                    Opcode::IDiv => (left / right).floor(),
328                    _ => return ConstantLattice::NotConstant,
329                };
330                ConstantLattice::VmConstant(
331                    self.find_or_add_constant(BytecodeFunctionConstant::Number(value)),
332                )
333            }
334            (ConstantLattice::Undetermined, ConstantLattice::Undetermined) => {
335                ConstantLattice::Undetermined
336            }
337            _ => ConstantLattice::NotConstant,
338        }
339    }
340
341    fn evaluate_condition(&self, operand: BytecodeOperand) -> ConditionState {
342        match self.operand_lattice(operand) {
343            ConstantLattice::VmConstant(index) => {
344                if self.constant_is_falsey(index) {
345                    ConditionState::AlwaysFalse
346                } else {
347                    ConditionState::AlwaysTrue
348                }
349            }
350            ConstantLattice::Immediate(BytecodeImmediate::Boolean(value)) => {
351                if value {
352                    ConditionState::AlwaysTrue
353                } else {
354                    ConditionState::AlwaysFalse
355                }
356            }
357            _ => ConditionState::Unknown,
358        }
359    }
360
361    fn evaluate_comparison(
362        &self,
363        opcode: Opcode,
364        left: BytecodeOperand,
365        right: BytecodeOperand,
366    ) -> ConditionState {
367        let left = self.operand_lattice(left);
368        let right = self.operand_lattice(right);
369        let ordering = matches!(
370            opcode,
371            Opcode::JumpIfLt | Opcode::JumpIfLe | Opcode::JumpIfNotLt | Opcode::JumpIfNotLe
372        );
373        if ordering && (!self.is_orderable(left) || !self.is_orderable(right)) {
374            return ConditionState::Unknown;
375        }
376
377        let comparison = match (left, right) {
378            (ConstantLattice::VmConstant(left), ConstantLattice::VmConstant(right)) => {
379                let (Some(left), Some(right)) = (self.constant(left), self.constant(right)) else {
380                    return ConditionState::Unknown;
381                };
382                if std::mem::discriminant(left) != std::mem::discriminant(right) {
383                    return ConditionState::Unknown;
384                }
385                compare_constants(left, right)
386            }
387            (ConstantLattice::Immediate(left), ConstantLattice::Immediate(right)) => {
388                compare_immediates(left, right)
389            }
390            (ConstantLattice::VmConstant(left), ConstantLattice::Immediate(right)) => self
391                .constant(left)
392                .and_then(|left| compare_constant_immediate(left, right)),
393            (ConstantLattice::Immediate(left), ConstantLattice::VmConstant(right)) => self
394                .constant(right)
395                .and_then(|right| compare_constant_immediate(right, left))
396                .map(|ordering| ordering.reverse()),
397            _ => None,
398        };
399
400        let Some(comparison) = comparison else {
401            return ConditionState::Unknown;
402        };
403        let value = match opcode {
404            Opcode::JumpIfEq | Opcode::JumpIfNotEq => comparison.is_eq(),
405            Opcode::JumpIfLt | Opcode::JumpIfNotLt => comparison.is_lt(),
406            Opcode::JumpIfLe | Opcode::JumpIfNotLe => comparison.is_le(),
407            _ => return ConditionState::Unknown,
408        };
409        if value {
410            ConditionState::AlwaysTrue
411        } else {
412            ConditionState::AlwaysFalse
413        }
414    }
415
416    fn evaluate_constant_comparison(
417        &mut self,
418        instruction: &BytecodeInstruction,
419    ) -> ConditionState {
420        let value = self.operand_lattice(instruction.operands[0]);
421        let equal = match instruction.opcode {
422            Opcode::JumpXEqKNil => match value {
423                ConstantLattice::VmConstant(index) => self
424                    .constant(index)
425                    .map(|constant| matches!(constant, BytecodeFunctionConstant::Nil)),
426                ConstantLattice::Immediate(_) => Some(false),
427                _ => None,
428            },
429            Opcode::JumpXEqKB => {
430                let expected = self.immediate(instruction.operands[3]);
431                match (value, expected) {
432                    (
433                        ConstantLattice::Immediate(BytecodeImmediate::Boolean(value)),
434                        Some(BytecodeImmediate::Boolean(expected)),
435                    ) => Some(value == expected),
436                    (ConstantLattice::VmConstant(index), Some(expected)) => self
437                        .constant(index)
438                        .and_then(|value| constant_equals_immediate(value, expected)),
439                    _ => None,
440                }
441            }
442            Opcode::JumpXEqKN | Opcode::JumpXEqKS => {
443                let BytecodeOperand::VmConstant(expected) = instruction.operands[3] else {
444                    return ConditionState::Unknown;
445                };
446                match value {
447                    ConstantLattice::VmConstant(value) => {
448                        constants_equal(self.constant(value), self.constant(expected))
449                    }
450                    ConstantLattice::Immediate(immediate) => self
451                        .constant(expected)
452                        .and_then(|constant| constant_equals_immediate(constant, immediate)),
453                    _ => None,
454                }
455            }
456            _ => None,
457        };
458
459        match equal {
460            Some(true) => ConditionState::AlwaysTrue,
461            Some(false) => ConditionState::AlwaysFalse,
462            None => ConditionState::Unknown,
463        }
464    }
465
466    fn jump_targets(&mut self, id: BytecodeInstructionId) -> Vec<JumpTarget> {
467        let instruction = self.function.graph_instruction(id).clone();
468        match instruction.opcode {
469            Opcode::Jump | Opcode::JumpBack => instruction
470                .operands
471                .first()
472                .and_then(|operand| block_operand(*operand))
473                .map(|block| vec![JumpTarget { dead: false, block }])
474                .unwrap_or_default(),
475            Opcode::JumpIf | Opcode::JumpIfNot => {
476                let condition = self.evaluate_condition(instruction.operands[0]);
477                self.conditional_targets(
478                    &instruction,
479                    instruction.operands[1],
480                    condition,
481                    instruction.opcode == Opcode::JumpIf,
482                )
483            }
484            Opcode::JumpIfEq
485            | Opcode::JumpIfLe
486            | Opcode::JumpIfLt
487            | Opcode::JumpIfNotEq
488            | Opcode::JumpIfNotLe
489            | Opcode::JumpIfNotLt => {
490                let condition = self.evaluate_comparison(
491                    instruction.opcode,
492                    instruction.operands[0],
493                    instruction.operands[1],
494                );
495                let negated = matches!(
496                    instruction.opcode,
497                    Opcode::JumpIfNotEq | Opcode::JumpIfNotLe | Opcode::JumpIfNotLt
498                );
499                self.conditional_targets(&instruction, instruction.operands[2], condition, !negated)
500            }
501            Opcode::JumpXEqKNil | Opcode::JumpXEqKB | Opcode::JumpXEqKN | Opcode::JumpXEqKS => {
502                let condition = self.evaluate_constant_comparison(&instruction);
503                let negated = self
504                    .immediate(instruction.operands[1])
505                    .is_some_and(|value| value == BytecodeImmediate::Boolean(true));
506                self.conditional_targets(&instruction, instruction.operands[2], condition, !negated)
507            }
508            Opcode::ForNPrep
509            | Opcode::ForNLoop
510            | Opcode::ForGPrep
511            | Opcode::ForGPrepNext
512            | Opcode::ForGPrepInext => self.conditional_targets(
513                &instruction,
514                instruction.operands[3],
515                ConditionState::Unknown,
516                true,
517            ),
518            Opcode::ForGLoop => self.conditional_targets(
519                &instruction,
520                instruction.operands[5],
521                ConditionState::Unknown,
522                true,
523            ),
524            Opcode::CmpProto => self.conditional_targets(
525                &instruction,
526                instruction.operands[2],
527                ConditionState::Unknown,
528                true,
529            ),
530            _ => Vec::new(),
531        }
532    }
533
534    fn conditional_targets(
535        &self,
536        instruction: &BytecodeInstruction,
537        target: BytecodeOperand,
538        condition: ConditionState,
539        target_taken_on_true: bool,
540    ) -> Vec<JumpTarget> {
541        let Some(target) = block_operand(target) else {
542            return Vec::new();
543        };
544        let Some(fallthrough) = self
545            .function
546            .block(instruction.block)
547            .successors
548            .iter()
549            .find(|edge| edge.kind == BytecodeEdgeKind::Fallthrough)
550            .map(|edge| edge.target)
551        else {
552            return Vec::new();
553        };
554
555        let (target_dead, fallthrough_dead) = match condition {
556            ConditionState::AlwaysTrue => (!target_taken_on_true, target_taken_on_true),
557            ConditionState::AlwaysFalse => (target_taken_on_true, !target_taken_on_true),
558            ConditionState::Unknown => (false, false),
559        };
560        vec![
561            JumpTarget {
562                dead: target_dead,
563                block: target,
564            },
565            JumpTarget {
566                dead: fallthrough_dead,
567                block: fallthrough,
568            },
569        ]
570    }
571
572    fn rewrite_arithmetic_constants(&mut self) {
573        for block_index in 0..self.function.blocks.len() {
574            let block = BytecodeBlockId::new(block_index);
575            if self.block_uses.get(&block).is_none_or(HashSet::is_empty) {
576                continue;
577            }
578            let instructions = self.function.block(block).instruction_ids.clone();
579            for id in instructions {
580                let instruction = self.function.graph_instruction(id).clone();
581                let Some(mut opcode) = arithmetic_constant_opcode(instruction.opcode) else {
582                    continue;
583                };
584                if instruction.operands.len() != 2 {
585                    continue;
586                }
587
588                let left = instruction.operands[0];
589                let right = instruction.operands[1];
590                let left_lattice = self.operand_lattice(left);
591                let right_lattice = self.operand_lattice(right);
592                let is_number = |lattice| match lattice {
593                    ConstantLattice::VmConstant(index) => {
594                        self.constant(index).is_some_and(|constant| {
595                            matches!(constant, BytecodeFunctionConstant::Number(_))
596                        })
597                    }
598                    _ => false,
599                };
600
601                let (nonconstant, constant, old_constant, reverse) =
602                    if is_number(right_lattice) && left_lattice == ConstantLattice::NotConstant {
603                        (left, right_lattice, right, false)
604                    } else if is_number(left_lattice)
605                        && right_lattice == ConstantLattice::NotConstant
606                        && matches!(
607                            instruction.opcode,
608                            Opcode::Add | Opcode::Mul | Opcode::Sub | Opcode::Div
609                        )
610                    {
611                        if instruction.opcode == Opcode::Sub {
612                            opcode = Opcode::SubRK;
613                        } else if instruction.opcode == Opcode::Div {
614                            opcode = Opcode::DivRK;
615                        }
616                        (
617                            right,
618                            left_lattice,
619                            left,
620                            matches!(instruction.opcode, Opcode::Sub | Opcode::Div),
621                        )
622                    } else {
623                        continue;
624                    };
625
626                let ConstantLattice::VmConstant(constant_index) = constant else {
627                    continue;
628                };
629                let Some(BytecodeFunctionConstant::Number(number)) = self.constant(constant_index)
630                else {
631                    continue;
632                };
633                let number = *number;
634                let replacement = if number == 0.0 && !reverse {
635                    match instruction.opcode {
636                        Opcode::Add | Opcode::Sub => Some((Opcode::Move, vec![nonconstant])),
637                        Opcode::Mul => Some((
638                            Opcode::LoadN,
639                            vec![self.add_immediate(BytecodeImmediate::Int(0))],
640                        )),
641                        Opcode::Pow => Some((
642                            Opcode::LoadN,
643                            vec![self.add_immediate(BytecodeImmediate::Int(1))],
644                        )),
645                        _ => None,
646                    }
647                } else if number == 1.0 && !reverse {
648                    match instruction.opcode {
649                        Opcode::Mul | Opcode::Pow | Opcode::Div => {
650                            Some((Opcode::Move, vec![nonconstant]))
651                        }
652                        _ => None,
653                    }
654                } else {
655                    None
656                };
657
658                let (opcode, operands) = replacement.unwrap_or_else(|| {
659                    if reverse {
660                        (
661                            opcode,
662                            vec![BytecodeOperand::VmConstant(constant_index), nonconstant],
663                        )
664                    } else {
665                        (
666                            opcode,
667                            vec![nonconstant, BytecodeOperand::VmConstant(constant_index)],
668                        )
669                    }
670                });
671                self.function.instructions[id.index()].opcode = opcode;
672                self.set_instruction_operands(id, operands);
673                self.erase_dead_producer(old_constant);
674            }
675        }
676    }
677
678    fn replace_folded_instructions(&mut self) {
679        let folded = self
680            .lattice
681            .iter()
682            .filter_map(|(operand, lattice)| match (operand, lattice) {
683                (BytecodeOperand::Instruction(id), ConstantLattice::VmConstant(_))
684                | (BytecodeOperand::Instruction(id), ConstantLattice::Immediate(_)) => {
685                    Some((*id, *lattice))
686                }
687                _ => None,
688            })
689            .collect::<Vec<_>>();
690
691        for (id, lattice) in folded {
692            let opcode = self.function.graph_instruction(id).opcode;
693            if matches!(
694                opcode,
695                Opcode::LoadK | Opcode::LoadKx | Opcode::LoadN | Opcode::LoadB | Opcode::LoadNil
696            ) {
697                continue;
698            }
699            if opcode.is_jump_d() {
700                self.remove_dead_edges(id);
701                self.erase_instruction(id);
702            } else {
703                let (opcode, operand) = match lattice {
704                    ConstantLattice::VmConstant(index) => {
705                        (Opcode::LoadK, BytecodeOperand::VmConstant(index))
706                    }
707                    ConstantLattice::Immediate(immediate) => {
708                        let opcode = if matches!(immediate, BytecodeImmediate::Boolean(_)) {
709                            Opcode::LoadB
710                        } else {
711                            Opcode::LoadN
712                        };
713                        (opcode, self.add_immediate(immediate))
714                    }
715                    _ => continue,
716                };
717                self.function.instructions[id.index()].opcode = opcode;
718                self.set_instruction_operands(id, vec![operand]);
719            }
720        }
721    }
722
723    fn remove_dead_edges(&mut self, id: BytecodeInstructionId) {
724        let targets = self.jump_targets(id);
725        let block = self.function.graph_instruction(id).block;
726        let mut live = None;
727        for target in targets {
728            if target.dead {
729                self.function.blocks[block.index()]
730                    .successors
731                    .retain(|edge| edge.target != target.block);
732            } else {
733                live = Some(target.block);
734            }
735        }
736        if let Some(live) = live {
737            if let Some(edge) = self.function.blocks[block.index()]
738                .successors
739                .iter_mut()
740                .find(|edge| edge.target == live)
741            {
742                edge.kind = BytecodeEdgeKind::Fallthrough;
743            } else {
744                self.function.blocks[block.index()]
745                    .successors
746                    .push(BytecodeEdge::new(BytecodeEdgeKind::Fallthrough, live));
747            }
748        }
749    }
750
751    fn simplify_phis(&mut self) {
752        let blocks = self.visited_blocks.iter().copied().collect::<Vec<_>>();
753        for block in blocks {
754            let phis = self.function.block(block).phis.clone();
755            for id in phis {
756                let operands = self.function.phi(id).operands.clone();
757                let Some(unique) = operands.first().copied() else {
758                    continue;
759                };
760                if operands.iter().all(|operand| *operand == unique) {
761                    let phi = BytecodeOperand::Phi(id);
762                    for user in self.users(phi) {
763                        self.replace_user_operand(user, phi, unique);
764                    }
765                    self.function.phis[id.index()].users.clear();
766                    self.function.blocks[block.index()]
767                        .phis
768                        .retain(|phi| *phi != id);
769                }
770            }
771        }
772    }
773
774    fn update_block_reachability(&mut self) {
775        let mut reachable = HashSet::new();
776        let mut worklist = vec![self.function.entry_block];
777        reachable.insert(self.function.entry_block);
778        reachable.insert(self.function.exit_block);
779        while let Some(block) = worklist.pop() {
780            for edge in self.function.block(block).successors.iter() {
781                if reachable.insert(edge.target) {
782                    worklist.push(edge.target);
783                }
784            }
785        }
786        for (index, block) in self.function.blocks.iter_mut().enumerate() {
787            let id = BytecodeBlockId::new(index);
788            block.use_count = self.block_uses.get(&id).map_or(0, |uses| uses.len() as u32);
789            block.dead = !reachable.contains(&id);
790        }
791    }
792
793    fn set_instruction_operands(
794        &mut self,
795        id: BytecodeInstructionId,
796        operands: Vec<BytecodeOperand>,
797    ) {
798        let user = BytecodeOperand::Instruction(id);
799        let old = std::mem::replace(
800            &mut self.function.instructions[id.index()].operands,
801            operands,
802        );
803        for operand in old {
804            self.erase_use(operand, user);
805        }
806        let new = self.function.instructions[id.index()].operands.clone();
807        for operand in new {
808            record_use(
809                &mut self.function.instructions,
810                &mut self.function.phis,
811                operand,
812                user,
813            );
814        }
815    }
816
817    fn replace_user_operand(
818        &mut self,
819        user: BytecodeOperand,
820        old: BytecodeOperand,
821        new: BytecodeOperand,
822    ) {
823        match user {
824            BytecodeOperand::Instruction(id) => {
825                for operand in &mut self.function.instructions[id.index()].operands {
826                    if *operand == old {
827                        *operand = new;
828                    }
829                }
830            }
831            BytecodeOperand::Phi(id) => {
832                for operand in &mut self.function.phis[id.index()].operands {
833                    if *operand == old {
834                        *operand = new;
835                    }
836                }
837            }
838            _ => return,
839        }
840        self.erase_use(old, user);
841        record_use(
842            &mut self.function.instructions,
843            &mut self.function.phis,
844            new,
845            user,
846        );
847    }
848
849    fn erase_use(&mut self, used: BytecodeOperand, user: BytecodeOperand) {
850        match used {
851            BytecodeOperand::Instruction(id) => self.function.instructions[id.index()]
852                .users
853                .retain(|candidate| *candidate != user),
854            BytecodeOperand::Phi(id) => self.function.phis[id.index()]
855                .users
856                .retain(|candidate| *candidate != user),
857            _ => {}
858        }
859    }
860
861    fn erase_instruction(&mut self, id: BytecodeInstructionId) {
862        let block = self.function.graph_instruction(id).block;
863        self.function.blocks[block.index()]
864            .instruction_ids
865            .retain(|candidate| *candidate != id);
866    }
867
868    fn erase_dead_producer(&mut self, operand: BytecodeOperand) {
869        let BytecodeOperand::Instruction(id) = operand else {
870            return;
871        };
872        let instruction = self.function.graph_instruction(id);
873        if !matches!(
874            instruction.opcode,
875            Opcode::LoadK
876                | Opcode::LoadKx
877                | Opcode::LoadN
878                | Opcode::LoadB
879                | Opcode::LoadNil
880                | Opcode::GetUpval
881        ) || !instruction.users.is_empty()
882        {
883            return;
884        }
885        self.erase_instruction(id);
886    }
887
888    fn users(&self, operand: BytecodeOperand) -> Vec<BytecodeOperand> {
889        match operand {
890            BytecodeOperand::Instruction(id) => {
891                self.function.instructions[id.index()].users.clone()
892            }
893            BytecodeOperand::Phi(id) => self.function.phis[id.index()].users.clone(),
894            _ => Vec::new(),
895        }
896    }
897
898    fn immediate(&self, operand: BytecodeOperand) -> Option<BytecodeImmediate> {
899        let BytecodeOperand::Immediate(id) = operand else {
900            return None;
901        };
902        self.function.immediates.get(id.index()).copied()
903    }
904
905    fn add_immediate(&mut self, immediate: BytecodeImmediate) -> BytecodeOperand {
906        if let Some(index) = self
907            .function
908            .immediates
909            .iter()
910            .position(|existing| *existing == immediate)
911        {
912            BytecodeOperand::Immediate(BytecodeImmediateId::new(index))
913        } else {
914            self.function.immediates.push(immediate);
915            BytecodeOperand::Immediate(BytecodeImmediateId::new(self.function.immediates.len() - 1))
916        }
917    }
918
919    fn constant(&self, index: ConstantIndex) -> Option<&BytecodeFunctionConstant<'table>> {
920        usize::try_from(index)
921            .ok()
922            .and_then(|index| self.function.constants.get(index))
923    }
924
925    fn find_or_add_constant(
926        &mut self,
927        constant: BytecodeFunctionConstant<'table>,
928    ) -> ConstantIndex {
929        if let Some(index) = self
930            .function
931            .constants
932            .iter()
933            .position(|existing| *existing == constant)
934        {
935            index as ConstantIndex
936        } else {
937            self.function.constants.push(constant);
938            (self.function.constants.len() - 1) as ConstantIndex
939        }
940    }
941
942    fn constant_is_falsey(&self, index: ConstantIndex) -> bool {
943        self.constant(index).is_some_and(|constant| {
944            matches!(
945                constant,
946                BytecodeFunctionConstant::Nil | BytecodeFunctionConstant::Boolean(false)
947            )
948        })
949    }
950
951    fn is_orderable(&self, lattice: ConstantLattice) -> bool {
952        match lattice {
953            ConstantLattice::VmConstant(index) => self.constant(index).is_some_and(|constant| {
954                matches!(
955                    constant,
956                    BytecodeFunctionConstant::Number(_)
957                        | BytecodeFunctionConstant::Integer(_)
958                        | BytecodeFunctionConstant::String(_)
959                )
960            }),
961            ConstantLattice::Immediate(BytecodeImmediate::Int(_)) => true,
962            _ => false,
963        }
964    }
965}
966
967pub fn fold_constants(function: &mut BytecodeFunction<'_>) {
968    let mut sccp = Sccp::new(function);
969    sccp.propagate();
970    sccp.rewrite();
971}
972
973fn block_operand(operand: BytecodeOperand) -> Option<BytecodeBlockId> {
974    let BytecodeOperand::Block(block) = operand else {
975        return None;
976    };
977    Some(block)
978}
979
980fn arithmetic_constant_opcode(opcode: Opcode) -> Option<Opcode> {
981    match opcode {
982        Opcode::Add => Some(Opcode::AddK),
983        Opcode::Sub => Some(Opcode::SubK),
984        Opcode::Mul => Some(Opcode::MulK),
985        Opcode::Div => Some(Opcode::DivK),
986        Opcode::Mod => Some(Opcode::ModK),
987        Opcode::Pow => Some(Opcode::PowK),
988        _ => None,
989    }
990}
991
992fn compare_constants(
993    left: &BytecodeFunctionConstant<'_>,
994    right: &BytecodeFunctionConstant<'_>,
995) -> Option<std::cmp::Ordering> {
996    match (left, right) {
997        (BytecodeFunctionConstant::Number(left), BytecodeFunctionConstant::Number(right)) => {
998            left.partial_cmp(right)
999        }
1000        (BytecodeFunctionConstant::Integer(left), BytecodeFunctionConstant::Integer(right)) => {
1001            Some(left.cmp(right))
1002        }
1003        (BytecodeFunctionConstant::Boolean(left), BytecodeFunctionConstant::Boolean(right)) => {
1004            Some(if left == right {
1005                std::cmp::Ordering::Equal
1006            } else {
1007                std::cmp::Ordering::Greater
1008            })
1009        }
1010        (BytecodeFunctionConstant::String(left), BytecodeFunctionConstant::String(right)) => {
1011            Some(left.cmp(right))
1012        }
1013        _ => None,
1014    }
1015}
1016
1017fn compare_immediates(
1018    left: BytecodeImmediate,
1019    right: BytecodeImmediate,
1020) -> Option<std::cmp::Ordering> {
1021    match (left, right) {
1022        (BytecodeImmediate::Int(left), BytecodeImmediate::Int(right)) => Some(left.cmp(&right)),
1023        (BytecodeImmediate::Boolean(left), BytecodeImmediate::Boolean(right)) => {
1024            Some(if left == right {
1025                std::cmp::Ordering::Equal
1026            } else {
1027                std::cmp::Ordering::Greater
1028            })
1029        }
1030        _ => None,
1031    }
1032}
1033
1034fn compare_constant_immediate(
1035    constant: &BytecodeFunctionConstant<'_>,
1036    immediate: BytecodeImmediate,
1037) -> Option<std::cmp::Ordering> {
1038    match (constant, immediate) {
1039        (BytecodeFunctionConstant::Number(left), BytecodeImmediate::Int(right)) => {
1040            left.partial_cmp(&f64::from(right))
1041        }
1042        (BytecodeFunctionConstant::Integer(left), BytecodeImmediate::Int(right)) => {
1043            Some(left.cmp(&i64::from(right)))
1044        }
1045        (BytecodeFunctionConstant::Boolean(left), BytecodeImmediate::Boolean(right)) => {
1046            Some(if *left == right {
1047                std::cmp::Ordering::Equal
1048            } else {
1049                std::cmp::Ordering::Greater
1050            })
1051        }
1052        _ => None,
1053    }
1054}
1055
1056fn constants_equal(
1057    left: Option<&BytecodeFunctionConstant<'_>>,
1058    right: Option<&BytecodeFunctionConstant<'_>>,
1059) -> Option<bool> {
1060    match (left?, right?) {
1061        (BytecodeFunctionConstant::Number(left), BytecodeFunctionConstant::Integer(right)) => {
1062            Some(*left == *right as f64)
1063        }
1064        (BytecodeFunctionConstant::Integer(left), BytecodeFunctionConstant::Number(right)) => {
1065            Some(*left as f64 == *right)
1066        }
1067        (left, right) => compare_constants(left, right).map(std::cmp::Ordering::is_eq),
1068    }
1069}
1070
1071fn constant_equals_immediate(
1072    constant: &BytecodeFunctionConstant<'_>,
1073    immediate: BytecodeImmediate,
1074) -> Option<bool> {
1075    compare_constant_immediate(constant, immediate).map(std::cmp::Ordering::is_eq)
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080    use super::*;
1081    use crate::model::{BytecodeTypedLocal, Register};
1082
1083    struct TestGraph {
1084        function: BytecodeFunction<'static>,
1085    }
1086
1087    impl TestGraph {
1088        fn new() -> Self {
1089            Self {
1090                function: BytecodeFunction {
1091                    max_stack_size: 0,
1092                    num_params: 0,
1093                    upvalue_count: 0,
1094                    is_vararg: false,
1095                    flags: 0,
1096                    type_info: Vec::new(),
1097                    upvalue_types: Vec::new(),
1098                    local_types: Vec::<BytecodeTypedLocal>::new(),
1099                    blocks: Vec::new(),
1100                    instructions: Vec::new(),
1101                    constants: Vec::new(),
1102                    immediates: Vec::new(),
1103                    phis: Vec::new(),
1104                    projections: Vec::new(),
1105                    registers: HashMap::new(),
1106                    table_shapes: Vec::new(),
1107                    class_shapes: Vec::new(),
1108                    entry_block: BytecodeBlockId::new(0),
1109                    exit_block: BytecodeBlockId::new(0),
1110                    pc_to_block: Vec::new(),
1111                    pc_to_instruction: Vec::new(),
1112                    protos: Vec::new(),
1113                    line_defined: 0,
1114                    debug_name: &[],
1115                    lines: Vec::new(),
1116                    locals: Vec::new(),
1117                    upvalue_names: Vec::new(),
1118                },
1119            }
1120        }
1121
1122        fn block(&mut self) -> BytecodeBlockId {
1123            let id = BytecodeBlockId::new(self.function.blocks.len());
1124            self.function
1125                .blocks
1126                .push(BytecodeBlock::new(InstructionPc::new(usize::MAX)));
1127            id
1128        }
1129
1130        fn connect(
1131            &mut self,
1132            source: BytecodeBlockId,
1133            target: BytecodeBlockId,
1134            kind: BytecodeEdgeKind,
1135        ) {
1136            connect_blocks(&mut self.function.blocks, source, target, kind);
1137        }
1138
1139        fn immediate(&mut self, value: BytecodeImmediate) -> BytecodeOperand {
1140            self.function.immediates.push(value);
1141            BytecodeOperand::Immediate(BytecodeImmediateId::new(self.function.immediates.len() - 1))
1142        }
1143
1144        fn constant(&mut self, value: BytecodeFunctionConstant<'static>) -> BytecodeOperand {
1145            self.function.constants.push(value);
1146            BytecodeOperand::VmConstant((self.function.constants.len() - 1) as i32)
1147        }
1148
1149        fn instruction(
1150            &mut self,
1151            block: BytecodeBlockId,
1152            opcode: Opcode,
1153            operands: Vec<BytecodeOperand>,
1154            register: Option<Register>,
1155        ) -> BytecodeOperand {
1156            let id = BytecodeInstructionId::new(self.function.instructions.len());
1157            self.function.instructions.push(BytecodeInstruction {
1158                pc: InstructionPc::new(usize::MAX),
1159                opcode,
1160                line: 0,
1161                block,
1162                operands,
1163                users: Vec::new(),
1164            });
1165            self.function.blocks[block.index()].instruction_ids.push(id);
1166            let operand = BytecodeOperand::Instruction(id);
1167            if let Some(register) = register {
1168                self.function.registers.insert(operand, register);
1169            }
1170            operand
1171        }
1172
1173        fn phi(
1174            &mut self,
1175            block: BytecodeBlockId,
1176            operands: Vec<BytecodeOperand>,
1177            register: Register,
1178        ) -> BytecodeOperand {
1179            let id = BytecodePhiId::new(self.function.phis.len());
1180            self.function.phis.push(BytecodePhi {
1181                operands,
1182                users: Vec::new(),
1183            });
1184            self.function.blocks[block.index()].phis.push(id);
1185            let operand = BytecodeOperand::Phi(id);
1186            self.function.registers.insert(operand, register);
1187            operand
1188        }
1189
1190        fn finish(
1191            mut self,
1192            entry: BytecodeBlockId,
1193            exit: BytecodeBlockId,
1194        ) -> BytecodeFunction<'static> {
1195            self.function.entry_block = entry;
1196            self.function.exit_block = exit;
1197            rebuild_uses(&mut self.function.instructions, &mut self.function.phis);
1198            self.function
1199        }
1200    }
1201
1202    fn add_return(graph: &mut TestGraph, block: BytecodeBlockId, exit: BytecodeBlockId) {
1203        graph.instruction(block, Opcode::Return, Vec::new(), None);
1204        graph.connect(block, exit, BytecodeEdgeKind::Fallthrough);
1205    }
1206
1207    // Sccp.test.cpp: sccp_does_not_fold_boolean_ordering
1208    #[test]
1209    fn does_not_fold_boolean_ordering() {
1210        let mut graph = TestGraph::new();
1211        let entry = graph.block();
1212        let on_true = graph.block();
1213        let on_false = graph.block();
1214        let exit = graph.block();
1215        let value = graph.immediate(BytecodeImmediate::Boolean(true));
1216        let load = graph.instruction(entry, Opcode::LoadB, vec![value], Some(0));
1217        graph.instruction(
1218            entry,
1219            Opcode::JumpIfLt,
1220            vec![load, load, BytecodeOperand::Block(on_true)],
1221            None,
1222        );
1223        graph.connect(entry, on_true, BytecodeEdgeKind::Branch);
1224        graph.connect(entry, on_false, BytecodeEdgeKind::Fallthrough);
1225        add_return(&mut graph, on_true, exit);
1226        add_return(&mut graph, on_false, exit);
1227
1228        let mut function = graph.finish(entry, exit);
1229        fold_constants(&mut function);
1230        assert!(!function.block(on_true).is_dead());
1231        assert!(!function.block(on_false).is_dead());
1232    }
1233
1234    // Sccp.test.cpp: sccp_folds_number_ordering
1235    #[test]
1236    fn folds_number_ordering() {
1237        let mut graph = TestGraph::new();
1238        let entry = graph.block();
1239        let on_true = graph.block();
1240        let on_false = graph.block();
1241        let exit = graph.block();
1242        let one = graph.immediate(BytecodeImmediate::Int(1));
1243        let two = graph.immediate(BytecodeImmediate::Int(2));
1244        let load_one = graph.instruction(entry, Opcode::LoadN, vec![one], Some(0));
1245        let load_two = graph.instruction(entry, Opcode::LoadN, vec![two], Some(1));
1246        graph.instruction(
1247            entry,
1248            Opcode::JumpIfLt,
1249            vec![load_one, load_two, BytecodeOperand::Block(on_true)],
1250            None,
1251        );
1252        graph.connect(entry, on_true, BytecodeEdgeKind::Branch);
1253        graph.connect(entry, on_false, BytecodeEdgeKind::Fallthrough);
1254        add_return(&mut graph, on_true, exit);
1255        add_return(&mut graph, on_false, exit);
1256
1257        let mut function = graph.finish(entry, exit);
1258        fold_constants(&mut function);
1259        assert!(!function.block(on_true).is_dead());
1260        assert!(function.block(on_false).is_dead());
1261    }
1262
1263    // Sccp.test.cpp: sccp_phi_filters_dead_predecessor
1264    #[test]
1265    fn phi_filters_dead_predecessor() {
1266        let mut graph = TestGraph::new();
1267        let entry = graph.block();
1268        let on_true = graph.block();
1269        let on_false = graph.block();
1270        let merge = graph.block();
1271        let exit = graph.block();
1272        let condition = graph.immediate(BytecodeImmediate::Boolean(true));
1273        let condition = graph.instruction(entry, Opcode::LoadB, vec![condition], Some(0));
1274        graph.instruction(
1275            entry,
1276            Opcode::JumpIf,
1277            vec![condition, BytecodeOperand::Block(on_true)],
1278            None,
1279        );
1280        graph.connect(entry, on_true, BytecodeEdgeKind::Branch);
1281        graph.connect(entry, on_false, BytecodeEdgeKind::Fallthrough);
1282        let forty_two = graph.constant(BytecodeFunctionConstant::Number(42.0));
1283        let ninety_nine = graph.constant(BytecodeFunctionConstant::Number(99.0));
1284        let load_true = graph.instruction(on_true, Opcode::LoadK, vec![forty_two], Some(1));
1285        let load_false = graph.instruction(on_false, Opcode::LoadK, vec![ninety_nine], Some(1));
1286        graph.connect(on_true, merge, BytecodeEdgeKind::Fallthrough);
1287        graph.connect(on_false, merge, BytecodeEdgeKind::Fallthrough);
1288        let phi = graph.phi(merge, vec![load_true, load_false], 1);
1289        graph.instruction(merge, Opcode::Return, vec![phi], None);
1290        graph.connect(merge, exit, BytecodeEdgeKind::Fallthrough);
1291
1292        let mut function = graph.finish(entry, exit);
1293        let mut sccp = Sccp::new(&mut function);
1294        sccp.propagate();
1295        assert_eq!(sccp.lattice(phi), ConstantLattice::VmConstant(0));
1296        sccp.rewrite();
1297        assert!(function.block(on_false).is_dead());
1298    }
1299
1300    // Sccp.test.cpp: sccp_erases_trivial_phi
1301    #[test]
1302    fn erases_trivial_phi() {
1303        let mut graph = TestGraph::new();
1304        let entry = graph.block();
1305        let exit = graph.block();
1306        let value = graph.constant(BytecodeFunctionConstant::Number(42.0));
1307        let load = graph.instruction(entry, Opcode::LoadK, vec![value], Some(0));
1308        graph.connect(entry, exit, BytecodeEdgeKind::Fallthrough);
1309        let phi = graph.phi(exit, vec![load, load], 0);
1310        graph.instruction(exit, Opcode::Return, vec![phi], None);
1311
1312        let mut function = graph.finish(entry, exit);
1313        fold_constants(&mut function);
1314        assert!(function.block(exit).phis().is_empty());
1315    }
1316
1317    fn arithmetic_graph(
1318        opcode: Opcode,
1319        value: f64,
1320        constant_on_left: bool,
1321    ) -> (BytecodeFunction<'static>, BytecodeBlockId, BytecodeOperand) {
1322        let mut graph = TestGraph::new();
1323        let entry = graph.block();
1324        let exit = graph.block();
1325        let constant = graph.constant(BytecodeFunctionConstant::Number(value));
1326        let load = graph.instruction(entry, Opcode::LoadK, vec![constant], Some(0));
1327        let upvalue = graph.instruction(
1328            entry,
1329            Opcode::GetUpval,
1330            vec![BytecodeOperand::VmUpvalue(0)],
1331            Some(1),
1332        );
1333        let operands = if constant_on_left {
1334            vec![load, upvalue]
1335        } else {
1336            vec![upvalue, load]
1337        };
1338        let arithmetic = graph.instruction(entry, opcode, operands, Some(2));
1339        graph.instruction(entry, Opcode::Return, vec![arithmetic], None);
1340        graph.connect(entry, exit, BytecodeEdgeKind::Fallthrough);
1341        (graph.finish(entry, exit), entry, arithmetic)
1342    }
1343
1344    // Sccp.test.cpp: sccp_loadk_mul_to_mulk
1345    #[test]
1346    fn loadk_mul_to_mulk() {
1347        let (mut function, entry, arithmetic) = arithmetic_graph(Opcode::Mul, 42.0, true);
1348        fold_constants(&mut function);
1349        assert_eq!(function.block(entry).graph_instructions().len(), 3);
1350        let BytecodeOperand::Instruction(id) = arithmetic else {
1351            unreachable!();
1352        };
1353        assert_eq!(function.graph_instruction(id).opcode(), Opcode::MulK);
1354    }
1355
1356    // Sccp.test.cpp: sccp_loadk_div_to_divrk
1357    #[test]
1358    fn loadk_div_to_divrk() {
1359        let (mut function, entry, arithmetic) = arithmetic_graph(Opcode::Div, 42.0, true);
1360        fold_constants(&mut function);
1361        assert_eq!(function.block(entry).graph_instructions().len(), 3);
1362        let BytecodeOperand::Instruction(id) = arithmetic else {
1363            unreachable!();
1364        };
1365        let instruction = function.graph_instruction(id);
1366        assert_eq!(instruction.opcode(), Opcode::DivRK);
1367        assert_eq!(instruction.operands()[0], BytecodeOperand::VmConstant(0));
1368    }
1369
1370    // Sccp.test.cpp: sccp_loadk_mul_to_zero
1371    #[test]
1372    fn loadk_mul_to_zero() {
1373        let (mut function, entry, arithmetic) = arithmetic_graph(Opcode::Mul, 0.0, true);
1374        fold_constants(&mut function);
1375        assert_eq!(function.block(entry).graph_instructions().len(), 3);
1376        let BytecodeOperand::Instruction(id) = arithmetic else {
1377            unreachable!();
1378        };
1379        let instruction = function.graph_instruction(id);
1380        assert_eq!(instruction.opcode(), Opcode::LoadN);
1381        let BytecodeOperand::Immediate(value) = instruction.operands()[0] else {
1382            unreachable!();
1383        };
1384        assert_eq!(*function.immediate(value), BytecodeImmediate::Int(0));
1385    }
1386}