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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}