Skip to main content

neo_decompiler/decompiler/analysis/
xrefs.rs

1//! Cross-reference analysis for local/argument/static slots.
2//!
3//! This module records where stack slot values are read and written, keyed by
4//! bytecode offset. The result is primarily intended for diagnostics and future
5//! data-flow passes.
6
7use serde::Serialize;
8
9use crate::instruction::{Instruction, OpCode, Operand};
10use crate::manifest::ContractManifest;
11
12use super::{MethodRef, MethodTable};
13
14/// Slot kinds addressable by `LD*`/`ST*` opcodes.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
16#[non_exhaustive]
17pub enum SlotKind {
18    /// Local slot (`LDLOC*` / `STLOC*`).
19    #[serde(rename = "local")]
20    Local,
21    /// Argument slot (`LDARG*` / `STARG*`).
22    #[serde(rename = "argument")]
23    Argument,
24    /// Static slot (`LDSFLD*` / `STSFLD*`).
25    #[serde(rename = "static")]
26    Static,
27}
28
29/// Read/write offsets for one slot index.
30#[derive(Debug, Clone, Default, Serialize)]
31pub struct SlotXref {
32    /// Slot index.
33    pub index: usize,
34    /// Offsets where the slot value is read.
35    pub reads: Vec<usize>,
36    /// Offsets where the slot value is written.
37    pub writes: Vec<usize>,
38}
39
40/// Cross-references for a single method range.
41#[derive(Debug, Clone, Serialize)]
42pub struct MethodXrefs {
43    /// Method containing the reads/writes.
44    pub method: MethodRef,
45    /// Local-slot references.
46    pub locals: Vec<SlotXref>,
47    /// Argument-slot references.
48    pub arguments: Vec<SlotXref>,
49    /// Static-slot references.
50    pub statics: Vec<SlotXref>,
51}
52
53/// Aggregated cross-reference information across all discovered methods.
54#[derive(Debug, Clone, Serialize)]
55pub struct Xrefs {
56    /// Per-method slot cross-references.
57    pub methods: Vec<MethodXrefs>,
58}
59
60/// Build cross-reference information for locals/arguments/statics.
61#[must_use]
62pub fn build_xrefs(instructions: &[Instruction], manifest: Option<&ContractManifest>) -> Xrefs {
63    let table = MethodTable::new(instructions, manifest);
64    let static_count = scan_static_slot_count(instructions).unwrap_or(0);
65
66    let mut methods = Vec::new();
67    for span in table.spans() {
68        let slice: Vec<&Instruction> = instructions
69            .iter()
70            .filter(|ins| ins.offset >= span.start && ins.offset < span.end)
71            .collect();
72        let (locals_count, args_count) = scan_slot_counts(&slice).unwrap_or((0, 0));
73
74        let mut method_xrefs = MethodXrefs {
75            method: span.method.clone(),
76            locals: (0..locals_count)
77                .map(|index| SlotXref {
78                    index,
79                    ..SlotXref::default()
80                })
81                .collect(),
82            arguments: (0..args_count)
83                .map(|index| SlotXref {
84                    index,
85                    ..SlotXref::default()
86                })
87                .collect(),
88            statics: (0..static_count)
89                .map(|index| SlotXref {
90                    index,
91                    ..SlotXref::default()
92                })
93                .collect(),
94        };
95
96        for instr in &slice {
97            if let Some((kind, index, is_write)) = slot_access(instr) {
98                let target = match kind {
99                    SlotKind::Local => &mut method_xrefs.locals,
100                    SlotKind::Argument => &mut method_xrefs.arguments,
101                    SlotKind::Static => &mut method_xrefs.statics,
102                };
103                if index >= target.len() {
104                    let start = target.len();
105                    target.extend((start..=index).map(|idx| SlotXref {
106                        index: idx,
107                        ..SlotXref::default()
108                    }));
109                }
110                let entry = &mut target[index];
111                entry.index = index;
112                if is_write {
113                    entry.writes.push(instr.offset);
114                } else {
115                    entry.reads.push(instr.offset);
116                }
117            }
118        }
119
120        methods.push(method_xrefs);
121    }
122
123    Xrefs { methods }
124}
125
126fn slot_access(instr: &Instruction) -> Option<(SlotKind, usize, bool)> {
127    use OpCode::*;
128
129    match instr.opcode {
130        // locals
131        Ldloc0 => Some((SlotKind::Local, 0, false)),
132        Ldloc1 => Some((SlotKind::Local, 1, false)),
133        Ldloc2 => Some((SlotKind::Local, 2, false)),
134        Ldloc3 => Some((SlotKind::Local, 3, false)),
135        Ldloc4 => Some((SlotKind::Local, 4, false)),
136        Ldloc5 => Some((SlotKind::Local, 5, false)),
137        Ldloc6 => Some((SlotKind::Local, 6, false)),
138        Ldloc => slot_from_operand(SlotKind::Local, instr.operand.as_ref(), false),
139        Stloc0 => Some((SlotKind::Local, 0, true)),
140        Stloc1 => Some((SlotKind::Local, 1, true)),
141        Stloc2 => Some((SlotKind::Local, 2, true)),
142        Stloc3 => Some((SlotKind::Local, 3, true)),
143        Stloc4 => Some((SlotKind::Local, 4, true)),
144        Stloc5 => Some((SlotKind::Local, 5, true)),
145        Stloc6 => Some((SlotKind::Local, 6, true)),
146        Stloc => slot_from_operand(SlotKind::Local, instr.operand.as_ref(), true),
147
148        // arguments
149        Ldarg0 => Some((SlotKind::Argument, 0, false)),
150        Ldarg1 => Some((SlotKind::Argument, 1, false)),
151        Ldarg2 => Some((SlotKind::Argument, 2, false)),
152        Ldarg3 => Some((SlotKind::Argument, 3, false)),
153        Ldarg4 => Some((SlotKind::Argument, 4, false)),
154        Ldarg5 => Some((SlotKind::Argument, 5, false)),
155        Ldarg6 => Some((SlotKind::Argument, 6, false)),
156        Ldarg => slot_from_operand(SlotKind::Argument, instr.operand.as_ref(), false),
157        Starg0 => Some((SlotKind::Argument, 0, true)),
158        Starg1 => Some((SlotKind::Argument, 1, true)),
159        Starg2 => Some((SlotKind::Argument, 2, true)),
160        Starg3 => Some((SlotKind::Argument, 3, true)),
161        Starg4 => Some((SlotKind::Argument, 4, true)),
162        Starg5 => Some((SlotKind::Argument, 5, true)),
163        Starg6 => Some((SlotKind::Argument, 6, true)),
164        Starg => slot_from_operand(SlotKind::Argument, instr.operand.as_ref(), true),
165
166        // statics
167        Ldsfld0 => Some((SlotKind::Static, 0, false)),
168        Ldsfld1 => Some((SlotKind::Static, 1, false)),
169        Ldsfld2 => Some((SlotKind::Static, 2, false)),
170        Ldsfld3 => Some((SlotKind::Static, 3, false)),
171        Ldsfld4 => Some((SlotKind::Static, 4, false)),
172        Ldsfld5 => Some((SlotKind::Static, 5, false)),
173        Ldsfld6 => Some((SlotKind::Static, 6, false)),
174        Ldsfld => slot_from_operand(SlotKind::Static, instr.operand.as_ref(), false),
175        Stsfld0 => Some((SlotKind::Static, 0, true)),
176        Stsfld1 => Some((SlotKind::Static, 1, true)),
177        Stsfld2 => Some((SlotKind::Static, 2, true)),
178        Stsfld3 => Some((SlotKind::Static, 3, true)),
179        Stsfld4 => Some((SlotKind::Static, 4, true)),
180        Stsfld5 => Some((SlotKind::Static, 5, true)),
181        Stsfld6 => Some((SlotKind::Static, 6, true)),
182        Stsfld => slot_from_operand(SlotKind::Static, instr.operand.as_ref(), true),
183
184        _ => None,
185    }
186}
187
188fn slot_from_operand(
189    kind: SlotKind,
190    operand: Option<&Operand>,
191    is_write: bool,
192) -> Option<(SlotKind, usize, bool)> {
193    let Some(Operand::U8(index)) = operand else {
194        return None;
195    };
196    Some((kind, *index as usize, is_write))
197}
198
199fn scan_slot_counts(instructions: &[&Instruction]) -> Option<(usize, usize)> {
200    for instr in instructions {
201        if instr.opcode != OpCode::Initslot {
202            continue;
203        }
204        if let Some(Operand::Bytes(bytes)) = &instr.operand {
205            if bytes.len() == 2 {
206                return Some((bytes[0] as usize, bytes[1] as usize));
207            }
208        }
209    }
210    None
211}
212
213fn scan_static_slot_count(instructions: &[Instruction]) -> Option<usize> {
214    for instr in instructions {
215        if instr.opcode != OpCode::Initsslot {
216            continue;
217        }
218        if let Some(Operand::U8(count)) = &instr.operand {
219            return Some(*count as usize);
220        }
221    }
222    None
223}