neo-decompiler 0.10.2

Neo N3 NEF decompiler: parse, disassemble, lift bytecode to high-level pseudocode and C# skeletons, with a CLI, JSON reports, and optional WebAssembly bindings.
Documentation
use crate::instruction::Instruction;

use super::super::super::super::HighLevelEmitter;

const PACK_MAX_INLINE: usize = 64;

impl HighLevelEmitter {
    pub(in super::super::super::super) fn emit_pack(
        &mut self,
        instruction: &Instruction,
        kind: &str,
    ) {
        self.push_comment(instruction);
        let Some(count_name) = self.stack.pop() else {
            self.stack_underflow(instruction, 1);
            return;
        };

        if let Some(need) = self.take_usize_literal(&count_name) {
            self.emit_literal_pack(kind, need);
        } else {
            self.emit_dynamic_pack(count_name);
        }
    }

    fn emit_literal_pack(&mut self, kind: &str, need: usize) {
        // Cap inline rendering to bound output for malformed inputs. For
        // PACKMAP the cap applies to entries (key/value pairs).
        let is_map = kind == "map";
        let unit = if is_map { 2 } else { 1 };
        // Bound inline width by simulated stack depth too, so literal-count
        // underflow renders a single elision marker instead of many synthetic
        // missing items, matching the JS port.
        let avail = self.stack.len() / unit;
        let cap = PACK_MAX_INLINE.min(need).min(avail);
        let mut rendered = Vec::with_capacity(cap);
        let mut elements = Vec::with_capacity(cap * unit);

        for _ in 0..cap {
            let unit_values = self.pop_pack_unit(unit);
            if is_map {
                rendered.push(format!("{}: {}", unit_values[0], unit_values[1]));
            } else {
                rendered.push(unit_values[0].clone());
            }
            elements.extend(unit_values);
        }

        if need > cap {
            self.drain_elided_pack_items(need, cap, unit);
            let marker = pack_remainder_marker(is_map, need - cap);
            rendered.push(marker.clone());
            elements.push(marker);
        }

        // Neo VM PACK: first popped item becomes array[0], second becomes
        // array[1], etc. Since we pop top-first, `elements` is already in
        // array-index order. PACKMAP entries are likewise added in pop order.
        let temp = self.next_temp();
        let body = rendered.join(", ");
        let (ctor, noun) = match kind {
            "map" => (format!("Map({body})"), "entry(s)"),
            "struct" => (format!("Struct({body})"), "element(s)"),
            _ => (format!("[{body}]"), "element(s)"),
        };
        self.statements
            .push(format!("let {temp} = {ctor}; // pack {need} {noun}"));
        // UNPACK of a map pushes key/value pairs plus entry count, so the flat
        // element replay below models arrays/structs only.
        if !is_map {
            self.packed_values_by_name.insert(temp.clone(), elements);
        }
        self.stack.push(temp);
    }

    fn emit_dynamic_pack(&mut self, count_name: String) {
        let temp = self.next_temp();
        self.statements.push(format!(
            "let {temp} = pack_dynamic({count_name}); // pack with dynamic count"
        ));
        self.packed_values_by_name.remove(&temp);
        self.stack.push(temp);
    }

    fn pop_pack_unit(&mut self, unit: usize) -> Vec<String> {
        let mut values = Vec::with_capacity(unit);
        for _ in 0..unit {
            if let Some((value, literal)) = self.pop_stack_value_with_literal() {
                if let Some(literal) = literal {
                    self.literal_values.insert(value.clone(), literal);
                }
                values.push(value);
            } else {
                let missing_temp = self.next_temp();
                self.statements.push(format!(
                    "let {missing_temp} = missing_pack_item(); // synthetic missing element for literal pack"
                ));
                values.push(missing_temp);
            }
        }
        values
    }

    fn drain_elided_pack_items(&mut self, need: usize, cap: usize, unit: usize) {
        // The VM still consumes elided entries: drain whatever the simulated
        // stack holds so subsequent instructions bind the right operands.
        let mut excess = (need - cap).saturating_mul(unit);
        while excess > 0 && self.pop_stack_value().is_some() {
            excess -= 1;
        }
    }
}

fn pack_remainder_marker(is_map: bool, remainder: usize) -> String {
    let noun = match (is_map, remainder == 1) {
        (true, true) => "entry",
        (true, false) => "entries",
        (false, true) => "element",
        (false, false) => "elements",
    };
    format!("/* {remainder} more {noun} */")
}