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