1use std::collections::{HashMap, HashSet};
12
13use cranelift::prelude::IntCC;
14use shape_vm::bytecode::{BytecodeProgram, Instruction, OpCode, Operand};
15
16#[derive(Debug, Clone)]
18pub struct LoopInfo {
19 pub header_idx: usize,
21 pub end_idx: usize,
23 pub body_locals_written: HashSet<u16>,
25 pub body_locals_read: HashSet<u16>,
27 pub body_module_bindings_written: HashSet<u16>,
29 pub body_module_bindings_read: HashSet<u16>,
31 pub induction_vars: Vec<InductionVar>,
33 pub invariant_locals: HashSet<u16>,
35 pub invariant_module_bindings: HashSet<u16>,
37 pub body_can_allocate: bool,
41 pub hoistable_calls: Vec<usize>,
45}
46
47#[derive(Debug, Clone)]
50pub struct InductionVar {
51 pub local_slot: u16,
53 pub is_module_binding: bool,
55 pub bound_cmp: IntCC,
57 pub bound_slot: Option<u16>,
59 pub step_value: Option<i64>,
61}
62
63pub fn analyze_loops(program: &BytecodeProgram) -> HashMap<usize, LoopInfo> {
68 let mut result = HashMap::new();
69
70 let mut loop_starts: Vec<usize> = Vec::new();
72 let mut loop_pairs: Vec<(usize, usize)> = Vec::new();
73
74 for (i, instr) in program.instructions.iter().enumerate() {
75 match instr.opcode {
76 OpCode::LoopStart => loop_starts.push(i),
77 OpCode::LoopEnd => {
78 if let Some(start) = loop_starts.pop() {
79 loop_pairs.push((start, i));
80 }
81 }
82 _ => {}
83 }
84 }
85
86 for (start_idx, end_idx) in loop_pairs {
88 let body_can_allocate = program.instructions[start_idx + 1..end_idx]
90 .iter()
91 .any(|instr| !opcode_is_non_allocating(instr.opcode));
92
93 let mut info = LoopInfo {
94 header_idx: start_idx,
95 end_idx,
96 body_locals_written: HashSet::new(),
97 body_locals_read: HashSet::new(),
98 body_module_bindings_written: HashSet::new(),
99 body_module_bindings_read: HashSet::new(),
100 induction_vars: Vec::new(),
101 invariant_locals: HashSet::new(),
102 invariant_module_bindings: HashSet::new(),
103 hoistable_calls: Vec::new(),
104 body_can_allocate,
105 };
106
107 let mut nested_depth = 0usize;
111 for i in (start_idx + 1)..end_idx {
112 let instr = &program.instructions[i];
113 match instr.opcode {
114 OpCode::LoopStart => {
115 nested_depth += 1;
116 continue;
117 }
118 OpCode::LoopEnd => {
119 nested_depth = nested_depth.saturating_sub(1);
120 continue;
121 }
122 _ => {}
123 }
124 if nested_depth > 0 {
125 continue;
126 }
127 match instr.opcode {
128 OpCode::StoreLocal => {
129 if let Some(Operand::Local(idx)) = &instr.operand {
130 info.body_locals_written.insert(*idx);
131 }
132 }
133 OpCode::StoreLocalTyped => {
134 if let Some(Operand::TypedLocal(idx, _)) = &instr.operand {
135 info.body_locals_written.insert(*idx);
136 }
137 }
138 OpCode::LoadLocal | OpCode::LoadLocalTrusted => {
139 if let Some(Operand::Local(idx)) = &instr.operand {
140 info.body_locals_read.insert(*idx);
141 }
142 }
143 OpCode::StoreModuleBinding => {
144 if let Some(Operand::ModuleBinding(idx)) = &instr.operand {
145 info.body_module_bindings_written.insert(*idx);
146 }
147 }
148 OpCode::LoadModuleBinding => {
149 if let Some(Operand::ModuleBinding(idx)) = &instr.operand {
150 info.body_module_bindings_read.insert(*idx);
151 }
152 }
153 _ => {}
154 }
155 }
156
157 for &local in &info.body_locals_read {
159 if !info.body_locals_written.contains(&local) {
160 info.invariant_locals.insert(local);
161 }
162 }
163
164 for &mb in &info.body_module_bindings_read {
166 if !info.body_module_bindings_written.contains(&mb) {
167 info.invariant_module_bindings.insert(mb);
168 }
169 }
170
171 detect_induction_vars(
175 &program.instructions,
176 start_idx,
177 end_idx,
178 &mut info,
179 &program.constants,
180 );
181
182 result.insert(start_idx, info);
183 }
184
185 result
186}
187
188fn detect_induction_vars(
197 instrs: &[Instruction],
198 start_idx: usize,
199 end_idx: usize,
200 info: &mut LoopInfo,
201 constants: &[shape_vm::bytecode::Constant],
202) {
203 let mut depth_by_idx = vec![0usize; instrs.len()];
206 let mut depth = 0usize;
207 for i in (start_idx + 1)..end_idx {
208 depth_by_idx[i] = depth;
209 match instrs[i].opcode {
210 OpCode::LoopStart => depth += 1,
211 OpCode::LoopEnd => depth = depth.saturating_sub(1),
212 _ => {}
213 }
214 }
215
216 for i in (start_idx + 1)..end_idx.saturating_sub(3) {
218 if depth_by_idx[i] != 0
219 || depth_by_idx[i + 1] != 0
220 || depth_by_idx[i + 2] != 0
221 || depth_by_idx[i + 3] != 0
222 {
223 continue;
224 }
225 let (load, step_src, arith, store) =
226 (&instrs[i], &instrs[i + 1], &instrs[i + 2], &instrs[i + 3]);
227
228 let is_arith = matches!(
229 arith.opcode,
230 OpCode::AddInt | OpCode::SubInt
231 );
232 let is_supported_step_src = matches!(
233 step_src.opcode,
234 OpCode::PushConst
235 | OpCode::LoadLocal
236 | OpCode::LoadLocalTrusted
237 | OpCode::LoadModuleBinding
238 );
239 if !is_arith || !is_supported_step_src {
240 continue;
241 }
242
243 let step_value = if step_src.opcode == OpCode::PushConst {
246 if let Some(Operand::Const(const_idx)) = &step_src.operand {
247 match constants.get(*const_idx as usize) {
248 Some(shape_vm::bytecode::Constant::Int(n)) => {
249 let step = if matches!(arith.opcode, OpCode::SubInt) {
250 -n
251 } else {
252 *n
253 };
254 Some(step)
255 }
256 Some(shape_vm::bytecode::Constant::UInt(n)) => {
257 let step = *n as i64;
258 let step = if matches!(arith.opcode, OpCode::SubInt) {
259 -step
260 } else {
261 step
262 };
263 Some(step)
264 }
265 Some(shape_vm::bytecode::Constant::Number(n)) if *n == (*n as i64) as f64 => {
266 let step = if matches!(arith.opcode, OpCode::SubInt) {
267 -(*n as i64)
268 } else {
269 *n as i64
270 };
271 Some(step)
272 }
273 _ => None,
274 }
275 } else {
276 None
277 }
278 } else {
279 None
280 };
281
282 if matches!(load.opcode, OpCode::LoadLocal | OpCode::LoadLocalTrusted)
284 && matches!(store.opcode, OpCode::StoreLocal | OpCode::StoreLocalTyped)
285 {
286 let store_local_idx = match &store.operand {
287 Some(Operand::Local(idx)) => Some(*idx),
288 Some(Operand::TypedLocal(idx, _)) => Some(*idx),
289 _ => None,
290 };
291 if let (Some(Operand::Local(load_idx)), Some(store_idx)) =
292 (&load.operand, store_local_idx)
293 {
294 if *load_idx == store_idx {
295 let bound_info =
296 detect_bound_comparison(instrs, start_idx, end_idx, *load_idx, false);
297 if bound_info.1.is_none() {
298 continue;
299 }
300
301 info.induction_vars.push(InductionVar {
302 local_slot: *load_idx,
303 is_module_binding: false,
304 bound_cmp: bound_info.0,
305 bound_slot: bound_info.1,
306 step_value,
307 });
308 }
309 }
310 }
311
312 if load.opcode == OpCode::LoadModuleBinding && store.opcode == OpCode::StoreModuleBinding {
314 if let (
315 Some(Operand::ModuleBinding(load_idx)),
316 Some(Operand::ModuleBinding(store_idx)),
317 ) = (&load.operand, &store.operand)
318 {
319 if load_idx == store_idx {
320 let bound_info =
321 detect_bound_comparison(instrs, start_idx, end_idx, *load_idx, true);
322 if bound_info.1.is_none() {
323 continue;
324 }
325
326 info.induction_vars.push(InductionVar {
327 local_slot: *load_idx,
328 is_module_binding: true,
329 bound_cmp: bound_info.0,
330 bound_slot: bound_info.1,
331 step_value,
332 });
333 }
334 }
335 }
336 }
337}
338
339fn detect_bound_comparison(
344 instrs: &[Instruction],
345 start_idx: usize,
346 end_idx: usize,
347 indvar_slot: u16,
348 is_module_binding: bool,
349) -> (IntCC, Option<u16>) {
350 let (load_op, bound_load_op) = if is_module_binding {
351 (OpCode::LoadModuleBinding, OpCode::LoadModuleBinding)
352 } else {
353 (OpCode::LoadLocal, OpCode::LoadLocal)
354 };
355
356 let scan_end = (start_idx + 10).min(end_idx);
358 for window in instrs[start_idx + 1..scan_end].windows(3) {
359 let (load1, load2, cmp) = (&window[0], &window[1], &window[2]);
360
361 let load1_matches = load1.opcode == load_op
362 || (!is_module_binding && load1.opcode == OpCode::LoadLocalTrusted);
363 let load2_matches = load2.opcode == bound_load_op
364 || (!is_module_binding && load2.opcode == OpCode::LoadLocalTrusted);
365 if load1_matches && load2_matches {
366 let l1 = match &load1.operand {
367 Some(Operand::Local(idx)) if !is_module_binding => Some(*idx),
368 Some(Operand::ModuleBinding(idx)) if is_module_binding => Some(*idx),
369 _ => None,
370 };
371 let l2 = match &load2.operand {
372 Some(Operand::Local(idx)) if !is_module_binding => Some(*idx),
373 Some(Operand::ModuleBinding(idx)) if is_module_binding => Some(*idx),
374 _ => None,
375 };
376
377 if let (Some(l1), Some(l2)) = (l1, l2) {
378 if l1 == indvar_slot {
379 let cc = match cmp.opcode {
380 OpCode::LtInt => IntCC::SignedLessThan,
381 OpCode::LteInt => IntCC::SignedLessThanOrEqual,
382 OpCode::GtInt => IntCC::SignedGreaterThan,
383 OpCode::GteInt => IntCC::SignedGreaterThanOrEqual,
384 _ => continue,
385 };
386 return (cc, Some(l2));
387 }
388 }
389 }
390 }
391
392 (IntCC::SignedLessThan, None) }
394
395fn opcode_is_non_allocating(opcode: OpCode) -> bool {
408 matches!(
409 opcode,
410 OpCode::PushConst
412 | OpCode::PushNull
413 | OpCode::Pop
414 | OpCode::Dup
415 | OpCode::Swap
416 | OpCode::AddInt
418 | OpCode::SubInt
419 | OpCode::MulInt
420 | OpCode::DivInt
421 | OpCode::ModInt
422 | OpCode::AddNumber
423 | OpCode::SubNumber
424 | OpCode::MulNumber
425 | OpCode::DivNumber
426 | OpCode::ModNumber
427 | OpCode::AddDecimal
428 | OpCode::SubDecimal
429 | OpCode::MulDecimal
430 | OpCode::DivDecimal
431 | OpCode::ModDecimal
432 | OpCode::NegInt
433 | OpCode::NegNumber
434 | OpCode::GtInt
436 | OpCode::LtInt
437 | OpCode::GteInt
438 | OpCode::LteInt
439 | OpCode::GtNumber
440 | OpCode::LtNumber
441 | OpCode::GteNumber
442 | OpCode::LteNumber
443 | OpCode::EqInt
444 | OpCode::EqNumber
445 | OpCode::NeqInt
446 | OpCode::NeqNumber
447 | OpCode::EqString
448 | OpCode::GtString
449 | OpCode::LtString
450 | OpCode::GteString
451 | OpCode::LteString
452 | OpCode::EqDecimal
453 | OpCode::IsNull
454 | OpCode::GtDecimal
455 | OpCode::LtDecimal
456 | OpCode::GteDecimal
457 | OpCode::LteDecimal
458 | OpCode::And
460 | OpCode::Or
461 | OpCode::Not
462 | OpCode::LoadLocal
464 | OpCode::LoadLocalTrusted
465 | OpCode::StoreLocal
466 | OpCode::StoreLocalTyped
467 | OpCode::LoadModuleBinding
468 | OpCode::StoreModuleBinding
469 | OpCode::StoreModuleBindingTyped
470 | OpCode::LoadClosure
471 | OpCode::StoreClosure
472 | OpCode::LoadOwnedMutableCapture
481 | OpCode::StoreOwnedMutableCapture
482 | OpCode::LoadSharedCapture
483 | OpCode::StoreSharedCapture
484 | OpCode::AllocSharedLocal
495 | OpCode::LoadSharedLocal
496 | OpCode::StoreSharedLocal
497 | OpCode::DropSharedLocal
498 | OpCode::AllocSharedModuleBinding
504 | OpCode::LoadSharedModuleBinding
505 | OpCode::StoreSharedModuleBinding
506 | OpCode::CastWidth
508 | OpCode::Jump
510 | OpCode::JumpIfFalse
511 | OpCode::JumpIfFalseTrusted
512 | OpCode::JumpIfTrue
513 | OpCode::LoopStart
514 | OpCode::LoopEnd
515 | OpCode::Break
516 | OpCode::Continue
517 | OpCode::Return
518 | OpCode::ReturnValue
519 | OpCode::BitAnd
521 | OpCode::BitOr
522 | OpCode::BitXor
523 | OpCode::BitShl
524 | OpCode::BitShr
525 | OpCode::BitNot
526 | OpCode::IntToNumber
528 | OpCode::NumberToInt
529 | OpCode::GetFieldTyped
531 | OpCode::SetFieldTyped
532 | OpCode::MakeRef
534 | OpCode::DerefLoad
535 | OpCode::DerefStore
536 | OpCode::SetIndexRef
537 | OpCode::GetProp
539 | OpCode::SetLocalIndex
540 | OpCode::SetModuleBindingIndex
541 | OpCode::Length
542 | OpCode::Halt
544 | OpCode::Nop
545 | OpCode::PushTimeframe
546 | OpCode::PopTimeframe
547 | OpCode::WrapTypeAnnotation
548 | OpCode::BindSchema
549 | OpCode::ErrorContext
550 | OpCode::CloseUpvalue
551 | OpCode::Yield
553 | OpCode::Suspend
554 | OpCode::Resume
555 | OpCode::Poll
556 | OpCode::AwaitBar
557 | OpCode::AwaitTick
558 | OpCode::Await
559 | OpCode::DropCall
561 | OpCode::DropCallAsync
562 | OpCode::EmitAlert
564 | OpCode::EmitEvent
565 | OpCode::AsyncScopeEnter
566 | OpCode::AsyncScopeExit
567 | OpCode::BoxTraitObject
569 | OpCode::DynMethodCall
570 )
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use shape_vm::bytecode::*;
577
578 fn make_instr(opcode: OpCode, operand: Option<Operand>) -> Instruction {
579 Instruction { opcode, operand }
580 }
581
582 fn make_program(instrs: Vec<Instruction>, constants: Vec<Constant>) -> BytecodeProgram {
583 BytecodeProgram {
584 instructions: instrs,
585 constants,
586 strings: vec![],
587 functions: vec![],
588 debug_info: DebugInfo::default(),
589 data_schema: None,
590 module_binding_names: vec![],
591 top_level_locals_count: 0,
592 top_level_local_storage_hints: vec![],
593 type_schema_registry: Default::default(),
594 module_binding_storage_hints: vec![],
595 function_local_storage_hints: vec![],
596 compiled_annotations: Default::default(),
597 trait_method_symbols: Default::default(),
598 expanded_function_defs: Default::default(),
599 string_index: Default::default(),
600 foreign_functions: Vec::new(),
601 native_struct_layouts: vec![],
602 content_addressed: None,
603 function_blob_hashes: vec![],
604 top_level_frame: None,
605 ..Default::default()
606 }
607 }
608
609 #[test]
610 fn test_detect_invariant_locals() {
611 let instrs = vec![
613 make_instr(OpCode::LoopStart, None),
614 make_instr(OpCode::LoadLocal, Some(Operand::Local(0))), make_instr(OpCode::LoadLocal, Some(Operand::Local(1))), make_instr(OpCode::AddInt, None),
617 make_instr(OpCode::StoreLocal, Some(Operand::Local(0))), make_instr(OpCode::LoopEnd, None),
619 ];
620
621 let loops = analyze_loops(&make_program(instrs, vec![]));
622 assert_eq!(loops.len(), 1);
623
624 let info = loops.get(&0).unwrap();
625 assert!(info.body_locals_written.contains(&0)); assert!(!info.body_locals_written.contains(&1)); assert!(info.invariant_locals.contains(&1)); assert!(!info.invariant_locals.contains(&0)); }
630
631 #[test]
632 fn test_detect_induction_variable() {
633 let instrs = vec![
635 make_instr(OpCode::LoopStart, None),
636 make_instr(OpCode::LoadLocal, Some(Operand::Local(0))), make_instr(OpCode::LoadLocal, Some(Operand::Local(1))), make_instr(OpCode::LtInt, None),
640 make_instr(OpCode::JumpIfFalse, Some(Operand::Offset(5))),
641 make_instr(OpCode::LoadLocal, Some(Operand::Local(0))), make_instr(OpCode::PushConst, Some(Operand::Const(0))), make_instr(OpCode::AddInt, None),
646 make_instr(OpCode::StoreLocal, Some(Operand::Local(0))), make_instr(OpCode::LoopEnd, None),
648 ];
649
650 let loops = analyze_loops(&make_program(instrs, vec![Constant::Int(1)]));
651 let info = loops.get(&0).unwrap();
652
653 assert_eq!(info.induction_vars.len(), 1);
654 assert_eq!(info.induction_vars[0].local_slot, 0);
655 assert_eq!(info.induction_vars[0].bound_slot, Some(1));
656 assert_eq!(info.induction_vars[0].bound_cmp, IntCC::SignedLessThan);
657 }
658
659 #[test]
660 fn test_non_allocating_loop() {
661 let instrs = vec![
664 make_instr(OpCode::LoopStart, None),
665 make_instr(OpCode::LoadLocal, Some(Operand::Local(0))),
666 make_instr(OpCode::LoadLocal, Some(Operand::Local(1))),
667 make_instr(OpCode::LtInt, None),
668 make_instr(OpCode::JumpIfFalse, Some(Operand::Offset(7))),
669 make_instr(OpCode::LoadLocal, Some(Operand::Local(2))),
670 make_instr(OpCode::LoadLocal, Some(Operand::Local(0))),
671 make_instr(OpCode::AddInt, None),
672 make_instr(OpCode::StoreLocal, Some(Operand::Local(2))),
673 make_instr(OpCode::LoadLocal, Some(Operand::Local(0))),
674 make_instr(OpCode::PushConst, Some(Operand::Const(0))),
675 make_instr(OpCode::AddInt, None),
676 make_instr(OpCode::StoreLocal, Some(Operand::Local(0))),
677 make_instr(OpCode::LoopEnd, None),
678 ];
679
680 let loops = analyze_loops(&make_program(instrs, vec![Constant::Int(1)]));
681 let info = loops.get(&0).unwrap();
682 assert!(
683 !info.body_can_allocate,
684 "Pure arithmetic loop should not need GC safepoint"
685 );
686 }
687
688 #[test]
689 fn test_allocating_loop() {
690 let instrs = vec![
693 make_instr(OpCode::LoopStart, None),
694 make_instr(OpCode::LoadLocal, Some(Operand::Local(0))),
695 make_instr(OpCode::LoadLocal, Some(Operand::Local(1))),
696 make_instr(OpCode::ArrayPush, None),
697 make_instr(OpCode::LoopEnd, None),
698 ];
699
700 let loops = analyze_loops(&make_program(instrs, vec![]));
701 let info = loops.get(&0).unwrap();
702 assert!(
703 info.body_can_allocate,
704 "Loop with ArrayPush should need GC safepoint"
705 );
706 }
707
708 #[test]
709 fn test_loop_with_function_call_allocates() {
710 let instrs = vec![
713 make_instr(OpCode::LoopStart, None),
714 make_instr(OpCode::LoadLocal, Some(Operand::Local(0))),
715 make_instr(
716 OpCode::Call,
717 Some(Operand::Function(shape_value::FunctionId(0))),
718 ),
719 make_instr(OpCode::Pop, None),
720 make_instr(OpCode::LoopEnd, None),
721 ];
722
723 let loops = analyze_loops(&make_program(instrs, vec![]));
724 let info = loops.get(&0).unwrap();
725 assert!(
726 info.body_can_allocate,
727 "Loop with Call should need GC safepoint"
728 );
729 }
730
731 #[test]
732 fn test_loop_with_set_index_ref_is_non_allocating() {
733 let instrs = vec![
736 make_instr(OpCode::MakeRef, Some(Operand::Local(0))),
737 make_instr(OpCode::StoreLocal, Some(Operand::Local(3))), make_instr(OpCode::LoopStart, None),
739 make_instr(OpCode::LoadLocal, Some(Operand::Local(1))), make_instr(OpCode::LoadLocal, Some(Operand::Local(2))), make_instr(OpCode::LtInt, None),
742 make_instr(OpCode::JumpIfFalse, Some(Operand::Offset(8))),
743 make_instr(OpCode::PushConst, Some(Operand::Const(0))), make_instr(OpCode::LoadLocal, Some(Operand::Local(1))), make_instr(OpCode::SetIndexRef, Some(Operand::Local(3))),
746 make_instr(OpCode::LoadLocal, Some(Operand::Local(1))),
747 make_instr(OpCode::PushConst, Some(Operand::Const(1))),
748 make_instr(OpCode::AddInt, None),
749 make_instr(OpCode::StoreLocal, Some(Operand::Local(1))),
750 make_instr(OpCode::LoopEnd, None),
751 ];
752
753 let loops = analyze_loops(&make_program(
754 instrs,
755 vec![Constant::Bool(false), Constant::Int(1)],
756 ));
757 let info = loops.get(&2).unwrap();
758 assert!(
759 !info.body_can_allocate,
760 "Loop with SetIndexRef should be treated as non-allocating"
761 );
762 }
763}