neo-decompiler 0.8.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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
// Bytecode offset arithmetic requires isize↔usize casts for signed jump deltas.
// NEF scripts are bounded (~1 MB), so these conversions are structurally safe.
#![allow(
    clippy::cast_possible_truncation,
    clippy::cast_possible_wrap,
    clippy::cast_sign_loss
)]

use crate::instruction::OpCode;
use crate::instruction::{Instruction, Operand};

use super::super::{HighLevelEmitter, LoopContext};

impl HighLevelEmitter {
    fn is_conditional_branch(opcode: OpCode) -> bool {
        matches!(
            opcode,
            OpCode::Jmpif
                | OpCode::Jmpif_L
                | OpCode::Jmpifnot
                | OpCode::Jmpifnot_L
                | OpCode::JmpEq
                | OpCode::JmpEq_L
                | OpCode::JmpNe
                | OpCode::JmpNe_L
                | OpCode::JmpGt
                | OpCode::JmpGt_L
                | OpCode::JmpGe
                | OpCode::JmpGe_L
                | OpCode::JmpLt
                | OpCode::JmpLt_L
                | OpCode::JmpLe
                | OpCode::JmpLe_L
        )
    }

    fn has_internal_crossing_branch(&self, branch_offset: usize, false_target: usize) -> bool {
        let start_index = self
            .index_by_offset
            .range((branch_offset + 1)..false_target)
            .next()
            .map(|(_, index)| *index);
        let Some(start_index) = start_index else {
            return false;
        };
        let end_index = self
            .index_by_offset
            .range(false_target..)
            .next()
            .map(|(_, index)| *index)
            .unwrap_or(self.program.len());

        self.program[start_index..end_index].iter().any(|inner| {
            Self::is_conditional_branch(inner.opcode)
                && self
                    .forward_jump_target(inner)
                    .map(|target| target > false_target)
                    .unwrap_or(false)
        })
    }

    fn has_crossing_closer(&self, target: usize) -> bool {
        self.pending_closers
            .keys()
            .next()
            .map(|next_close| target > *next_close)
            .unwrap_or(false)
    }

    fn emit_conditional_goto(&mut self, instruction: &Instruction, condition: &str, target: usize) {
        self.push_comment(instruction);
        if self.index_by_offset.contains_key(&target) {
            self.transfer_labels.insert(target);
        }
        self.statements.push(format!(
            "if {condition} {{ goto {}; }}",
            Self::transfer_label_name(target)
        ));
    }

    pub(in super::super) fn emit_comparison_if_block(
        &mut self,
        instruction: &Instruction,
        symbol: &str,
    ) {
        let delta = match instruction.operand {
            Some(Operand::Jump(value)) => value as isize,
            Some(Operand::Jump32(value)) => value as isize,
            _ => {
                self.emit_relative(instruction, &format!("jump-if-{symbol}"));
                return;
            }
        };
        // Neo VM: target = opcode_offset + delta (offset is relative to instruction start).
        let target = instruction.offset as isize + delta;
        if target <= instruction.offset as isize {
            self.emit_relative(instruction, &format!("jump-if-{symbol}"));
            return;
        }

        if self.stack.len() < 2 {
            self.push_comment(instruction);
            self.stack_underflow(instruction, 2);
            return;
        }

        let (Some(right), Some(left)) = (self.stack.pop(), self.stack.pop()) else {
            return;
        };
        let condition = format!("{left} {symbol} {right}");

        let false_target = target as usize;
        if self.has_crossing_closer(false_target)
            || self.has_internal_crossing_branch(instruction.offset, false_target)
        {
            // A structured `if` here would cross another open block's closer and
            // produce malformed mis-nested output (e.g. a double `else`). Emit a
            // guarded goto on the jump condition instead, mirroring the unary
            // JMPIF path (`emit_unary_if_block`). The jump fires when the
            // structured fall-through condition is false.
            self.emit_conditional_goto(instruction, &format!("!({condition})"), false_target);
            return;
        }

        self.push_comment(instruction);
        self.statements.push(format!("if {condition} {{"));

        // Save stack state so it can be restored when the if-body closes.
        // This handles cases where the if-body terminates (throw/return/abort)
        // and clears the stack — the code after the if-block still needs the
        // pre-branch stack state.
        self.branch_saved_stacks
            .entry(false_target)
            .or_insert_with(|| self.stack.clone());
        let closer_entry = self.pending_closers.entry(false_target).or_insert(0);
        *closer_entry += 1;

        if let Some((jump_offset, jump_target)) = self.detect_else(instruction.offset, false_target)
        {
            if !self.is_loop_control_target(jump_target)
                && !self.else_targets.contains_key(&false_target)
            {
                self.skip_jumps.insert(jump_offset);
                let else_entry = self.else_targets.entry(false_target).or_insert(0);
                *else_entry += 1;
                let closer = self.pending_closers.entry(jump_target).or_insert(0);
                *closer += 1;
                // Record pre-branch stack depth at the merge offset so that
                // merge-time logic can detect branch-produced stack values.
                self.pre_branch_stack_depth
                    .entry(jump_target)
                    .or_insert(self.stack.len());
            }
        } else if let Some(else_end) = self.detect_implicit_else(instruction.offset, false_target) {
            if !self.else_targets.contains_key(&false_target) {
                let else_entry = self.else_targets.entry(false_target).or_insert(0);
                *else_entry += 1;
                let closer = self.pending_closers.entry(else_end).or_insert(0);
                *closer += 1;
            }
        }
    }

