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
use crate::instruction::{Instruction, OpCode, Operand};
use super::super::super::{HighLevelEmitter, LiteralValue, SlotKind};
impl HighLevelEmitter {
pub(in super::super::super) fn emit_return(&mut self, instruction: &Instruction) {
self.push_comment(instruction);
if self.returns_void {
// Void method: discard any leftover stack values.
self.statements.push("return;".into());
} else if let Some(value) = self.pop_stack_value() {
self.statements.push(format!("return {value};"));
} else {
self.statements.push("return;".into());
}
self.stack.clear();
}
pub(in super::super::super) fn emit_syscall(&mut self, instruction: &Instruction) {
self.push_comment(instruction);
if let Some(Operand::Syscall(hash)) = instruction.operand {
// `System.Contract.Call(scriptHash, method, callFlags, args)` is the
// cross-contract-call syscalls the devpack emits. Lifting it into
// `ContractName::method(args)` form is what lets a reader follow
// the contract's logic without first mentally mapping every
// `syscall("System.Contract.Call", ...)` back to its target.
if hash == crate::syscalls::CONTRACT_CALL_HASH {
self.emit_contract_call(instruction, hash);
return;
}
if hash == crate::syscalls::CONTRACT_CALL_NATIVE_HASH {
self.emit_contract_call_native(instruction, hash);
return;
}
let info = crate::syscalls::lookup(hash);
// When we do not have metadata, assume the syscall returns a value.
// This is conservative (avoids stack underflow) and matches Neo's
// "unknown syscalls push an item" convention.
let returns_value = info.map(|i| i.returns_value).unwrap_or(true);
let param_count = info.map(|i| i.param_count).unwrap_or(0) as usize;
let syscall_name = info.map(|i| i.name).unwrap_or("unknown syscall");
// Syscall arguments are pushed right-to-left (Cdecl) by the
// devpack, so parameters[0] sits on top of the stack at SYSCALL
// and `ApplicationEngine.OnSysCall` pops it first: pop order
// already equals declaration order.
let mut args: Vec<String> = Vec::with_capacity(param_count);
let mut missing_argument = false;
for _ in 0..param_count {
match self.pop_stack_value() {
Some(value) => args.push(value),
None => {
missing_argument = true;
args.push("???".into());
}
}
}
if missing_argument {
let mut message =
format!("missing syscall argument values for {syscall_name} (substituted ???)");
if let Some(context) =
self.missing_syscall_argument_context(instruction, syscall_name)
{
message.push_str("; ");
message.push_str(&context);
}
// Use `warn(...)` so the inline `// XXXX: missing syscall
// argument values for Foo (substituted ???)` comment fires
// regardless of trace mode — earlier this delegated to
// `note(...)` which gated on `emit_trace_comments`, so a
// reader of the clean-mode rendering saw `???` placeholders
// with no inline explanation. The JS port has always
// emitted the comment unconditionally.
self.warn(instruction, &message);
}
// Do NOT reverse: unlike `emit_call`'s VM intrinsics (operands
// pushed left-to-right), syscall pop order is declaration order.
// This matches the internal-call/CALLT convention documented in
// `control_flow/jumps.rs`.
let arg_list = args.join(", ");
if let Some(info) = info {
let call = if arg_list.is_empty() {
format!("{}()", info.name)
} else {
format!("{}({})", info.name, arg_list)
};
// For known syscalls the name already identifies the call;
// the 32-bit hash adds no information and clutters output.
// Keep it only as a debug aid when trace comments are on.
let trailing = if self.emit_trace_comments {
format!(" // 0x{hash:08X}")
} else {
String::new()
};
if returns_value {
let temp = self.next_temp();
self.statements
.push(format!("let {temp} = {call};{trailing}"));
self.stack.push(temp);
} else {
self.statements.push(format!("{call};{trailing}"));
}
} else {
// Unknown syscall: keep the `syscall(0xHASH)` wrapper
// because we have no human-readable name to substitute.
let call = format!("syscall(0x{hash:08X})");
self.warn(instruction, &format!("unknown syscall 0x{hash:08X}"));
if returns_value {
let temp = self.next_temp();
self.statements.push(format!("let {temp} = {call};"));
self.stack.push(temp);
} else {
self.statements.push(format!("{call};"));
}
}
} else {
self.statements.push(format!(
"// {:04X}: missing syscall operand",
instruction.offset
));
}
}
/// Lift `System.Contract.Call(scriptHash, method, callFlags, args)` into a
/// direct `ContractName::method(args)` call.
///
/// The devpack pushes the four arguments right-to-left (Cdecl), so at the
/// SYSCALL the stack top-to-bottom is `scriptHash, method, callFlags,
/// args`. We pop in that order — the popped values already match the
/// C#-style `ContractName::method(args)` argument order. The contract
/// hash is looked up against the bundled native-contract table; when
/// the hash isn't recognised (custom contracts) we fall back to the
/// hex form `0xHASH::method(args)` so the reader still sees the target
/// without having to grep the manifest.
///
/// Fall-back behaviour: if the contract hash on the stack isn't a tracked
/// 20-byte literal (synthetic test bytecode, or a hash produced by a
/// runtime computation), the expansion would render the call as
/// `t0::t1(t3)` — syntactically valid but unreadable. In that case we
/// fall back to the legacy `syscall("System.Contract.Call", hash, …)`
/// form so the reader still sees the devpack's intended syscall.
fn emit_contract_call(&mut self, instruction: &Instruction, hash: u32) {
let contract_hash = self.pop_stack_value_with_literal();
let method = self.pop_stack_value_with_literal();
let flags = self.pop_stack_value();
let args_array = self.pop_stack_value();
let args_str = args_array.as_deref().unwrap_or("???");
let trailing = if self.emit_trace_comments {
format!(" // 0x{hash:08X}")
} else {
String::new()
};
// Hash isn't a tracked 20-byte literal — fall back to the syscall
// form so the reader still sees the devpack's intended syscall.
let contract_hash_is_tracked = matches!(
contract_hash.as_ref().map(|(_, lit)| lit),
Some(Some(LiteralValue::ContractHash(_)))
);
if !contract_hash_is_tracked {
let hash_str = contract_hash
.as_ref()
.map(|(s, _)| s.as_str())
.unwrap_or("???");
let method_str = method.as_ref().map(|(s, _)| s.as_str()).unwrap_or("???");
let flags_str = flags.as_deref().unwrap_or("???");
let call =
format!("System.Contract.Call({hash_str}, {method_str}, {flags_str}, {args_str})");
self.warn(
instruction,
"contract-call hash was not a tracked 20-byte literal; fell back to syscall form",
);
self.statements.push(format!("{call};{trailing}"));
return;
}
let call_label = Self::contract_call_label(contract_hash.as_ref(), method.as_ref());
// The call always returns a value on the evaluation stack — even a
// `void` target method leaves its return on the stack for the
// caller to drop or use. Match the surrounding emitter's
// convention of pushing a fresh temp for any value-producing call.
let temp = self.next_temp();
self.statements
.push(format!("let {temp} = {call_label}({args_str});{trailing}"));
self.stack.push(temp);
}
/// Resolve the `ContractName::method` (or `0xHASH::method` for unknown
/// custom contracts) display label for a `System.Contract.Call`.
///
/// Both the contract hash and the method name are taken from the popped
/// stack values' literal metadata: a literal hash lifts to a bundled
/// native contract name, and a literal string method lifts to the
/// human-readable method identifier. When either literal is missing we
/// fall back to the temp name so the call site still references the
/// actual value instead of an opaque syscall wrapper.
fn contract_call_label(
contract_hash: Option<&(String, Option<LiteralValue>)>,
method_value: Option<&(String, Option<LiteralValue>)>,
) -> String {
let contract_part = match contract_hash {
Some((_, Some(LiteralValue::ContractHash(bytes)))) => {
if let Some(contract) = crate::native_contracts::lookup(bytes) {
contract.name.to_string()
} else {
format!("0x{}", hex::encode_upper(bytes))
}
}
Some((s, _)) => s.clone(),
None => "???".to_string(),
};
// Note: the test hex above uses 0xHEXUPPER (matching format_pushdata
// and the disassembler's operand Display) so the reader can grep
// the manifest for the exact literal.
let method_part = match method_value {
Some((_, Some(LiteralValue::String(s)))) => s.clone(),
Some((s, _)) => s.clone(),
None => "?".to_string(),
};
format!("{contract_part}.{method_part}")
}
/// Lift `System.Contract.CallNative(nativeId)` into a placeholder
/// `native_call_<id>()` call. `CallNative` is the deprecated
/// integer-indexed path; the modern devpack uses `System.Contract.Call`
/// with the native hash instead, so this branch only fires on legacy
/// bytecode.
fn emit_contract_call_native(&mut self, _instruction: &Instruction, hash: u32) {
let _ = self.pop_stack_value();
let trailing = if self.emit_trace_comments {
format!(" // 0x{hash:08X}")
} else {
String::new()
};
self.statements
.push(format!("System.Contract.CallNative();{trailing}"));
}
fn missing_syscall_argument_context(
&self,
instruction: &Instruction,
syscall_name: &str,
) -> Option<String> {
let &instruction_index = self.index_by_offset.get(&instruction.offset)?;
let previous = instruction_index
.checked_sub(1)
.and_then(|index| self.program.get(index))?;
let (kind, index) = Self::store_slot_context(previous)?;
let slot_name = Self::format_slot_label(kind, index);
let stored_value = if self.packed_values_by_name.contains_key(&slot_name) {
"a packed value"
} else {
"the last produced value"
};
Some(format!(
"preceding {} stored {stored_value} into {slot_name}; no value remains on the evaluation stack before {syscall_name}",
previous.opcode
))
}
fn store_slot_context(instruction: &Instruction) -> Option<(SlotKind, usize)> {
use OpCode::{
Starg, Starg0, Starg1, Starg2, Starg3, Starg4, Starg5, Starg6, Stloc, Stloc0, Stloc1,
Stloc2, Stloc3, Stloc4, Stloc5, Stloc6, Stsfld, Stsfld0, Stsfld1, Stsfld2, Stsfld3,
Stsfld4, Stsfld5, Stsfld6,
};
match instruction.opcode {
Stloc0 => Some((SlotKind::Local, 0)),
Stloc1 => Some((SlotKind::Local, 1)),
Stloc2 => Some((SlotKind::Local, 2)),
Stloc3 => Some((SlotKind::Local, 3)),
Stloc4 => Some((SlotKind::Local, 4)),
Stloc5 => Some((SlotKind::Local, 5)),
Stloc6 => Some((SlotKind::Local, 6)),
Stloc => Self::operand_slot_index(instruction).map(|index| (SlotKind::Local, index)),
Starg0 => Some((SlotKind::Argument, 0)),
Starg1 => Some((SlotKind::Argument, 1)),
Starg2 => Some((SlotKind::Argument, 2)),
Starg3 => Some((SlotKind::Argument, 3)),
Starg4 => Some((SlotKind::Argument, 4)),
Starg5 => Some((SlotKind::Argument, 5)),
Starg6 => Some((SlotKind::Argument, 6)),
Starg => Self::operand_slot_index(instruction).map(|index| (SlotKind::Argument, index)),
Stsfld0 => Some((SlotKind::Static, 0)),
Stsfld1 => Some((SlotKind::Static, 1)),
Stsfld2 => Some((SlotKind::Static, 2)),
Stsfld3 => Some((SlotKind::Static, 3)),
Stsfld4 => Some((SlotKind::Static, 4)),
Stsfld5 => Some((SlotKind::Static, 5)),
Stsfld6 => Some((SlotKind::Static, 6)),
Stsfld => Self::operand_slot_index(instruction).map(|index| (SlotKind::Static, index)),
_ => None,
}
}
fn operand_slot_index(instruction: &Instruction) -> Option<usize> {
match instruction.operand {
Some(Operand::U8(value)) => Some(value as usize),
_ => None,
}
}
fn format_slot_label(kind: SlotKind, index: usize) -> String {
match kind {
SlotKind::Local => format!("loc{index}"),
SlotKind::Argument => format!("arg{index}"),
SlotKind::Static => format!("static{index}"),
}
}
}