Skip to main content

neo_decompiler/decompiler/analysis/
types.rs

1//! Lightweight type inference for lifted Neo N3 bytecode.
2//!
3//! The Neo VM is dynamically typed and most syscalls do not encode argument
4//! signatures in the bytecode. The goal of this module is therefore to provide
5//! a best-effort type recovery pass that is:
6//!
7//! - conservative (falls back to `unknown`/`any` rather than guessing)
8//! - useful for collection recovery and readability improvements
9//! - deterministic and panic-free on malformed input
10
11// Stack depth → i64 and type-tag byte reinterpretation casts are structurally
12// safe: stack depth fits in i64, and the i8→u8 cast is intentional.
13#![allow(
14    clippy::cast_possible_truncation,
15    clippy::cast_possible_wrap,
16    clippy::cast_sign_loss
17)]
18
19use serde::Serialize;
20
21use crate::instruction::{Instruction, OpCode, Operand};
22use crate::manifest::ContractManifest;
23
24use super::{MethodRef, MethodTable};
25
26/// Primitive/value types inferred from the instruction stream.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
28#[non_exhaustive]
29pub enum ValueType {
30    /// Unknown or not yet inferred.
31    #[serde(rename = "unknown")]
32    Unknown,
33    /// Dynamic `any` value.
34    #[serde(rename = "any")]
35    Any,
36    /// Null literal.
37    #[serde(rename = "null")]
38    Null,
39    /// Boolean.
40    #[serde(rename = "bool")]
41    Boolean,
42    /// Integer.
43    #[serde(rename = "integer")]
44    Integer,
45    /// ByteString.
46    #[serde(rename = "bytestring")]
47    ByteString,
48    /// Buffer.
49    #[serde(rename = "buffer")]
50    Buffer,
51    /// Array.
52    #[serde(rename = "array")]
53    Array,
54    /// Struct.
55    #[serde(rename = "struct")]
56    Struct,
57    /// Map.
58    #[serde(rename = "map")]
59    Map,
60    /// Interop interface.
61    #[serde(rename = "interopinterface")]
62    InteropInterface,
63    /// Pointer.
64    #[serde(rename = "pointer")]
65    Pointer,
66}
67
68impl ValueType {
69    fn join(self, other: Self) -> Self {
70        use ValueType::*;
71        if self == other {
72            return self;
73        }
74        match (self, other) {
75            (Unknown, x) | (x, Unknown) => x,
76            (Null, _) | (_, Null) => Any,
77            _ => Any,
78        }
79    }
80}
81
82#[derive(Debug, Clone, Copy)]
83struct StackValue {
84    ty: ValueType,
85    int_literal: Option<i64>,
86}
87
88impl StackValue {
89    fn unknown() -> Self {
90        Self {
91            ty: ValueType::Unknown,
92            int_literal: None,
93        }
94    }
95
96    fn with_type(ty: ValueType) -> Self {
97        Self {
98            ty,
99            int_literal: None,
100        }
101    }
102
103    fn integer_literal(value: i64) -> Self {
104        Self {
105            ty: ValueType::Integer,
106            int_literal: Some(value),
107        }
108    }
109}
110
111/// Per-method inferred types.
112#[derive(Debug, Clone, Serialize)]
113pub struct MethodTypes {
114    /// Method whose slots were analyzed.
115    pub method: MethodRef,
116    /// Inferred argument types indexed by argument slot.
117    pub arguments: Vec<ValueType>,
118    /// Inferred local types indexed by local slot.
119    pub locals: Vec<ValueType>,
120}
121
122/// Aggregated type inference results.
123#[derive(Debug, Clone, Default, Serialize)]
124pub struct TypeInfo {
125    /// Per-method inferred locals/arguments.
126    pub methods: Vec<MethodTypes>,
127    /// Inferred static slot types indexed by static slot.
128    pub statics: Vec<ValueType>,
129}
130
131/// Infer primitive types and collection kinds from the instruction stream.
132#[must_use]
133pub fn infer_types(instructions: &[Instruction], manifest: Option<&ContractManifest>) -> TypeInfo {
134    let table = MethodTable::new(instructions, manifest);
135    let static_count = scan_static_slot_count(instructions).unwrap_or(0);
136    let mut statics = vec![ValueType::Unknown; static_count];
137
138    let mut methods = Vec::new();
139    // `instructions` is sorted by offset and `table.spans()` is sorted by start
140    // with contiguous ranges, so sweep a single forward cursor instead of
141    // re-filtering the whole instruction stream per span (O(instructions *
142    // spans), quadratic on call-dense bytecode). See build_xrefs.
143    let mut cursor = 0usize;
144    for span in table.spans() {
145        while cursor < instructions.len() && instructions[cursor].offset < span.start {
146            cursor += 1;
147        }
148        let begin = cursor;
149        while cursor < instructions.len() && instructions[cursor].offset < span.end {
150            cursor += 1;
151        }
152        let slice: Vec<&Instruction> = instructions[begin..cursor].iter().collect();
153
154        let (locals_count, args_count) = scan_slot_counts(&slice).unwrap_or((0, 0));
155        let mut locals = vec![ValueType::Unknown; locals_count];
156        let mut arguments = vec![ValueType::Unknown; args_count];
157
158        if let Some(manifest) = manifest {
159            if let Some(index) = table.manifest_index_for_start(span.start) {
160                if let Some(method) = manifest.abi.methods.get(index) {
161                    if arguments.len() < method.parameters.len() {
162                        arguments.resize(method.parameters.len(), ValueType::Unknown);
163                    }
164                    for (idx, param) in method.parameters.iter().enumerate() {
165                        arguments[idx] = arguments[idx].join(type_from_manifest(&param.kind));
166                    }
167                }
168            }
169        }
170
171        infer_types_in_slice(&slice, &mut locals, &mut arguments, &mut statics);
172
173        methods.push(MethodTypes {
174            method: span.method.clone(),
175            arguments,
176            locals,
177        });
178    }
179
180    TypeInfo { methods, statics }
181}
182
183fn infer_types_in_slice(
184    instructions: &[&Instruction],
185    locals: &mut Vec<ValueType>,
186    arguments: &mut Vec<ValueType>,
187    statics: &mut Vec<ValueType>,
188) {
189    let mut stack: Vec<StackValue> = Vec::new();
190
191    for instr in instructions {
192        match instr.opcode {
193            OpCode::Initsslot => {
194                if let Some(Operand::U8(count)) = &instr.operand {
195                    let need = *count as usize;
196                    if statics.len() < need {
197                        statics.resize(need, ValueType::Unknown);
198                    }
199                }
200            }
201            OpCode::Initslot => {
202                // Operand is 2 bytes: locals, args.
203                if let Some(Operand::Bytes(bytes)) = &instr.operand {
204                    if bytes.len() == 2 {
205                        let locals_count = bytes[0] as usize;
206                        let args_count = bytes[1] as usize;
207                        if locals.len() < locals_count {
208                            locals.resize(locals_count, ValueType::Unknown);
209                        }
210                        if arguments.len() < args_count {
211                            arguments.resize(args_count, ValueType::Unknown);
212                        }
213                    }
214                }
215            }
216
217            // Literals
218            OpCode::PushNull => stack.push(StackValue::with_type(ValueType::Null)),
219            OpCode::PushT | OpCode::PushF => stack.push(StackValue::with_type(ValueType::Boolean)),
220            OpCode::Pushdata1 | OpCode::Pushdata2 | OpCode::Pushdata4 => {
221                stack.push(StackValue::with_type(ValueType::ByteString));
222            }
223            OpCode::Pushint8
224            | OpCode::Pushint16
225            | OpCode::Pushint32
226            | OpCode::Pushint64
227            | OpCode::Pushint128
228            | OpCode::Pushint256
229            | OpCode::PushM1
230            | OpCode::Push0
231            | OpCode::Push1
232            | OpCode::Push2
233            | OpCode::Push3
234            | OpCode::Push4
235            | OpCode::Push5
236            | OpCode::Push6
237            | OpCode::Push7
238            | OpCode::Push8
239            | OpCode::Push9
240            | OpCode::Push10
241            | OpCode::Push11
242            | OpCode::Push12
243            | OpCode::Push13
244            | OpCode::Push14
245            | OpCode::Push15
246            | OpCode::Push16 => {
247                if let Some(lit) = int_literal_from_operand(instr.operand.as_ref()) {
248                    stack.push(StackValue::integer_literal(lit));
249                } else {
250                    stack.push(StackValue::with_type(ValueType::Integer));
251                }
252            }
253            OpCode::PushA => stack.push(StackValue::with_type(ValueType::Pointer)),
254
255            // Stack manipulation
256            OpCode::Clear => stack.clear(),
257            OpCode::Depth => stack.push(StackValue::integer_literal(stack.len() as i64)),
258            OpCode::Drop => {
259                let _ = pop_or_unknown(&mut stack);
260            }
261            OpCode::Dup => {
262                let value = stack.last().copied().unwrap_or_else(StackValue::unknown);
263                stack.push(value);
264            }
265            OpCode::Swap if stack.len() >= 2 => {
266                let len = stack.len();
267                stack.swap(len - 1, len - 2);
268            }
269            OpCode::Over => {
270                let value = stack
271                    .get(stack.len().saturating_sub(2))
272                    .copied()
273                    .unwrap_or_else(StackValue::unknown);
274                stack.push(value);
275            }
276            OpCode::Nip if stack.len() >= 2 => {
277                let len = stack.len();
278                stack.remove(len - 2);
279            }
280            OpCode::Rot => {
281                if let (Some(top), Some(mid), Some(bottom)) =
282                    (stack.pop(), stack.pop(), stack.pop())
283                {
284                    stack.push(mid);
285                    stack.push(top);
286                    stack.push(bottom);
287                }
288            }
289            OpCode::Tuck => {
290                if let (Some(top), Some(second)) = (stack.pop(), stack.pop()) {
291                    stack.push(top);
292                    stack.push(second);
293                    stack.push(top);
294                }
295            }
296            OpCode::Pick => {
297                let index = pop_or_unknown(&mut stack);
298                if let Some(depth) = index.int_literal.and_then(|v| usize::try_from(v).ok()) {
299                    let pos = stack.len().checked_sub(1 + depth);
300                    if let Some(pos) = pos {
301                        if let Some(value) = stack.get(pos).copied() {
302                            stack.push(value);
303                            continue;
304                        }
305                    }
306                }
307                stack.push(StackValue::unknown());
308            }
309            OpCode::Roll => {
310                let index = pop_or_unknown(&mut stack);
311                if let Some(depth) = index.int_literal.and_then(|v| usize::try_from(v).ok()) {
312                    if depth < stack.len() {
313                        let pos = stack.len() - 1 - depth;
314                        let value = stack.remove(pos);
315                        stack.push(value);
316                    }
317                }
318            }
319            OpCode::Xdrop => {
320                let index = pop_or_unknown(&mut stack);
321                if let Some(depth) = index.int_literal.and_then(|v| usize::try_from(v).ok()) {
322                    if depth < stack.len() {
323                        let pos = stack.len() - 1 - depth;
324                        stack.remove(pos);
325                        continue;
326                    }
327                }
328                let _ = pop_or_unknown(&mut stack);
329            }
330            OpCode::Reverse3 => reverse_top(&mut stack, 3),
331            OpCode::Reverse4 => reverse_top(&mut stack, 4),
332            OpCode::Reversen => {
333                let count = pop_or_unknown(&mut stack);
334                if let Some(depth) = count.int_literal.and_then(|v| usize::try_from(v).ok()) {
335                    reverse_top(&mut stack, depth);
336                }
337            }
338
339            // Slot ops
340            OpCode::Ldloc0 => push_fixed_slot(&mut stack, locals, 0),
341            OpCode::Ldloc1 => push_fixed_slot(&mut stack, locals, 1),
342            OpCode::Ldloc2 => push_fixed_slot(&mut stack, locals, 2),
343            OpCode::Ldloc3 => push_fixed_slot(&mut stack, locals, 3),
344            OpCode::Ldloc4 => push_fixed_slot(&mut stack, locals, 4),
345            OpCode::Ldloc5 => push_fixed_slot(&mut stack, locals, 5),
346            OpCode::Ldloc6 => push_fixed_slot(&mut stack, locals, 6),
347            OpCode::Ldloc => push_indexed_slot(&mut stack, locals, instr.operand.as_ref()),
348            OpCode::Stloc0 => store_slot(&mut stack, locals, 0),
349            OpCode::Stloc1 => store_slot(&mut stack, locals, 1),
350            OpCode::Stloc2 => store_slot(&mut stack, locals, 2),
351            OpCode::Stloc3 => store_slot(&mut stack, locals, 3),
352            OpCode::Stloc4 => store_slot(&mut stack, locals, 4),
353            OpCode::Stloc5 => store_slot(&mut stack, locals, 5),
354            OpCode::Stloc6 => store_slot(&mut stack, locals, 6),
355            OpCode::Stloc => store_indexed_slot(&mut stack, locals, instr.operand.as_ref()),
356
357            OpCode::Ldarg0 => push_fixed_slot(&mut stack, arguments, 0),
358            OpCode::Ldarg1 => push_fixed_slot(&mut stack, arguments, 1),
359            OpCode::Ldarg2 => push_fixed_slot(&mut stack, arguments, 2),
360            OpCode::Ldarg3 => push_fixed_slot(&mut stack, arguments, 3),
361            OpCode::Ldarg4 => push_fixed_slot(&mut stack, arguments, 4),
362            OpCode::Ldarg5 => push_fixed_slot(&mut stack, arguments, 5),
363            OpCode::Ldarg6 => push_fixed_slot(&mut stack, arguments, 6),
364            OpCode::Ldarg => push_indexed_slot(&mut stack, arguments, instr.operand.as_ref()),
365            OpCode::Starg0 => store_slot(&mut stack, arguments, 0),
366            OpCode::Starg1 => store_slot(&mut stack, arguments, 1),
367            OpCode::Starg2 => store_slot(&mut stack, arguments, 2),
368            OpCode::Starg3 => store_slot(&mut stack, arguments, 3),
369            OpCode::Starg4 => store_slot(&mut stack, arguments, 4),
370            OpCode::Starg5 => store_slot(&mut stack, arguments, 5),
371            OpCode::Starg6 => store_slot(&mut stack, arguments, 6),
372            OpCode::Starg => store_indexed_slot(&mut stack, arguments, instr.operand.as_ref()),
373
374            OpCode::Ldsfld0 => push_fixed_slot(&mut stack, statics, 0),
375            OpCode::Ldsfld1 => push_fixed_slot(&mut stack, statics, 1),
376            OpCode::Ldsfld2 => push_fixed_slot(&mut stack, statics, 2),
377            OpCode::Ldsfld3 => push_fixed_slot(&mut stack, statics, 3),
378            OpCode::Ldsfld4 => push_fixed_slot(&mut stack, statics, 4),
379            OpCode::Ldsfld5 => push_fixed_slot(&mut stack, statics, 5),
380            OpCode::Ldsfld6 => push_fixed_slot(&mut stack, statics, 6),
381            OpCode::Ldsfld => push_indexed_slot(&mut stack, statics, instr.operand.as_ref()),
382            OpCode::Stsfld0 => store_slot(&mut stack, statics, 0),
383            OpCode::Stsfld1 => store_slot(&mut stack, statics, 1),
384            OpCode::Stsfld2 => store_slot(&mut stack, statics, 2),
385            OpCode::Stsfld3 => store_slot(&mut stack, statics, 3),
386            OpCode::Stsfld4 => store_slot(&mut stack, statics, 4),
387            OpCode::Stsfld5 => store_slot(&mut stack, statics, 5),
388            OpCode::Stsfld6 => store_slot(&mut stack, statics, 6),
389            OpCode::Stsfld => store_indexed_slot(&mut stack, statics, instr.operand.as_ref()),
390
391            // Collections
392            OpCode::Newarray0 => stack.push(StackValue::with_type(ValueType::Array)),
393            OpCode::Newarray | OpCode::NewarrayT => {
394                let _ = pop_or_unknown(&mut stack); // count (NEWARRAY_T also carries an element-type operand)
395                stack.push(StackValue::with_type(ValueType::Array));
396            }
397            OpCode::Newmap => stack.push(StackValue::with_type(ValueType::Map)),
398            OpCode::Newstruct0 => stack.push(StackValue::with_type(ValueType::Struct)),
399            OpCode::Newstruct => {
400                let _ = pop_or_unknown(&mut stack); // count
401                stack.push(StackValue::with_type(ValueType::Struct));
402            }
403            OpCode::Newbuffer => {
404                let _ = pop_or_unknown(&mut stack); // length
405                stack.push(StackValue::with_type(ValueType::Buffer));
406            }
407            OpCode::Pack => {
408                let count = pop_or_unknown(&mut stack);
409                if let Some(count) = count.int_literal.and_then(|v| usize::try_from(v).ok()) {
410                    // Clamp to the actual stack depth: the count literal is
411                    // attacker-controlled (up to i64::MAX) and popping an
412                    // empty abstract stack only yields unknowns, so iterating
413                    // past the real depth is an unbounded busy-loop.
414                    for _ in 0..count.min(stack.len()) {
415                        let _ = pop_or_unknown(&mut stack);
416                    }
417                }
418                stack.push(StackValue::with_type(ValueType::Array));
419            }
420            OpCode::Packmap => {
421                let count = pop_or_unknown(&mut stack);
422                if let Some(count) = count.int_literal.and_then(|v| usize::try_from(v).ok()) {
423                    // PACKMAP pops a key/value pair per entry (`Pop: 2n+1`,
424                    // OpCode.cs); clamp like PACK to bound attacker-controlled
425                    // counts.
426                    for _ in 0..count.saturating_mul(2).min(stack.len()) {
427                        let _ = pop_or_unknown(&mut stack);
428                    }
429                }
430                stack.push(StackValue::with_type(ValueType::Map));
431            }
432            OpCode::Packstruct => {
433                let count = pop_or_unknown(&mut stack);
434                if let Some(count) = count.int_literal.and_then(|v| usize::try_from(v).ok()) {
435                    // Clamp like PACK to bound attacker-controlled counts.
436                    for _ in 0..count.min(stack.len()) {
437                        let _ = pop_or_unknown(&mut stack);
438                    }
439                }
440                stack.push(StackValue::with_type(ValueType::Struct));
441            }
442            OpCode::Unpack => {
443                let _ = pop_or_unknown(&mut stack);
444                stack.push(StackValue::unknown());
445            }
446            OpCode::Pickitem => {
447                let _ = pop_or_unknown(&mut stack);
448                let _ = pop_or_unknown(&mut stack);
449                stack.push(StackValue::unknown());
450            }
451            OpCode::Setitem => {
452                let _ = pop_or_unknown(&mut stack);
453                let _ = pop_or_unknown(&mut stack);
454                let _ = pop_or_unknown(&mut stack);
455            }
456            OpCode::Append => {
457                let _ = pop_or_unknown(&mut stack);
458                let _ = pop_or_unknown(&mut stack);
459            }
460            OpCode::Remove => {
461                let _ = pop_or_unknown(&mut stack);
462                let _ = pop_or_unknown(&mut stack);
463            }
464            OpCode::Clearitems => {
465                let _ = pop_or_unknown(&mut stack);
466            }
467            OpCode::Popitem => {
468                let _ = pop_or_unknown(&mut stack);
469                let _ = pop_or_unknown(&mut stack);
470                stack.push(StackValue::unknown());
471            }
472            OpCode::Size => {
473                let _ = pop_or_unknown(&mut stack);
474                stack.push(StackValue::with_type(ValueType::Integer));
475            }
476            OpCode::Haskey => {
477                let _ = pop_or_unknown(&mut stack);
478                let _ = pop_or_unknown(&mut stack);
479                stack.push(StackValue::with_type(ValueType::Boolean));
480            }
481            OpCode::Isnull => {
482                let _ = pop_or_unknown(&mut stack);
483                stack.push(StackValue::with_type(ValueType::Boolean));
484            }
485            OpCode::Istype => {
486                let _ = pop_or_unknown(&mut stack);
487                let _ = pop_or_unknown(&mut stack);
488                stack.push(StackValue::with_type(ValueType::Boolean));
489            }
490            OpCode::Convert => {
491                // CONVERT deterministically yields the target type regardless of
492                // the input type, so push the decoded target directly instead of
493                // joining with the consumed value's type (which over-widened to
494                // Any). Mirrors the JS port. Unknown operands fall back to Any.
495                let _ = pop_or_unknown(&mut stack);
496                let target = instr
497                    .operand
498                    .as_ref()
499                    .and_then(convert_target_type)
500                    .unwrap_or(ValueType::Any);
501                stack.push(StackValue::with_type(target));
502            }
503
504            // Arithmetic + comparisons (subset)
505            OpCode::Add
506            | OpCode::Sub
507            | OpCode::Mul
508            | OpCode::Div
509            | OpCode::Mod
510            | OpCode::Pow
511            | OpCode::Min
512            | OpCode::Max
513            | OpCode::Shl
514            | OpCode::Shr => {
515                let _ = pop_or_unknown(&mut stack);
516                let _ = pop_or_unknown(&mut stack);
517                stack.push(StackValue::with_type(ValueType::Integer));
518            }
519            OpCode::Modmul | OpCode::Modpow => {
520                let _ = pop_or_unknown(&mut stack);
521                let _ = pop_or_unknown(&mut stack);
522                let _ = pop_or_unknown(&mut stack);
523                stack.push(StackValue::with_type(ValueType::Integer));
524            }
525            OpCode::Within => {
526                let _ = pop_or_unknown(&mut stack);
527                let _ = pop_or_unknown(&mut stack);
528                let _ = pop_or_unknown(&mut stack);
529                stack.push(StackValue::with_type(ValueType::Boolean));
530            }
531            OpCode::Sqrt
532            | OpCode::Abs
533            | OpCode::Sign
534            | OpCode::Inc
535            | OpCode::Dec
536            | OpCode::Negate => {
537                let _ = pop_or_unknown(&mut stack);
538                stack.push(StackValue::with_type(ValueType::Integer));
539            }
540            OpCode::And | OpCode::Or | OpCode::Xor => {
541                let _ = pop_or_unknown(&mut stack);
542                let _ = pop_or_unknown(&mut stack);
543                stack.push(StackValue::with_type(ValueType::Integer));
544            }
545            OpCode::Invert => {
546                let _ = pop_or_unknown(&mut stack);
547                stack.push(StackValue::with_type(ValueType::Integer));
548            }
549            OpCode::Not => {
550                let _ = pop_or_unknown(&mut stack);
551                stack.push(StackValue::with_type(ValueType::Boolean));
552            }
553            OpCode::Booland | OpCode::Boolor => {
554                let _ = pop_or_unknown(&mut stack);
555                let _ = pop_or_unknown(&mut stack);
556                stack.push(StackValue::with_type(ValueType::Boolean));
557            }
558            OpCode::Equal
559            | OpCode::Numequal
560            | OpCode::Notequal
561            | OpCode::Numnotequal
562            | OpCode::Gt
563            | OpCode::Ge
564            | OpCode::Lt
565            | OpCode::Le
566            | OpCode::Nz => {
567                // NZ is unary, but treating it as "pop 1" is enough for type recovery.
568                let _ = pop_or_unknown(&mut stack);
569                if !matches!(instr.opcode, OpCode::Nz) {
570                    let _ = pop_or_unknown(&mut stack);
571                }
572                stack.push(StackValue::with_type(ValueType::Boolean));
573            }
574
575            // Most remaining opcodes are treated as unknown/no-op for typing purposes.
576            _ => {}
577        }
578    }
579}
580
581fn reverse_top(stack: &mut [StackValue], count: usize) {
582    if count == 0 || stack.len() < count {
583        return;
584    }
585    let start = stack.len() - count;
586    stack[start..].reverse();
587}
588
589fn pop_or_unknown(stack: &mut Vec<StackValue>) -> StackValue {
590    stack.pop().unwrap_or_else(StackValue::unknown)
591}
592
593fn push_slot(stack: &mut Vec<StackValue>, ty: Option<ValueType>) {
594    stack.push(StackValue::with_type(ty.unwrap_or(ValueType::Unknown)));
595}
596
597fn push_fixed_slot(stack: &mut Vec<StackValue>, slots: &mut Vec<ValueType>, index: usize) {
598    if index >= slots.len() {
599        slots.resize(index + 1, ValueType::Unknown);
600    }
601    push_slot(stack, slots.get(index).copied());
602}
603
604fn push_indexed_slot(
605    stack: &mut Vec<StackValue>,
606    slots: &mut Vec<ValueType>,
607    operand: Option<&Operand>,
608) {
609    let Some(Operand::U8(index)) = operand else {
610        stack.push(StackValue::unknown());
611        return;
612    };
613    let idx = *index as usize;
614    if idx >= slots.len() {
615        slots.resize(idx + 1, ValueType::Unknown);
616    }
617    push_slot(stack, slots.get(idx).copied());
618}
619
620fn store_slot(stack: &mut Vec<StackValue>, slots: &mut Vec<ValueType>, index: usize) {
621    let value = pop_or_unknown(stack);
622    if index >= slots.len() {
623        slots.resize(index + 1, ValueType::Unknown);
624    }
625    let current = slots[index];
626    slots[index] = current.join(value.ty);
627}
628
629fn store_indexed_slot(
630    stack: &mut Vec<StackValue>,
631    slots: &mut Vec<ValueType>,
632    operand: Option<&Operand>,
633) {
634    let Some(Operand::U8(index)) = operand else {
635        let _ = pop_or_unknown(stack);
636        return;
637    };
638    store_slot(stack, slots, *index as usize);
639}
640
641fn scan_slot_counts(instructions: &[&Instruction]) -> Option<(usize, usize)> {
642    for instr in instructions {
643        if instr.opcode != OpCode::Initslot {
644            continue;
645        }
646        if let Some(Operand::Bytes(bytes)) = &instr.operand {
647            if bytes.len() == 2 {
648                return Some((bytes[0] as usize, bytes[1] as usize));
649            }
650        }
651    }
652    None
653}
654
655fn scan_static_slot_count(instructions: &[Instruction]) -> Option<usize> {
656    for instr in instructions {
657        if instr.opcode != OpCode::Initsslot {
658            continue;
659        }
660        if let Some(Operand::U8(count)) = &instr.operand {
661            return Some(*count as usize);
662        }
663    }
664    None
665}
666
667fn int_literal_from_operand(operand: Option<&Operand>) -> Option<i64> {
668    match operand {
669        Some(Operand::I8(v)) => Some(*v as i64),
670        Some(Operand::I16(v)) => Some(*v as i64),
671        Some(Operand::I32(v)) => Some(*v as i64),
672        Some(Operand::I64(v)) => Some(*v),
673        Some(Operand::U8(v)) => Some(*v as i64),
674        Some(Operand::U16(v)) => Some(*v as i64),
675        Some(Operand::U32(v)) => Some(*v as i64),
676        _ => None,
677    }
678}
679
680fn type_from_manifest(kind: &str) -> ValueType {
681    match kind.to_ascii_lowercase().as_str() {
682        "any" => ValueType::Any,
683        "boolean" => ValueType::Boolean,
684        "integer" => ValueType::Integer,
685        "string" => ValueType::ByteString,
686        "bytearray" => ValueType::ByteString,
687        "signature" => ValueType::ByteString,
688        "hash160" => ValueType::ByteString,
689        "hash256" => ValueType::ByteString,
690        "array" => ValueType::Array,
691        "map" => ValueType::Map,
692        "interopinterface" => ValueType::InteropInterface,
693        _ => ValueType::Unknown,
694    }
695}
696
697fn convert_target_type(operand: &Operand) -> Option<ValueType> {
698    let byte = match operand {
699        Operand::U8(v) => *v,
700        Operand::I8(v) => *v as u8,
701        _ => return None,
702    };
703    match byte {
704        0x00 => Some(ValueType::Any),
705        0x10 => Some(ValueType::Pointer),
706        0x20 => Some(ValueType::Boolean),
707        0x21 => Some(ValueType::Integer),
708        0x28 => Some(ValueType::ByteString),
709        0x30 => Some(ValueType::Buffer),
710        0x40 => Some(ValueType::Array),
711        0x41 => Some(ValueType::Struct),
712        0x48 => Some(ValueType::Map),
713        0x60 => Some(ValueType::InteropInterface),
714        _ => None,
715    }
716}