    pub(in super::super) fn emit_if_block(&mut self, instruction: &Instruction) {
        self.emit_unary_if_block(instruction, false, "jump-ifnot");
    }

    pub(in super::super) fn emit_jmpif_block(&mut self, instruction: &Instruction) {
        self.emit_unary_if_block(instruction, true, "jump-if");
    }

    fn emit_unary_if_block(
        &mut self,
        instruction: &Instruction,
        negate_condition: bool,
        fallback_label: &str,
    ) {
        let delta = match instruction.operand {
            Some(Operand::Jump(value)) => value as isize,
            Some(Operand::Jump32(value)) => value as isize,
            _ => {
                self.emit_relative(instruction, fallback_label);
                return;
            }
        };
        // Neo VM: target = opcode_offset + delta (offset is relative to instruction start).
        let target = instruction.offset as isize + delta;
        if target <= instruction.offset as isize {
            self.emit_relative(instruction, fallback_label);
            return;
        }

        let raw_condition = match self.stack.pop() {
            Some(value) => value,
            None => {
                self.push_comment(instruction);
                self.stack_underflow(instruction, 1);
                return;
            }
        };
        let mut condition = raw_condition.clone();
        if negate_condition {
            condition = format!("!{condition}");
        }

        let false_target = target as usize;
        if self.has_crossing_closer(false_target)
            || self.has_internal_crossing_branch(instruction.offset, false_target)
        {
            let jump_condition = if negate_condition {
                raw_condition
            } else {
                format!("!{raw_condition}")
            };
            self.emit_conditional_goto(instruction, &jump_condition, false_target);
            return;
        }

        self.push_comment(instruction);
        let loop_jump = self.detect_loop_back(false_target, instruction.offset);
        if let Some(loop_jump) = loop_jump.as_ref() {
            self.statements.push(format!("while {condition} {{"));
            self.skip_jumps.insert(loop_jump.jump_offset);
            self.loop_stack.push(LoopContext {
                break_offset: false_target,
                continue_offset: loop_jump.target,
            });
        } else {
            self.statements.push(format!("if {condition} {{"));
        }
        // Save stack state so it can be restored when the if/while-body closes.
        self.branch_saved_stacks
            .entry(false_target)
            .or_insert_with(|| self.stack.clone());
        let closer_entry = self.pending_closers.entry(false_target).or_insert(0);
        *closer_entry += 1;

        if loop_jump.is_none() {
            if let Some((jump_offset, jump_target)) =
                self.detect_else(instruction.offset, false_target)
            {
                if !self.is_loop_control_target(jump_target)
                    && !self.else_targets.contains_key(&false_target)
                {
                    self.skip_jumps.insert(jump_offset);
                    let else_entry = self.else_targets.entry(false_target).or_insert(0);
                    *else_entry += 1;
                    let closer = self.pending_closers.entry(jump_target).or_insert(0);
                    *closer += 1;
                    // Record pre-branch stack depth at the merge offset so that
                    // merge-time logic can detect branch-produced stack values.
                    self.pre_branch_stack_depth
                        .entry(jump_target)
                        .or_insert(self.stack.len());
                }
            } else if let Some(else_end) =
                self.detect_implicit_else(instruction.offset, false_target)
            {
                if !self.else_targets.contains_key(&false_target) {
                    let else_entry = self.else_targets.entry(false_target).or_insert(0);
                    *else_entry += 1;
                    let closer = self.pending_closers.entry(else_end).or_insert(0);
                    *closer += 1;
                }
            }
        }
    }

