1#![allow(
6 clippy::cast_possible_truncation,
7 clippy::cast_possible_wrap,
8 clippy::cast_sign_loss
9)]
10
11use std::collections::{BTreeMap, BTreeSet, HashSet};
12
13use serde::Serialize;
14
15use crate::instruction::{Instruction, OpCode, Operand};
16use crate::manifest::ContractManifest;
17use crate::nef::NefFile;
18use crate::{syscalls, util};
19
20use super::{MethodRef, MethodTable};
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
24#[non_exhaustive]
25pub enum CallTarget {
26 Internal {
28 method: MethodRef,
30 },
31 MethodToken {
33 index: u16,
35 hash_le: String,
37 hash_be: String,
39 method: String,
41 parameters_count: u16,
43 has_return_value: bool,
45 call_flags: u8,
47 },
48 Syscall {
50 hash: u32,
52 name: Option<String>,
54 returns_value: bool,
56 },
57 Indirect {
59 opcode: String,
61 operand: Option<u16>,
63 },
64 UnresolvedInternal {
66 target: isize,
68 },
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
73pub struct CallEdge {
74 pub caller: MethodRef,
76 pub call_offset: usize,
78 pub opcode: String,
80 pub target: CallTarget,
82}
83
84#[derive(Debug, Clone, Default, Serialize)]
86pub struct CallGraph {
87 pub methods: Vec<MethodRef>,
89 pub edges: Vec<CallEdge>,
91}
92
93#[must_use]
95pub fn build_call_graph(
96 nef: &NefFile,
97 instructions: &[Instruction],
98 manifest: Option<&ContractManifest>,
99) -> CallGraph {
100 let table = MethodTable::new(instructions, manifest);
101 let mut methods: BTreeMap<usize, MethodRef> = table
102 .spans()
103 .iter()
104 .map(|span| (span.method.offset, span.method.clone()))
105 .collect();
106
107 let instruction_offsets: HashSet<usize> = instructions.iter().map(|i| i.offset).collect();
112
113 let mut edges = Vec::new();
114 for (index, instr) in instructions.iter().enumerate() {
115 match instr.opcode {
116 OpCode::Syscall => {
117 let Some(Operand::Syscall(hash)) = instr.operand else {
118 continue;
119 };
120 let info = syscalls::lookup(hash);
121 edges.push(CallEdge {
122 caller: table.method_for_offset(instr.offset),
123 call_offset: instr.offset,
124 opcode: instr.opcode.to_string(),
125 target: CallTarget::Syscall {
126 hash,
127 name: info.map(|i| i.name.to_string()),
128 returns_value: info.map(|i| i.returns_value).unwrap_or(true),
129 },
130 });
131 }
132 OpCode::Call | OpCode::Call_L => {
133 let caller = table.method_for_offset(instr.offset);
134 match relative_target_isize(instr) {
135 Some(target)
136 if target >= 0 && instruction_offsets.contains(&(target as usize)) =>
137 {
138 let target = target as usize;
139 let callee = table.resolve_internal_target(target);
140 methods.insert(callee.offset, callee.clone());
141 edges.push(CallEdge {
142 caller,
143 call_offset: instr.offset,
144 opcode: instr.opcode.to_string(),
145 target: CallTarget::Internal { method: callee },
146 });
147 }
148 Some(target) => edges.push(CallEdge {
149 caller,
150 call_offset: instr.offset,
151 opcode: instr.opcode.to_string(),
152 target: CallTarget::UnresolvedInternal { target },
153 }),
154 None => edges.push(CallEdge {
155 caller,
156 call_offset: instr.offset,
157 opcode: instr.opcode.to_string(),
158 target: CallTarget::UnresolvedInternal { target: -1 },
159 }),
160 }
161 }
162 OpCode::CallT => {
163 let Some(Operand::U16(index)) = instr.operand else {
164 continue;
165 };
166 let token = nef.method_tokens.get(index as usize);
167 if let Some(token) = token {
168 edges.push(CallEdge {
169 caller: table.method_for_offset(instr.offset),
170 call_offset: instr.offset,
171 opcode: instr.opcode.to_string(),
172 target: CallTarget::MethodToken {
173 index,
174 hash_le: util::format_hash(&token.hash),
175 hash_be: util::format_hash_be(&token.hash),
176 method: token.method.clone(),
177 parameters_count: token.parameters_count,
178 has_return_value: token.has_return_value,
179 call_flags: token.call_flags,
180 },
181 });
182 } else {
183 edges.push(CallEdge {
184 caller: table.method_for_offset(instr.offset),
185 call_offset: instr.offset,
186 opcode: instr.opcode.to_string(),
187 target: CallTarget::Indirect {
188 opcode: instr.opcode.to_string(),
189 operand: Some(index),
190 },
191 });
192 }
193 }
194 OpCode::CallA => {
195 let caller = table.method_for_offset(instr.offset);
198 if let Some(target) = calla_target_from_pusha(instructions, index)
199 .filter(|target| instruction_offsets.contains(target))
200 {
201 let callee = table.resolve_internal_target(target);
202 methods.insert(callee.offset, callee.clone());
203 edges.push(CallEdge {
204 caller,
205 call_offset: instr.offset,
206 opcode: instr.opcode.to_string(),
207 target: CallTarget::Internal { method: callee },
208 });
209 } else {
210 edges.push(CallEdge {
211 caller,
212 call_offset: instr.offset,
213 opcode: instr.opcode.to_string(),
214 target: CallTarget::Indirect {
215 opcode: instr.opcode.to_string(),
216 operand: None,
217 },
218 });
219 }
220 }
221 _ => {}
222 }
223 }
224
225 resolve_ldarg_calla_targets(instructions, &mut edges, &table, &mut methods);
228
229 CallGraph {
230 methods: methods.into_values().collect(),
231 edges,
232 }
233}
234
235fn relative_target_isize(instr: &Instruction) -> Option<isize> {
236 let delta = match &instr.operand {
237 Some(Operand::Jump(v)) => *v as isize,
238 Some(Operand::Jump32(v)) => *v as isize,
239 _ => return None,
240 };
241 Some(instr.offset as isize + delta)
242}
243
244pub(super) fn calla_target_from_pusha(instructions: &[Instruction], index: usize) -> Option<usize> {
245 let mut cursor = index.checked_sub(1)?;
246 loop {
247 let prev = instructions.get(cursor)?;
248 if prev.opcode == OpCode::Nop {
249 cursor = cursor.checked_sub(1)?;
250 continue;
251 }
252 return trace_pointer_target_from_value_source(instructions, cursor);
253 }
254}
255
256fn pusha_absolute_target(instruction: &Instruction) -> Option<usize> {
257 let delta = match instruction.operand {
261 Some(Operand::I32(value)) => value as isize,
262 _ => return None,
263 };
264 instruction.offset.checked_add_signed(delta)
265}
266
267#[derive(Clone, Copy, Debug, PartialEq, Eq)]
268enum SlotDomain {
269 Local(u8),
270 Static(u8),
271}
272
273fn local_load_index(instruction: &Instruction) -> Option<u8> {
274 match instruction.opcode {
275 OpCode::Ldloc0 => Some(0),
276 OpCode::Ldloc1 => Some(1),
277 OpCode::Ldloc2 => Some(2),
278 OpCode::Ldloc3 => Some(3),
279 OpCode::Ldloc4 => Some(4),
280 OpCode::Ldloc5 => Some(5),
281 OpCode::Ldloc6 => Some(6),
282 OpCode::Ldloc => match instruction.operand {
283 Some(Operand::U8(index)) => Some(index),
284 _ => None,
285 },
286 _ => None,
287 }
288}
289
290fn static_load_index(instruction: &Instruction) -> Option<u8> {
291 match instruction.opcode {
292 OpCode::Ldsfld0 => Some(0),
293 OpCode::Ldsfld1 => Some(1),
294 OpCode::Ldsfld2 => Some(2),
295 OpCode::Ldsfld3 => Some(3),
296 OpCode::Ldsfld4 => Some(4),
297 OpCode::Ldsfld5 => Some(5),
298 OpCode::Ldsfld6 => Some(6),
299 OpCode::Ldsfld => match instruction.operand {
300 Some(Operand::U8(index)) => Some(index),
301 _ => None,
302 },
303 _ => None,
304 }
305}
306
307fn arg_load_index(instruction: &Instruction) -> Option<u8> {
308 match instruction.opcode {
309 OpCode::Ldarg0 => Some(0),
310 OpCode::Ldarg1 => Some(1),
311 OpCode::Ldarg2 => Some(2),
312 OpCode::Ldarg3 => Some(3),
313 OpCode::Ldarg4 => Some(4),
314 OpCode::Ldarg5 => Some(5),
315 OpCode::Ldarg6 => Some(6),
316 OpCode::Ldarg => match instruction.operand {
317 Some(Operand::U8(index)) => Some(index),
318 _ => None,
319 },
320 _ => None,
321 }
322}
323
324fn slot_store_domain(instruction: &Instruction) -> Option<SlotDomain> {
325 match instruction.opcode {
326 OpCode::Stloc0 => Some(SlotDomain::Local(0)),
327 OpCode::Stloc1 => Some(SlotDomain::Local(1)),
328 OpCode::Stloc2 => Some(SlotDomain::Local(2)),
329 OpCode::Stloc3 => Some(SlotDomain::Local(3)),
330 OpCode::Stloc4 => Some(SlotDomain::Local(4)),
331 OpCode::Stloc5 => Some(SlotDomain::Local(5)),
332 OpCode::Stloc6 => Some(SlotDomain::Local(6)),
333 OpCode::Stloc => match instruction.operand {
334 Some(Operand::U8(index)) => Some(SlotDomain::Local(index)),
335 _ => None,
336 },
337 OpCode::Stsfld0 => Some(SlotDomain::Static(0)),
338 OpCode::Stsfld1 => Some(SlotDomain::Static(1)),
339 OpCode::Stsfld2 => Some(SlotDomain::Static(2)),
340 OpCode::Stsfld3 => Some(SlotDomain::Static(3)),
341 OpCode::Stsfld4 => Some(SlotDomain::Static(4)),
342 OpCode::Stsfld5 => Some(SlotDomain::Static(5)),
343 OpCode::Stsfld6 => Some(SlotDomain::Static(6)),
344 OpCode::Stsfld => match instruction.operand {
345 Some(Operand::U8(index)) => Some(SlotDomain::Static(index)),
346 _ => None,
347 },
348 _ => None,
349 }
350}
351
352fn resolve_slot_pointer_target(
353 instructions: &[Instruction],
354 before_index: usize,
355 domain: SlotDomain,
356) -> Option<usize> {
357 let store_index = find_slot_store_before(instructions, before_index, domain)?;
358 let source_index = previous_non_nop_index(instructions, store_index.checked_sub(1)?)?;
359 trace_pointer_target_from_value_source(instructions, source_index)
360}
361
362fn trace_pointer_target_from_value_source(
363 instructions: &[Instruction],
364 mut source_index: usize,
365) -> Option<usize> {
366 loop {
367 let instruction = instructions.get(source_index)?;
368 if instruction.opcode == OpCode::Dup {
369 source_index = previous_non_nop_index(instructions, source_index.checked_sub(1)?)?;
370 continue;
371 }
372 if instruction.opcode == OpCode::PushA {
373 return pusha_absolute_target(instruction);
374 }
375 if instruction.opcode == OpCode::Pickitem {
376 return resolve_pickitem_pointer_target(instructions, source_index);
377 }
378
379 let domain = if let Some(slot) = local_load_index(instruction) {
380 SlotDomain::Local(slot)
381 } else {
382 SlotDomain::Static(static_load_index(instruction)?)
383 };
384
385 let store_index = find_slot_store_before(instructions, source_index, domain)?;
386 source_index = previous_non_nop_index(instructions, store_index.checked_sub(1)?)?;
387 }
388}
389
390pub(super) fn calla_ldarg_index(instructions: &[Instruction], calla_index: usize) -> Option<u8> {
397 let producer_index = previous_non_nop_index(instructions, calla_index.checked_sub(1)?)?;
398 trace_argument_index_from_value_source(instructions, producer_index)
399}
400
401fn trace_argument_index_from_value_source(
402 instructions: &[Instruction],
403 mut source_index: usize,
404) -> Option<u8> {
405 loop {
406 let instruction = instructions.get(source_index)?;
407 if instruction.opcode == OpCode::Dup {
408 source_index = previous_non_nop_index(instructions, source_index.checked_sub(1)?)?;
409 continue;
410 }
411 if let Some(arg_index) = arg_load_index(instruction) {
412 return Some(arg_index);
413 }
414
415 let domain = if let Some(slot) = local_load_index(instruction) {
416 SlotDomain::Local(slot)
417 } else {
418 SlotDomain::Static(static_load_index(instruction)?)
419 };
420
421 let store_index = find_slot_store_before(instructions, source_index, domain)?;
422 source_index = previous_non_nop_index(instructions, store_index.checked_sub(1)?)?;
423 }
424}
425
426fn resolve_pickitem_pointer_target(
427 instructions: &[Instruction],
428 pickitem_index: usize,
429) -> Option<usize> {
430 let index_source = previous_non_nop_index(instructions, pickitem_index.checked_sub(1)?)?;
431 let array_source_index = previous_non_nop_index(instructions, index_source.checked_sub(1)?)?;
432 let domain = trace_container_domain_from_value_source(instructions, array_source_index)?;
433
434 let scan_start = match domain {
435 SlotDomain::Local(_) => find_resolution_start_index(instructions, pickitem_index),
436 SlotDomain::Static(_) => 0,
437 };
438
439 let mut resolved_target = None;
440 for (index, instruction) in instructions
441 .iter()
442 .enumerate()
443 .take(pickitem_index)
444 .skip(scan_start)
445 {
446 if instruction.opcode != OpCode::Append {
447 continue;
448 }
449 let item_index = trace_stack_value_producer_before(instructions, index, 0)?;
450 let array_index = trace_stack_value_producer_before(instructions, index, 1)?;
451 let Some(array_domain) =
452 trace_container_domain_from_value_source(instructions, array_index)
453 else {
454 continue;
455 };
456 if array_domain != domain {
457 continue;
458 }
459 let target = trace_pointer_target_from_value_source(instructions, item_index)?;
460 if let Some(existing) = resolved_target {
461 if existing != target {
462 return None;
463 }
464 } else {
465 resolved_target = Some(target);
466 }
467 }
468 resolved_target
469}
470
471fn trace_container_domain_from_value_source(
472 instructions: &[Instruction],
473 mut source_index: usize,
474) -> Option<SlotDomain> {
475 loop {
476 let instruction = instructions.get(source_index)?;
477 if instruction.opcode == OpCode::Dup {
478 source_index = previous_non_nop_index(instructions, source_index.checked_sub(1)?)?;
479 continue;
480 }
481 if let Some(slot) = local_load_index(instruction) {
482 let domain = SlotDomain::Local(slot);
483 let Some(store_index) = find_slot_store_before(instructions, source_index, domain)
484 else {
485 return Some(domain);
486 };
487 let source = previous_non_nop_index(instructions, store_index.checked_sub(1)?)?;
488 let source_instruction = instructions.get(source)?;
489 if source_instruction.opcode == OpCode::Dup {
490 source_index = previous_non_nop_index(instructions, source.checked_sub(1)?)?;
491 continue;
492 }
493 if local_load_index(source_instruction).is_some()
494 || static_load_index(source_instruction).is_some()
495 {
496 source_index = source;
497 continue;
498 }
499 return Some(domain);
500 }
501 if let Some(slot) = static_load_index(instruction) {
502 let domain = SlotDomain::Static(slot);
503 let Some(store_index) = find_slot_store_before(instructions, source_index, domain)
504 else {
505 return Some(domain);
506 };
507 let source = previous_non_nop_index(instructions, store_index.checked_sub(1)?)?;
508 let source_instruction = instructions.get(source)?;
509 if source_instruction.opcode == OpCode::Dup {
510 source_index = previous_non_nop_index(instructions, source.checked_sub(1)?)?;
511 continue;
512 }
513 if local_load_index(source_instruction).is_some()
514 || static_load_index(source_instruction).is_some()
515 {
516 source_index = source;
517 continue;
518 }
519 return Some(domain);
520 }
521 return None;
522 }
523}
524
525fn trace_stack_value_producer_before(
526 instructions: &[Instruction],
527 before_index: usize,
528 mut depth: usize,
529) -> Option<usize> {
530 for index in (0..before_index).rev() {
531 let instruction = instructions.get(index)?;
532 let (pops, pushes) = stack_effect(instruction)?;
533 if depth < pushes {
534 return Some(index);
535 }
536 depth = depth.checked_add(pops)?.checked_sub(pushes)?;
537 }
538 None
539}
540
541fn stack_effect(instruction: &Instruction) -> Option<(usize, usize)> {
542 use OpCode::*;
543 let opcode = instruction.opcode;
544 match opcode {
545 Nop => Some((0, 0)),
546 PushA | PushNull | PushT | PushF | PushM1 | Push0 | Push1 | Push2 | Push3 | Push4
547 | Push5 | Push6 | Push7 | Push8 | Push9 | Push10 | Push11 | Push12 | Push13 | Push14
548 | Push15 | Push16 | Pushint8 | Pushint16 | Pushint32 | Pushint64 | Pushint128
549 | Pushint256 | Pushdata1 | Pushdata2 | Pushdata4 | Newarray0 | Newmap | Newstruct0
550 | Ldloc0 | Ldloc1 | Ldloc2 | Ldloc3 | Ldloc4 | Ldloc5 | Ldloc6 | Ldloc | Ldarg0
551 | Ldarg1 | Ldarg2 | Ldarg3 | Ldarg4 | Ldarg5 | Ldarg6 | Ldarg | Ldsfld0 | Ldsfld1
552 | Ldsfld2 | Ldsfld3 | Ldsfld4 | Ldsfld5 | Ldsfld6 | Ldsfld => Some((0, 1)),
553 Stloc0 | Stloc1 | Stloc2 | Stloc3 | Stloc4 | Stloc5 | Stloc6 | Stloc | Starg0 | Starg1
554 | Starg2 | Starg3 | Starg4 | Starg5 | Starg6 | Starg | Stsfld0 | Stsfld1 | Stsfld2
555 | Stsfld3 | Stsfld4 | Stsfld5 | Stsfld6 | Stsfld => Some((1, 0)),
556 Append => Some((2, 0)),
557 Pickitem => Some((2, 1)),
558 Dup => Some((1, 2)),
559 _ => None,
560 }
561}
562
563fn find_resolution_start_index(instructions: &[Instruction], before_index: usize) -> usize {
564 for index in (0..before_index).rev() {
565 if let Some(instruction) = instructions.get(index) {
566 if is_pointer_resolution_boundary(instruction.opcode) {
567 return index + 1;
568 }
569 }
570 }
571 0
572}
573
574fn find_slot_store_before(
575 instructions: &[Instruction],
576 before_index: usize,
577 domain: SlotDomain,
578) -> Option<usize> {
579 for index in (0..before_index).rev() {
580 let instruction = instructions.get(index)?;
581 if matches!(domain, SlotDomain::Local(_))
582 && is_pointer_resolution_boundary(instruction.opcode)
583 {
584 return None;
585 }
586 if slot_store_domain(instruction) == Some(domain) {
587 return Some(index);
588 }
589 }
590 None
591}
592
593fn is_pointer_resolution_boundary(opcode: OpCode) -> bool {
594 matches!(
595 opcode,
596 OpCode::Ret
597 | OpCode::Throw
598 | OpCode::Abort
599 | OpCode::Abortmsg
600 | OpCode::Initslot
601 | OpCode::Initsslot
602 )
603}
604
605fn previous_non_nop_index(instructions: &[Instruction], mut index: usize) -> Option<usize> {
606 loop {
607 let instruction = instructions.get(index)?;
608 if instruction.opcode != OpCode::Nop {
609 return Some(index);
610 }
611 index = index.checked_sub(1)?;
612 }
613}
614
615pub(super) fn initslot_arg_count_at(
617 instructions: &[Instruction],
618 method_offset: usize,
619) -> Option<usize> {
620 instructions
621 .iter()
622 .find(|i| i.offset == method_offset && i.opcode == OpCode::Initslot)
623 .and_then(|i| match &i.operand {
624 Some(Operand::Bytes(bytes)) if bytes.len() >= 2 => Some(bytes[1] as usize),
625 _ => None,
626 })
627}
628
629#[derive(Clone, Copy, Debug, PartialEq, Eq)]
630pub(super) enum CallArgSource {
631 Target(usize),
632 PassThrough(u8),
633}
634
635pub(super) fn trace_call_arg_source(
642 instructions: &[Instruction],
643 call_index: usize,
644 arg_index: u8,
645 callee_arg_count: usize,
646) -> Option<CallArgSource> {
647 if (arg_index as usize) >= callee_arg_count {
648 return None;
649 }
650 let call_instruction = instructions.get(call_index)?;
651 let skip_count = if call_instruction.opcode == OpCode::CallA {
652 arg_index as usize + 1
653 } else {
654 arg_index as usize
655 };
656
657 let mut cursor = call_index.checked_sub(1)?;
658 let mut remaining = skip_count;
659
660 loop {
661 let instr = instructions.get(cursor)?;
662 if instr.opcode == OpCode::Nop {
663 cursor = cursor.checked_sub(1)?;
664 continue;
665 }
666
667 if remaining == 0 {
668 if instr.opcode == OpCode::PushA {
669 return pusha_absolute_target(instr).map(CallArgSource::Target);
670 }
671 if let Some(slot) = local_load_index(instr) {
672 return resolve_slot_pointer_target(instructions, cursor, SlotDomain::Local(slot))
673 .map(CallArgSource::Target)
674 .or_else(|| {
675 trace_argument_index_from_value_source(instructions, cursor)
676 .map(CallArgSource::PassThrough)
677 });
678 }
679 if let Some(slot) = static_load_index(instr) {
680 return resolve_slot_pointer_target(instructions, cursor, SlotDomain::Static(slot))
681 .map(CallArgSource::Target)
682 .or_else(|| {
683 trace_argument_index_from_value_source(instructions, cursor)
684 .map(CallArgSource::PassThrough)
685 });
686 }
687 return arg_load_index(instr).map(CallArgSource::PassThrough);
688 }
689
690 remaining -= 1;
691 cursor = cursor.checked_sub(1)?;
692 }
693}
694
695fn resolve_ldarg_calla_targets(
698 instructions: &[Instruction],
699 edges: &mut [CallEdge],
700 table: &MethodTable,
701 methods: &mut BTreeMap<usize, MethodRef>,
702) {
703 let offset_to_index: BTreeMap<usize, usize> = instructions
705 .iter()
706 .enumerate()
707 .map(|(i, instr)| (instr.offset, i))
708 .collect();
709
710 let mut sites: Vec<(usize, u8, usize)> = Vec::new(); for (edge_idx, edge) in edges.iter().enumerate() {
717 if edge.opcode != "CALLA" || !matches!(edge.target, CallTarget::Indirect { .. }) {
718 continue;
719 }
720 let Some(&calla_idx) = offset_to_index.get(&edge.call_offset) else {
721 continue;
722 };
723 if let Some(arg_idx) = calla_ldarg_index(instructions, calla_idx) {
724 let actual_method_offset = methods
727 .range(..=edge.call_offset)
728 .next_back()
729 .map(|(&offset, _)| offset)
730 .unwrap_or(edge.caller.offset);
731 sites.push((edge_idx, arg_idx, actual_method_offset));
732 }
733 }
734
735 if sites.is_empty() {
736 return;
737 }
738
739 loop {
740 let mut callers_by_target: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
741 for edge in edges.iter() {
742 if let CallTarget::Internal { method } = &edge.target {
743 if edge.opcode == "CALL" || edge.opcode == "CALL_L" || edge.opcode == "CALLA" {
744 callers_by_target
745 .entry(method.offset)
746 .or_default()
747 .push(edge.call_offset);
748 }
749 }
750 }
751
752 let mut progress = false;
753 for (edge_idx, arg_idx, method_offset) in &sites {
754 if !matches!(edges[*edge_idx].target, CallTarget::Indirect { .. }) {
755 continue;
756 }
757
758 let mut visited = BTreeSet::new();
759 let resolved = resolve_argument_target_recursive(
760 instructions,
761 &offset_to_index,
762 &callers_by_target,
763 methods,
764 *method_offset,
765 *arg_idx,
766 &mut visited,
767 );
768
769 if let Some(target) = resolved.filter(|target| offset_to_index.contains_key(target)) {
770 let callee = table.resolve_internal_target(target);
771 methods.insert(callee.offset, callee.clone());
772 edges[*edge_idx].target = CallTarget::Internal { method: callee };
773 progress = true;
774 }
775 }
776
777 if !progress {
778 break;
779 }
780 }
781}
782
783fn resolve_argument_target_recursive(
784 instructions: &[Instruction],
785 offset_to_index: &BTreeMap<usize, usize>,
786 callers_by_target: &BTreeMap<usize, Vec<usize>>,
787 methods: &BTreeMap<usize, MethodRef>,
788 method_offset: usize,
789 arg_index: u8,
790 visited: &mut BTreeSet<(usize, u8)>,
791) -> Option<usize> {
792 if !visited.insert((method_offset, arg_index)) {
793 return None;
794 }
795
796 let call_sites = callers_by_target.get(&method_offset)?;
797 let callee_arg_count =
798 initslot_arg_count_at(instructions, method_offset).unwrap_or(arg_index as usize + 1);
799
800 for &call_offset in call_sites {
801 let &call_idx = offset_to_index.get(&call_offset)?;
802 match trace_call_arg_source(instructions, call_idx, arg_index, callee_arg_count) {
803 Some(CallArgSource::Target(target)) => return Some(target),
804 Some(CallArgSource::PassThrough(next_arg)) => {
805 let caller_method_offset = methods
806 .range(..=call_offset)
807 .next_back()
808 .map(|(&offset, _)| offset)
809 .unwrap_or(call_offset);
810 if let Some(target) = resolve_argument_target_recursive(
811 instructions,
812 offset_to_index,
813 callers_by_target,
814 methods,
815 caller_method_offset,
816 next_arg,
817 visited,
818 ) {
819 return Some(target);
820 }
821 }
822 None => {}
823 }
824 }
825
826 None
827}