use serde::{Deserialize, Serialize};
use super::opcodes::opcode_byte;
use super::stack::{PushValue, StackMethod, StackOp};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConstructorSlot {
#[serde(rename = "paramIndex")]
pub param_index: usize,
#[serde(rename = "byteOffset")]
pub byte_offset: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceMapping {
#[serde(rename = "opcodeIndex")]
pub opcode_index: usize,
#[serde(rename = "sourceFile")]
pub source_file: String,
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone)]
pub struct EmitResult {
pub script_hex: String,
pub script_asm: String,
pub constructor_slots: Vec<ConstructorSlot>,
pub code_separator_index: i64,
pub code_separator_indices: Vec<usize>,
pub source_map: Vec<SourceMapping>,
}
struct EmitContext {
hex_parts: Vec<String>,
asm_parts: Vec<String>,
byte_length: usize,
constructor_slots: Vec<ConstructorSlot>,
code_separator_index: i64,
code_separator_indices: Vec<usize>,
opcode_index: usize,
source_map: Vec<SourceMapping>,
pending_source_loc: Option<crate::ir::SourceLocation>,
}
impl EmitContext {
fn new() -> Self {
EmitContext {
hex_parts: Vec::new(),
asm_parts: Vec::new(),
byte_length: 0,
constructor_slots: Vec::new(),
code_separator_index: -1,
code_separator_indices: Vec::new(),
opcode_index: 0,
source_map: Vec::new(),
pending_source_loc: None,
}
}
fn append_hex(&mut self, hex: &str) {
self.byte_length += hex.len() / 2;
self.hex_parts.push(hex.to_string());
}
fn record_source_mapping(&mut self) {
if let Some(ref loc) = self.pending_source_loc {
self.source_map.push(SourceMapping {
opcode_index: self.opcode_index,
source_file: loc.file.clone(),
line: loc.line,
column: loc.column,
});
}
}
fn emit_opcode(&mut self, name: &str) -> Result<(), String> {
let byte = opcode_byte(name)
.ok_or_else(|| format!("unknown opcode: {}", name))?;
if name == "OP_CODESEPARATOR" {
self.code_separator_index = self.byte_length as i64;
self.code_separator_indices.push(self.byte_length);
}
self.record_source_mapping();
self.append_hex(&format!("{:02x}", byte));
self.asm_parts.push(name.to_string());
self.opcode_index += 1;
Ok(())
}
fn emit_push(&mut self, value: &PushValue) {
let (h, a) = encode_push_value(value);
self.record_source_mapping();
self.append_hex(&h);
self.asm_parts.push(a);
self.opcode_index += 1;
}
fn emit_placeholder(&mut self, param_index: usize, _param_name: &str) {
let byte_offset = self.byte_length;
self.record_source_mapping();
self.append_hex("00"); self.asm_parts.push("OP_0".to_string());
self.opcode_index += 1;
self.constructor_slots.push(ConstructorSlot {
param_index,
byte_offset,
});
}
fn get_hex(&self) -> String {
self.hex_parts.join("")
}
fn get_asm(&self) -> String {
self.asm_parts.join(" ")
}
}
pub fn encode_script_number(n: i128) -> Vec<u8> {
if n == 0 {
return Vec::new();
}
let negative = n < 0;
let mut abs = if negative { (-n) as u128 } else { n as u128 };
let mut bytes = Vec::new();
while abs > 0 {
bytes.push((abs & 0xff) as u8);
abs >>= 8;
}
let last_byte = *bytes.last().unwrap();
if last_byte & 0x80 != 0 {
bytes.push(if negative { 0x80 } else { 0x00 });
} else if negative {
let len = bytes.len();
bytes[len - 1] = last_byte | 0x80;
}
bytes
}
pub fn encode_push_data(data: &[u8]) -> Vec<u8> {
let len = data.len();
if len == 0 {
return vec![0x00]; }
if len == 1 {
let b = data[0];
if b >= 1 && b <= 16 {
return vec![0x50 + b]; }
if b == 0x81 {
return vec![0x4f]; }
}
if len <= 75 {
let mut result = vec![len as u8];
result.extend_from_slice(data);
return result;
}
if len <= 255 {
let mut result = vec![0x4c, len as u8]; result.extend_from_slice(data);
return result;
}
if len <= 65535 {
let mut result = vec![0x4d, (len & 0xff) as u8, ((len >> 8) & 0xff) as u8]; result.extend_from_slice(data);
return result;
}
let mut result = vec![
0x4e,
(len & 0xff) as u8,
((len >> 8) & 0xff) as u8,
((len >> 16) & 0xff) as u8,
((len >> 24) & 0xff) as u8,
];
result.extend_from_slice(data);
result
}
fn encode_push_value(value: &PushValue) -> (String, String) {
match value {
PushValue::Bool(b) => {
if *b {
("51".to_string(), "OP_TRUE".to_string())
} else {
("00".to_string(), "OP_FALSE".to_string())
}
}
PushValue::Int(n) => encode_push_int(*n),
PushValue::Bytes(bytes) => {
let encoded = encode_push_data(bytes);
let h = hex::encode(&encoded);
if bytes.is_empty() {
(h, "OP_0".to_string())
} else {
(h, format!("<{}>", hex::encode(bytes)))
}
}
}
}
pub fn encode_push_int(n: i128) -> (String, String) {
if n == 0 {
return ("00".to_string(), "OP_0".to_string());
}
if n == -1 {
return ("4f".to_string(), "OP_1NEGATE".to_string());
}
if n >= 1 && n <= 16 {
let opcode = 0x50 + n as u8;
return (format!("{:02x}", opcode), format!("OP_{}", n));
}
let num_bytes = encode_script_number(n);
let encoded = encode_push_data(&num_bytes);
(hex::encode(&encoded), format!("<{}>", hex::encode(&num_bytes)))
}
fn emit_stack_op(op: &StackOp, ctx: &mut EmitContext) -> Result<(), String> {
match op {
StackOp::Push(value) => {
ctx.emit_push(value);
Ok(())
}
StackOp::Dup => ctx.emit_opcode("OP_DUP"),
StackOp::Swap => ctx.emit_opcode("OP_SWAP"),
StackOp::Roll { .. } => ctx.emit_opcode("OP_ROLL"),
StackOp::Pick { .. } => ctx.emit_opcode("OP_PICK"),
StackOp::Drop => ctx.emit_opcode("OP_DROP"),
StackOp::Nip => ctx.emit_opcode("OP_NIP"),
StackOp::Over => ctx.emit_opcode("OP_OVER"),
StackOp::Rot => ctx.emit_opcode("OP_ROT"),
StackOp::Tuck => ctx.emit_opcode("OP_TUCK"),
StackOp::Opcode(code) => ctx.emit_opcode(code),
StackOp::If {
then_ops,
else_ops,
} => emit_if(then_ops, else_ops, ctx),
StackOp::Placeholder {
param_index,
param_name,
} => {
ctx.emit_placeholder(*param_index, param_name);
Ok(())
}
StackOp::PushCodeSepIndex => {
let idx = if ctx.code_separator_index < 0 {
0i64
} else {
ctx.code_separator_index
};
ctx.emit_push(&PushValue::Int(idx as i128));
Ok(())
}
}
}
fn emit_if(
then_ops: &[StackOp],
else_ops: &[StackOp],
ctx: &mut EmitContext,
) -> Result<(), String> {
ctx.emit_opcode("OP_IF")?;
for op in then_ops {
emit_stack_op(op, ctx)?;
}
if !else_ops.is_empty() {
ctx.emit_opcode("OP_ELSE")?;
for op in else_ops {
emit_stack_op(op, ctx)?;
}
}
ctx.emit_opcode("OP_ENDIF")
}
pub fn emit(methods: &[StackMethod]) -> Result<EmitResult, String> {
let mut ctx = EmitContext::new();
let public_methods: Vec<StackMethod> = methods
.iter()
.filter(|m| m.name != "constructor")
.cloned()
.collect();
if public_methods.is_empty() {
return Ok(EmitResult {
script_hex: String::new(),
script_asm: String::new(),
constructor_slots: Vec::new(),
code_separator_index: -1,
code_separator_indices: Vec::new(),
source_map: Vec::new(),
});
}
if public_methods.len() == 1 {
let m = &public_methods[0];
for (idx, op) in m.ops.iter().enumerate() {
ctx.pending_source_loc = m.source_locs.get(idx).cloned().flatten();
emit_stack_op(op, &mut ctx)?;
}
} else {
let refs: Vec<&StackMethod> = public_methods.iter().collect();
emit_method_dispatch(&refs, &mut ctx)?;
}
Ok(EmitResult {
script_hex: ctx.get_hex(),
script_asm: ctx.get_asm(),
constructor_slots: ctx.constructor_slots,
code_separator_index: ctx.code_separator_index,
code_separator_indices: ctx.code_separator_indices,
source_map: ctx.source_map,
})
}
fn emit_method_dispatch(
methods: &[&StackMethod],
ctx: &mut EmitContext,
) -> Result<(), String> {
for (i, method) in methods.iter().enumerate() {
let is_last = i == methods.len() - 1;
if !is_last {
ctx.emit_opcode("OP_DUP")?;
ctx.emit_push(&PushValue::Int(i as i128));
ctx.emit_opcode("OP_NUMEQUAL")?;
ctx.emit_opcode("OP_IF")?;
ctx.emit_opcode("OP_DROP")?;
} else {
ctx.emit_push(&PushValue::Int(i as i128));
ctx.emit_opcode("OP_NUMEQUALVERIFY")?;
}
for (idx, op) in method.ops.iter().enumerate() {
ctx.pending_source_loc = method.source_locs.get(idx).cloned().flatten();
emit_stack_op(op, ctx)?;
}
if !is_last {
ctx.emit_opcode("OP_ELSE")?;
}
}
for _ in 0..methods.len() - 1 {
ctx.emit_opcode("OP_ENDIF")?;
}
Ok(())
}
pub fn emit_method(method: &StackMethod) -> Result<EmitResult, String> {
let mut ctx = EmitContext::new();
for (idx, op) in method.ops.iter().enumerate() {
ctx.pending_source_loc = method.source_locs.get(idx).cloned().flatten();
emit_stack_op(op, &mut ctx)?;
}
Ok(EmitResult {
script_hex: ctx.get_hex(),
script_asm: ctx.get_asm(),
constructor_slots: ctx.constructor_slots,
code_separator_index: ctx.code_separator_index,
code_separator_indices: ctx.code_separator_indices,
source_map: ctx.source_map,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_emit_placeholder_produces_constructor_slot() {
let method = StackMethod {
name: "unlock".to_string(),
ops: vec![StackOp::Placeholder {
param_index: 0,
param_name: "pubKeyHash".to_string(),
}],
max_stack_depth: 1,
source_locs: vec![],
};
let result = emit_method(&method).expect("emit should succeed");
assert_eq!(
result.constructor_slots.len(),
1,
"should produce exactly one constructor slot"
);
assert_eq!(result.constructor_slots[0].param_index, 0);
assert_eq!(result.constructor_slots[0].byte_offset, 0);
}
#[test]
fn test_multiple_placeholders_produce_distinct_byte_offsets() {
let method = StackMethod {
name: "test".to_string(),
ops: vec![
StackOp::Placeholder {
param_index: 0,
param_name: "a".to_string(),
},
StackOp::Placeholder {
param_index: 1,
param_name: "b".to_string(),
},
],
max_stack_depth: 2,
source_locs: vec![],
};
let result = emit_method(&method).expect("emit should succeed");
assert_eq!(
result.constructor_slots.len(),
2,
"should produce two constructor slots"
);
assert_eq!(result.constructor_slots[0].param_index, 0);
assert_eq!(result.constructor_slots[0].byte_offset, 0);
assert_eq!(result.constructor_slots[1].param_index, 1);
assert_eq!(result.constructor_slots[1].byte_offset, 1);
assert_ne!(
result.constructor_slots[0].byte_offset,
result.constructor_slots[1].byte_offset
);
}
#[test]
fn test_placeholder_byte_offset_position_is_op_0() {
let method = StackMethod {
name: "test".to_string(),
ops: vec![
StackOp::Push(PushValue::Int(42)), StackOp::Placeholder {
param_index: 0,
param_name: "x".to_string(),
},
],
max_stack_depth: 2,
source_locs: vec![],
};
let result = emit_method(&method).expect("emit should succeed");
assert_eq!(result.constructor_slots.len(), 1);
let slot = &result.constructor_slots[0];
let hex = &result.script_hex;
let byte_hex = &hex[slot.byte_offset * 2..slot.byte_offset * 2 + 2];
assert_eq!(
byte_hex, "00",
"expected OP_0 at placeholder byte offset {}, got '{}' in hex '{}'",
slot.byte_offset, byte_hex, hex
);
}
#[test]
fn test_emit_single_method_produces_hex_and_asm() {
use super::super::optimizer::optimize_stack_ops;
let method = StackMethod {
name: "check".to_string(),
ops: vec![
StackOp::Push(PushValue::Int(42)),
StackOp::Opcode("OP_NUMEQUAL".to_string()),
StackOp::Opcode("OP_VERIFY".to_string()),
],
max_stack_depth: 1,
source_locs: vec![],
};
let optimized_method = StackMethod {
name: method.name.clone(),
ops: optimize_stack_ops(&method.ops),
max_stack_depth: method.max_stack_depth,
source_locs: vec![],
};
let result = emit(&[optimized_method]).expect("emit should succeed");
assert!(!result.script_hex.is_empty(), "hex should not be empty");
assert!(!result.script_asm.is_empty(), "asm should not be empty");
assert!(
result.script_asm.contains("OP_NUMEQUALVERIFY"),
"standalone peephole optimizer should combine OP_NUMEQUAL + OP_VERIFY into OP_NUMEQUALVERIFY, got: {}",
result.script_asm
);
}
#[test]
fn test_emit_empty_methods_produces_empty_output() {
let result = emit(&[]).expect("emit with no methods should succeed");
assert!(
result.script_hex.is_empty(),
"empty methods should produce empty hex"
);
assert!(
result.constructor_slots.is_empty(),
"empty methods should produce no constructor slots"
);
}
#[test]
fn test_emit_push_bool_values() {
let method = StackMethod {
name: "test".to_string(),
ops: vec![
StackOp::Push(PushValue::Bool(true)),
StackOp::Push(PushValue::Bool(false)),
],
max_stack_depth: 2,
source_locs: vec![],
};
let result = emit_method(&method).expect("emit should succeed");
assert!(
result.script_hex.starts_with("51"),
"true should emit 0x51, got: {}",
result.script_hex
);
assert!(
result.script_hex.ends_with("00"),
"false should emit 0x00, got: {}",
result.script_hex
);
assert!(result.script_asm.contains("OP_TRUE"));
assert!(result.script_asm.contains("OP_FALSE"));
}
#[test]
fn test_byte_offset_with_push_data() {
let method = StackMethod {
name: "check".to_string(),
ops: vec![
StackOp::Push(PushValue::Int(17)), StackOp::Placeholder {
param_index: 0,
param_name: "x".to_string(),
},
StackOp::Opcode("OP_ADD".to_string()),
],
max_stack_depth: 2,
source_locs: vec![],
};
let result = emit_method(&method).expect("emit should succeed");
assert_eq!(
result.constructor_slots.len(),
1,
"expected 1 constructor slot"
);
let slot = &result.constructor_slots[0];
assert_eq!(
slot.byte_offset, 2,
"expected byteOffset=2 (after push 17 = 2 bytes), got {}",
slot.byte_offset
);
}
#[test]
fn test_simple_sequence_hex() {
let method = StackMethod {
name: "check".to_string(),
ops: vec![
StackOp::Opcode("OP_DUP".to_string()),
StackOp::Opcode("OP_HASH160".to_string()),
],
max_stack_depth: 1,
source_locs: vec![],
};
let result = emit_method(&method).expect("emit should succeed");
assert_eq!(
result.script_hex, "76a9",
"expected hex '76a9' for DUP+HASH160, got: {}",
result.script_hex
);
}
#[test]
fn test_peephole_optimization_applied() {
use super::super::optimizer::optimize_stack_ops;
let ops = vec![
StackOp::Opcode("OP_CHECKSIG".to_string()),
StackOp::Opcode("OP_VERIFY".to_string()),
StackOp::Opcode("OP_1".to_string()),
];
let optimized_ops = optimize_stack_ops(&ops);
let method = StackMethod {
name: "check".to_string(),
ops: optimized_ops,
max_stack_depth: 1,
source_locs: vec![],
};
let result = emit_method(&method).expect("emit should succeed");
assert_eq!(
result.script_hex, "ad51",
"expected 'ad51' (CHECKSIGVERIFY + OP_1) after peephole, got: {}",
result.script_hex
);
assert!(
result.script_asm.contains("OP_CHECKSIGVERIFY"),
"expected OP_CHECKSIGVERIFY in ASM, got: {}",
result.script_asm
);
}
#[test]
fn test_multi_method_dispatch_produces_if_else() {
use super::super::stack::lower_to_stack;
use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFValue};
let program = ANFProgram {
contract_name: "Multi".to_string(),
properties: vec![],
methods: vec![
ANFMethod {
name: "constructor".to_string(),
params: vec![],
body: vec![],
is_public: false,
},
ANFMethod {
name: "m1".to_string(),
params: vec![ANFParam {
name: "x".to_string(),
param_type: "bigint".to_string(),
}],
body: vec![
ANFBinding {
name: "t0".to_string(),
value: ANFValue::LoadParam { name: "x".to_string() },
source_loc: None,
},
ANFBinding {
name: "t1".to_string(),
value: ANFValue::LoadConst {
value: serde_json::Value::Number(serde_json::Number::from(1)),
},
source_loc: None,
},
ANFBinding {
name: "t2".to_string(),
value: ANFValue::BinOp {
op: "===".to_string(),
left: "t0".to_string(),
right: "t1".to_string(),
result_type: None,
},
source_loc: None,
},
ANFBinding {
name: "t3".to_string(),
value: ANFValue::Assert { value: "t2".to_string() },
source_loc: None,
},
],
is_public: true,
},
ANFMethod {
name: "m2".to_string(),
params: vec![ANFParam {
name: "y".to_string(),
param_type: "bigint".to_string(),
}],
body: vec![
ANFBinding {
name: "t0".to_string(),
value: ANFValue::LoadParam { name: "y".to_string() },
source_loc: None,
},
ANFBinding {
name: "t1".to_string(),
value: ANFValue::LoadConst {
value: serde_json::Value::Number(serde_json::Number::from(2)),
},
source_loc: None,
},
ANFBinding {
name: "t2".to_string(),
value: ANFValue::BinOp {
op: "===".to_string(),
left: "t0".to_string(),
right: "t1".to_string(),
result_type: None,
},
source_loc: None,
},
ANFBinding {
name: "t3".to_string(),
value: ANFValue::Assert { value: "t2".to_string() },
source_loc: None,
},
],
is_public: true,
},
],
};
let methods = lower_to_stack(&program).expect("lower_to_stack should succeed");
use super::super::optimizer::optimize_stack_ops;
let optimized: Vec<StackMethod> = methods
.iter()
.map(|m| StackMethod {
name: m.name.clone(),
ops: optimize_stack_ops(&m.ops),
max_stack_depth: m.max_stack_depth,
source_locs: vec![],
})
.collect();
let result = emit(&optimized).expect("emit should succeed");
assert!(
result.script_asm.contains("OP_IF"),
"expected OP_IF in multi-method dispatch, got: {}",
result.script_asm
);
assert!(
result.script_asm.contains("OP_ELSE"),
"expected OP_ELSE in multi-method dispatch, got: {}",
result.script_asm
);
assert!(
result.script_asm.contains("OP_ENDIF"),
"expected OP_ENDIF in multi-method dispatch, got: {}",
result.script_asm
);
}
#[test]
fn test_byte_offset_accounts_for_preceding_opcodes() {
let method = StackMethod {
name: "check".to_string(),
ops: vec![
StackOp::Opcode("OP_DUP".to_string()), StackOp::Opcode("OP_HASH160".to_string()), StackOp::Placeholder {
param_index: 0,
param_name: "pubKeyHash".to_string(),
},
StackOp::Opcode("OP_EQUALVERIFY".to_string()),
StackOp::Opcode("OP_CHECKSIG".to_string()),
],
max_stack_depth: 2,
source_locs: vec![],
};
let result = emit_method(&method).expect("emit should succeed");
assert_eq!(result.constructor_slots.len(), 1, "expected 1 constructor slot");
let slot = &result.constructor_slots[0];
assert_eq!(
slot.byte_offset, 2,
"expected byteOffset=2 (after OP_DUP + OP_HASH160), got {}",
slot.byte_offset
);
}
#[test]
fn test_full_p2pkh() {
use super::super::stack::lower_to_stack;
use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFProperty, ANFValue};
let program = ANFProgram {
contract_name: "P2PKH".to_string(),
properties: vec![ANFProperty {
name: "pubKeyHash".to_string(),
prop_type: "Addr".to_string(),
readonly: true,
initial_value: None,
}],
methods: vec![ANFMethod {
name: "unlock".to_string(),
params: vec![
ANFParam { name: "sig".to_string(), param_type: "Sig".to_string() },
ANFParam { name: "pubKey".to_string(), param_type: "PubKey".to_string() },
],
body: vec![
ANFBinding {
name: "t0".to_string(),
value: ANFValue::LoadParam { name: "pubKey".to_string() },
source_loc: None,
},
ANFBinding {
name: "t1".to_string(),
value: ANFValue::Call {
func: "hash160".to_string(),
args: vec!["t0".to_string()],
},
source_loc: None,
},
ANFBinding {
name: "t2".to_string(),
value: ANFValue::LoadProp { name: "pubKeyHash".to_string() },
source_loc: None,
},
ANFBinding {
name: "t3".to_string(),
value: ANFValue::BinOp {
op: "===".to_string(),
left: "t1".to_string(),
right: "t2".to_string(),
result_type: Some("bytes".to_string()),
},
source_loc: None,
},
ANFBinding {
name: "t4".to_string(),
value: ANFValue::Assert { value: "t3".to_string() },
source_loc: None,
},
ANFBinding {
name: "t5".to_string(),
value: ANFValue::LoadParam { name: "sig".to_string() },
source_loc: None,
},
ANFBinding {
name: "t6".to_string(),
value: ANFValue::LoadParam { name: "pubKey".to_string() },
source_loc: None,
},
ANFBinding {
name: "t7".to_string(),
value: ANFValue::Call {
func: "checkSig".to_string(),
args: vec!["t5".to_string(), "t6".to_string()],
},
source_loc: None,
},
ANFBinding {
name: "t8".to_string(),
value: ANFValue::Assert { value: "t7".to_string() },
source_loc: None,
},
],
is_public: true,
}],
};
let stack_methods = lower_to_stack(&program).expect("stack lowering should succeed");
let result = emit(&stack_methods).expect("emit should succeed");
assert!(
!result.script_hex.is_empty(),
"P2PKH should produce non-empty script hex"
);
assert!(
!result.constructor_slots.is_empty(),
"P2PKH should have at least one constructor slot for pubKeyHash"
);
}
#[test]
fn test_m10_integer_17_uses_push_prefix_not_op17() {
let method = StackMethod {
name: "test".to_string(),
ops: vec![StackOp::Push(PushValue::Int(17))],
max_stack_depth: 1,
source_locs: vec![],
};
let result = emit_method(&method).expect("emit should succeed");
assert!(
!result.script_hex.starts_with("61"),
"17 should NOT be encoded as OP_17 (0x61); OP_1..OP_16 are for 1–16 only. got: {}",
result.script_hex
);
assert!(
result.script_hex.contains("11"),
"17 (0x11) should appear in the script hex; got: {}",
result.script_hex
);
}
#[test]
fn test_m12_256_byte_data_uses_pushdata2() {
let data = vec![0xabu8; 256];
let method = StackMethod {
name: "test".to_string(),
ops: vec![StackOp::Push(PushValue::Bytes(data))],
max_stack_depth: 1,
source_locs: vec![],
};
let result = emit_method(&method).expect("emit should succeed");
assert!(
result.script_hex.starts_with("4d0001"),
"256-byte push should use OP_PUSHDATA2 prefix '4d0001', got: {}",
&result.script_hex[..result.script_hex.len().min(12)]
);
}
#[test]
fn test_m19_sha256_contract_has_op_sha256_in_asm() {
use super::super::stack::lower_to_stack;
use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFValue};
let program = ANFProgram {
contract_name: "Sha256Test".to_string(),
properties: vec![],
methods: vec![ANFMethod {
name: "check".to_string(),
params: vec![ANFParam {
name: "data".to_string(),
param_type: "ByteString".to_string(),
}],
body: vec![
ANFBinding {
name: "t0".to_string(),
value: ANFValue::LoadParam { name: "data".to_string() },
source_loc: None,
},
ANFBinding {
name: "t1".to_string(),
value: ANFValue::Call {
func: "sha256".to_string(),
args: vec!["t0".to_string()],
},
source_loc: None,
},
ANFBinding {
name: "t2".to_string(),
value: ANFValue::Assert { value: "t1".to_string() },
source_loc: None,
},
],
is_public: true,
}],
};
let stack_methods = lower_to_stack(&program).expect("stack lowering should succeed");
let result = emit(&stack_methods).expect("emit should succeed");
assert!(
result.script_asm.contains("OP_SHA256"),
"sha256() call should produce OP_SHA256 in ASM; got: {}",
result.script_asm
);
}
#[test]
fn test_m21_op_dup_encodes_0x76() {
let method = StackMethod {
name: "test".to_string(),
ops: vec![StackOp::Dup],
max_stack_depth: 1,
source_locs: vec![],
};
let result = emit_method(&method).expect("emit should succeed");
assert_eq!(
result.script_hex, "76",
"OP_DUP should encode as 0x76; got: {}",
result.script_hex
);
}
#[test]
fn test_m22_op_swap_encodes_0x7c() {
let method = StackMethod {
name: "test".to_string(),
ops: vec![StackOp::Swap],
max_stack_depth: 2,
source_locs: vec![],
};
let result = emit_method(&method).expect("emit should succeed");
assert_eq!(
result.script_hex, "7c",
"OP_SWAP should encode as 0x7c; got: {}",
result.script_hex
);
}
#[test]
fn test_m24_if_without_else_no_op_else() {
let method = StackMethod {
name: "test".to_string(),
ops: vec![StackOp::If {
then_ops: vec![StackOp::Opcode("OP_DROP".to_string())],
else_ops: vec![],
}],
max_stack_depth: 1,
source_locs: vec![],
};
let result = emit_method(&method).expect("emit should succeed");
assert!(
!result.script_asm.contains("OP_ELSE"),
"if with empty else branch should NOT contain OP_ELSE; got asm: {}",
result.script_asm
);
assert!(
result.script_asm.contains("OP_IF"),
"should still contain OP_IF; got asm: {}",
result.script_asm
);
}
#[test]
fn test_m25_single_method_no_dispatch() {
use super::super::stack::lower_to_stack;
use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFProperty, ANFValue};
let program = ANFProgram {
contract_name: "Single".to_string(),
properties: vec![ANFProperty {
name: "x".to_string(),
prop_type: "bigint".to_string(),
readonly: true,
initial_value: None,
}],
methods: vec![
ANFMethod {
name: "constructor".to_string(),
params: vec![ANFParam {
name: "x".to_string(),
param_type: "bigint".to_string(),
}],
body: vec![],
is_public: false,
},
ANFMethod {
name: "check".to_string(),
params: vec![ANFParam {
name: "v".to_string(),
param_type: "bigint".to_string(),
}],
body: vec![
ANFBinding {
name: "t0".to_string(),
value: ANFValue::LoadParam { name: "v".to_string() },
source_loc: None,
},
ANFBinding {
name: "t1".to_string(),
value: ANFValue::LoadProp { name: "x".to_string() },
source_loc: None,
},
ANFBinding {
name: "t2".to_string(),
value: ANFValue::BinOp {
op: "===".to_string(),
left: "t0".to_string(),
right: "t1".to_string(),
result_type: None,
},
source_loc: None,
},
ANFBinding {
name: "t3".to_string(),
value: ANFValue::Assert { value: "t2".to_string() },
source_loc: None,
},
],
is_public: true,
},
],
};
let stack_methods = lower_to_stack(&program).expect("stack lowering should succeed");
let result = emit(&stack_methods).expect("emit should succeed");
assert!(
!result.script_asm.contains("OP_IF"),
"single public method should NOT produce OP_IF dispatch; got asm: {}",
result.script_asm
);
}
}