    fn detect_else(&self, branch_offset: usize, false_offset: usize) -> Option<(usize, usize)> {
        let target_index = *self.index_by_offset.get(&false_offset)?;
        if target_index == 0 {
            return None;
        }
        let jump = self.program.get(target_index.checked_sub(1)?)?;
        // Only unconditional JMP/JMP_L indicate an else branch.  Other opcodes
        // that share the Jump8/Jump32 operand encoding (CALL, ENDTRY, etc.)
        // must not be mistaken for else jumps — doing so nests catch/finally
        // blocks inside spurious else branches.
        if !matches!(jump.opcode, OpCode::Jmp | OpCode::Jmp_L) {
            return None;
        }
        let target = self.forward_jump_target(jump)?;
        if target <= false_offset {
            return None;
        }

        // Guard against guarded-goto switch patterns: if any conditional branch
        // outside the current branch has a forward target that lands between
        // `branch_offset` (exclusive) and `jump.offset` (inclusive), the JMP
        // belongs to that branch's case body — not to our if-body.  Treating
        // it as an else-jump would swallow the case's break goto.
        let jump_offset = jump.offset;
        let has_external_entry = self.program.iter().any(|ins| {
            Self::is_conditional_branch(ins.opcode)
                && ins.offset < branch_offset
                && self
                    .forward_jump_target(ins)
                    .map(|t| t > branch_offset && t <= jump_offset)
                    .unwrap_or(false)
        });
        if has_external_entry {
            return None;
        }

        Some((jump_offset, target))
    }

    /// Detect an implicit else branch when the if-true body ends with a
    /// noreturn instruction.  The Neo C# compiler omits the JMP before the
    /// false-target when the if-true branch always terminates (ABORT, ABORTMSG,
    /// THROW, RET, or CALL to a known noreturn method).
    ///
    /// Returns `Some(else_end_offset)` — the offset where the else closer `}`
    /// should be placed.  The caller must NOT insert a `skip_jumps` entry
    /// (there is no JMP to skip).
    fn detect_implicit_else(&self, branch_offset: usize, false_offset: usize) -> Option<usize> {
        let target_index = *self.index_by_offset.get(&false_offset)?;
        if target_index == 0 {
            return None;
        }
        let prev = self.program.get(target_index.checked_sub(1)?)?;

        // Check if the if-true body ends with a direct terminator. RET is
        // included so `if (cond) { return X; } else { return Y; }` patterns
        // emit the explicit else block instead of falling out as
        // `if (cond) { return X; } return Y;` — matches JS port behaviour
        // and reads more faithfully against the original C# source where
        // the `else` was actually written. The `} else {` formatting is
        // produced by `join_close_brace_with_chain` (final-pass cleanup).
        let is_direct_terminator = matches!(
            prev.opcode,
            OpCode::Abort | OpCode::Abortmsg | OpCode::Throw | OpCode::Ret
        );

        // Check if the if-true body ends with a CALL to a known noreturn method.
        let is_noreturn_call = matches!(prev.opcode, OpCode::Call | OpCode::Call_L)
            && self
                .call_targets_by_offset
                .get(&prev.offset)
                .is_some_and(|target| self.noreturn_method_offsets.contains(target));

        if !is_direct_terminator && !is_noreturn_call {
            return None;
        }

        // Don't create an else around catch/finally handlers.
        if self.catch_targets.contains_key(&false_offset)
            || self.finally_targets.contains_key(&false_offset)
        {
            return None;
        }

        // Don't create an implicit else when an inner conditional branch
        // within the if-body also targets false_offset.  Such an escape
        // path means the if-body does NOT always terminate — the inner
        // branch can skip the terminator and reach false_offset directly.
        // Wrapping the continuation in else { } would be structurally wrong.
        let has_inner_escape = self.program.iter().any(|ins| {
            ins.offset > branch_offset
                && ins.offset < false_offset
                && Self::is_conditional_branch(ins.opcode)
                && self.forward_jump_target(ins) == Some(false_offset)
        });
        if has_inner_escape {
            return None;
        }

        // Don't create an implicit else when multiple if-blocks close at
        // the same offset (pending_closers count > 1).  The current
        // if-block already incremented the closer count before this call,
        // so count > 1 means an enclosing if also closes here.  The
        // continuation code belongs to the enclosing scope, not just this
        // inner if — wrapping it in else { } would be structurally wrong.
        if self
            .pending_closers
            .get(&false_offset)
            .copied()
            .unwrap_or(0)
            > 1
        {
            return None;
        }

        // Find the else body end: the next structural boundary (catch/finally
        // target) after false_offset, or the offset past the last instruction.
        let next_boundary = self
            .catch_targets
            .keys()
            .chain(self.finally_targets.keys())
            .filter(|&&offset| offset > false_offset)
            .min()
            .copied();

        let fallback = self.program.last().map(|ins| ins.offset + 1);
        let else_end = next_boundary.unwrap_or_else(|| fallback.unwrap_or(false_offset + 1));

        // Don't create an else when the body would be empty — i.e. every
        // instruction between false_offset (inclusive) and else_end (exclusive)
        // is purely structural control flow (ENDTRY, NOP, unconditional JMP).
        let end_index = self
            .index_by_offset
            .range(else_end..)
            .next()
            .map(|(_, idx)| *idx)
            .unwrap_or(self.program.len());
        let body_has_substance = self.program[target_index..end_index].iter().any(|ins| {
            !matches!(
                ins.opcode,
                OpCode::Endtry | OpCode::EndtryL | OpCode::Nop | OpCode::Jmp | OpCode::Jmp_L
            )
        });
        if !body_has_substance {
            return None;
        }

        // Don't create an implicit else inside a finally block when the
        // else body contains sequential cleanup code (has ENDFINALLY).
        // Code after an if inside finally is normally sequential — wrapping
        // it in else { } would make the post-if code conditional when it
        // should be unconditional.  However, when the compiler omits
        // ENDFINALLY because all paths abort/throw, the else IS genuine
        // (both branches are noreturn), so we allow it.
        let inside_finally = self
            .finally_body_ranges
            .iter()
            .any(|&(start, end)| false_offset >= start && false_offset < end);
        if inside_finally {
            let has_endfinally = self.program[target_index..end_index]
                .iter()
                .any(|ins| matches!(ins.opcode, OpCode::Endfinally));
            if has_endfinally {
                return None;
            }
        }

        // Don't create an implicit else whose body would cross an ENCLOSING
        // if-block's closer. An else spanning [false_offset, else_end) that
        // steps over a pending closer registered strictly inside that range
        // swallows the enclosing if's continuation and produces a malformed
        // double-else (`} else { … } else { … }` on one if). The inner block's
        // own closer is not registered until after this returns, so an
        // exclusive-both-ends check catches only genuinely enclosing closers —
        // mirroring the crossing protection the comparison-jump path receives.
        if self
            .pending_closers
            .keys()
            .any(|&offset| offset > false_offset && offset < else_end)
        {
            return None;
        }

        Some(else_end)
    }
}