use super::stack::{PushValue, StackOp};
const MAX_ITERATIONS: usize = 100;
pub fn optimize_stack_ops(ops: &[StackOp]) -> Vec<StackOp> {
let mut current: Vec<StackOp> = ops.iter().map(optimize_nested_if).collect();
for _ in 0..MAX_ITERATIONS {
let (result, changed) = apply_one_pass(¤t);
if !changed {
break;
}
current = result;
}
current
}
fn optimize_nested_if(op: &StackOp) -> StackOp {
match op {
StackOp::If {
then_ops,
else_ops,
} => {
let optimized_then = optimize_stack_ops(then_ops);
let optimized_else = if else_ops.is_empty() {
Vec::new()
} else {
optimize_stack_ops(else_ops)
};
StackOp::If {
then_ops: optimized_then,
else_ops: optimized_else,
}
}
other => other.clone(),
}
}
fn apply_one_pass(ops: &[StackOp]) -> (Vec<StackOp>, bool) {
let mut result = Vec::new();
let mut changed = false;
let mut i = 0;
while i < ops.len() {
if i + 3 < ops.len() {
if let Some(replacement) =
match_window_4(&ops[i], &ops[i + 1], &ops[i + 2], &ops[i + 3])
{
result.extend(replacement);
i += 4;
changed = true;
continue;
}
}
if i + 2 < ops.len() {
if let Some(replacement) = match_window_3(&ops[i], &ops[i + 1], &ops[i + 2]) {
result.extend(replacement);
i += 3;
changed = true;
continue;
}
}
if i + 1 < ops.len() {
if let Some(replacement) = match_window_2(&ops[i], &ops[i + 1]) {
result.extend(replacement);
i += 2;
changed = true;
continue;
}
}
result.push(ops[i].clone());
i += 1;
}
(result, changed)
}
fn match_window_3(a: &StackOp, b: &StackOp, c: &StackOp) -> Option<Vec<StackOp>> {
if let (StackOp::Push(PushValue::Int(va)), StackOp::Push(PushValue::Int(vb))) = (a, b) {
if is_opcode(c, "OP_ADD") {
return Some(vec![StackOp::Push(PushValue::Int(va + vb))]);
}
if is_opcode(c, "OP_SUB") {
return Some(vec![StackOp::Push(PushValue::Int(va - vb))]);
}
if is_opcode(c, "OP_MUL") {
return Some(vec![StackOp::Push(PushValue::Int(va * vb))]);
}
}
None
}
fn match_window_4(
a: &StackOp,
b: &StackOp,
c: &StackOp,
d: &StackOp,
) -> Option<Vec<StackOp>> {
if let StackOp::Push(PushValue::Int(va)) = a {
if is_opcode(b, "OP_ADD") {
if let StackOp::Push(PushValue::Int(vb)) = c {
if is_opcode(d, "OP_ADD") {
return Some(vec![
StackOp::Push(PushValue::Int(va + vb)),
StackOp::Opcode("OP_ADD".to_string()),
]);
}
}
}
if is_opcode(b, "OP_SUB") {
if let StackOp::Push(PushValue::Int(vb)) = c {
if is_opcode(d, "OP_SUB") {
return Some(vec![
StackOp::Push(PushValue::Int(va + vb)),
StackOp::Opcode("OP_SUB".to_string()),
]);
}
}
}
}
None
}
fn match_window_2(a: &StackOp, b: &StackOp) -> Option<Vec<StackOp>> {
if matches!(a, StackOp::Push(_)) && matches!(b, StackOp::Drop) {
return Some(vec![]);
}
if matches!(a, StackOp::Dup) && matches!(b, StackOp::Drop) {
return Some(vec![]);
}
if matches!(a, StackOp::Swap) && matches!(b, StackOp::Swap) {
return Some(vec![]);
}
if is_push_int(a, 1) && is_opcode(b, "OP_ADD") {
return Some(vec![StackOp::Opcode("OP_1ADD".to_string())]);
}
if is_push_int(a, 1) && is_opcode(b, "OP_SUB") {
return Some(vec![StackOp::Opcode("OP_1SUB".to_string())]);
}
if is_push_int(a, 0) && is_opcode(b, "OP_ADD") {
return Some(vec![]);
}
if is_push_int(a, 0) && is_opcode(b, "OP_SUB") {
return Some(vec![]);
}
if is_opcode(a, "OP_NOT") && is_opcode(b, "OP_NOT") {
return Some(vec![]);
}
if is_opcode(a, "OP_NEGATE") && is_opcode(b, "OP_NEGATE") {
return Some(vec![]);
}
if is_opcode(a, "OP_EQUAL") && is_opcode(b, "OP_VERIFY") {
return Some(vec![StackOp::Opcode("OP_EQUALVERIFY".to_string())]);
}
if is_opcode(a, "OP_CHECKSIG") && is_opcode(b, "OP_VERIFY") {
return Some(vec![StackOp::Opcode("OP_CHECKSIGVERIFY".to_string())]);
}
if is_opcode(a, "OP_NUMEQUAL") && is_opcode(b, "OP_VERIFY") {
return Some(vec![StackOp::Opcode("OP_NUMEQUALVERIFY".to_string())]);
}
if is_opcode(a, "OP_CHECKMULTISIG") && is_opcode(b, "OP_VERIFY") {
return Some(vec![StackOp::Opcode(
"OP_CHECKMULTISIGVERIFY".to_string(),
)]);
}
if is_opcode(a, "OP_DUP") && is_opcode(b, "OP_DROP") {
return Some(vec![]);
}
if matches!(a, StackOp::Over) && matches!(b, StackOp::Over) {
return Some(vec![StackOp::Opcode("OP_2DUP".to_string())]);
}
if matches!(a, StackOp::Drop) && matches!(b, StackOp::Drop) {
return Some(vec![StackOp::Opcode("OP_2DROP".to_string())]);
}
if is_push_int(a, 0) && matches!(b, StackOp::Roll { depth: 0 }) {
return Some(vec![]);
}
if is_push_int(a, 1) && matches!(b, StackOp::Roll { depth: 1 }) {
return Some(vec![StackOp::Swap]);
}
if is_push_int(a, 2) && matches!(b, StackOp::Roll { depth: 2 }) {
return Some(vec![StackOp::Rot]);
}
if is_push_int(a, 0) && matches!(b, StackOp::Pick { depth: 0 }) {
return Some(vec![StackOp::Dup]);
}
if is_push_int(a, 1) && matches!(b, StackOp::Pick { depth: 1 }) {
return Some(vec![StackOp::Over]);
}
if is_push_int(a, 0) && is_opcode(b, "OP_ROLL") {
return Some(vec![]);
}
if is_push_int(a, 1) && is_opcode(b, "OP_ROLL") {
return Some(vec![StackOp::Swap]);
}
if is_push_int(a, 2) && is_opcode(b, "OP_ROLL") {
return Some(vec![StackOp::Rot]);
}
if is_push_int(a, 0) && is_opcode(b, "OP_PICK") {
return Some(vec![StackOp::Dup]);
}
if is_push_int(a, 1) && is_opcode(b, "OP_PICK") {
return Some(vec![StackOp::Over]);
}
if is_opcode(a, "OP_SHA256") && is_opcode(b, "OP_SHA256") {
return Some(vec![StackOp::Opcode("OP_HASH256".to_string())]);
}
if is_push_int(a, 0) && is_opcode(b, "OP_NUMEQUAL") {
return Some(vec![StackOp::Opcode("OP_NOT".to_string())]);
}
None
}
fn is_push_int(op: &StackOp, n: i128) -> bool {
matches!(op, StackOp::Push(PushValue::Int(v)) if *v == n)
}
fn is_opcode(op: &StackOp, code: &str) -> bool {
matches!(op, StackOp::Opcode(c) if c == code)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_swap_swap_removed() {
let ops = vec![StackOp::Swap, StackOp::Swap];
let result = optimize_stack_ops(&ops);
assert!(result.is_empty(), "SWAP SWAP should be eliminated, got {:?}", result);
}
#[test]
fn test_dup_drop_removed() {
let ops = vec![StackOp::Dup, StackOp::Drop];
let result = optimize_stack_ops(&ops);
assert!(result.is_empty(), "DUP DROP should be eliminated, got {:?}", result);
}
#[test]
fn test_push_drop_removed() {
let ops = vec![StackOp::Push(PushValue::Int(42)), StackOp::Drop];
let result = optimize_stack_ops(&ops);
assert!(result.is_empty(), "PUSH DROP should be eliminated, got {:?}", result);
}
#[test]
fn test_not_not_removed() {
let ops = vec![
StackOp::Opcode("OP_NOT".to_string()),
StackOp::Opcode("OP_NOT".to_string()),
];
let result = optimize_stack_ops(&ops);
assert!(result.is_empty(), "NOT NOT should be eliminated, got {:?}", result);
}
#[test]
fn test_negate_negate_removed() {
let ops = vec![
StackOp::Opcode("OP_NEGATE".to_string()),
StackOp::Opcode("OP_NEGATE".to_string()),
];
let result = optimize_stack_ops(&ops);
assert!(result.is_empty(), "NEGATE NEGATE should be eliminated, got {:?}", result);
}
#[test]
fn test_equal_verify_combined() {
let ops = vec![
StackOp::Opcode("OP_EQUAL".to_string()),
StackOp::Opcode("OP_VERIFY".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_EQUALVERIFY"));
}
#[test]
fn test_numequal_verify_combined() {
let ops = vec![
StackOp::Opcode("OP_NUMEQUAL".to_string()),
StackOp::Opcode("OP_VERIFY".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_NUMEQUALVERIFY"));
}
#[test]
fn test_checksig_verify_combined() {
let ops = vec![
StackOp::Opcode("OP_CHECKSIG".to_string()),
StackOp::Opcode("OP_VERIFY".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_CHECKSIGVERIFY"));
}
#[test]
fn test_push1_add_becomes_1add() {
let ops = vec![
StackOp::Push(PushValue::Int(1)),
StackOp::Opcode("OP_ADD".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_1ADD"));
}
#[test]
fn test_push1_sub_becomes_1sub() {
let ops = vec![
StackOp::Push(PushValue::Int(1)),
StackOp::Opcode("OP_SUB".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_1SUB"));
}
#[test]
fn test_push0_add_removed() {
let ops = vec![
StackOp::Push(PushValue::Int(0)),
StackOp::Opcode("OP_ADD".to_string()),
];
let result = optimize_stack_ops(&ops);
assert!(result.is_empty(), "PUSH(0) ADD should be eliminated, got {:?}", result);
}
#[test]
fn test_push0_sub_removed() {
let ops = vec![
StackOp::Push(PushValue::Int(0)),
StackOp::Opcode("OP_SUB".to_string()),
];
let result = optimize_stack_ops(&ops);
assert!(result.is_empty(), "PUSH(0) SUB should be eliminated, got {:?}", result);
}
#[test]
fn test_over_over_becomes_2dup() {
let ops = vec![StackOp::Over, StackOp::Over];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_2DUP"));
}
#[test]
fn test_drop_drop_becomes_2drop() {
let ops = vec![StackOp::Drop, StackOp::Drop];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_2DROP"));
}
#[test]
fn test_sha256_sha256_becomes_hash256() {
let ops = vec![
StackOp::Opcode("OP_SHA256".to_string()),
StackOp::Opcode("OP_SHA256".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_HASH256"));
}
#[test]
fn test_push0_numequal_becomes_not() {
let ops = vec![
StackOp::Push(PushValue::Int(0)),
StackOp::Opcode("OP_NUMEQUAL".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_NOT"));
}
#[test]
fn test_push0_roll_struct_removed() {
let ops = vec![
StackOp::Push(PushValue::Int(0)),
StackOp::Roll { depth: 0 },
];
let result = optimize_stack_ops(&ops);
assert!(result.is_empty(), "PUSH(0) Roll{{0}} should be eliminated, got {:?}", result);
}
#[test]
fn test_push1_roll_struct_becomes_swap() {
let ops = vec![
StackOp::Push(PushValue::Int(1)),
StackOp::Roll { depth: 1 },
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Swap));
}
#[test]
fn test_push2_roll_struct_becomes_rot() {
let ops = vec![
StackOp::Push(PushValue::Int(2)),
StackOp::Roll { depth: 2 },
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Rot));
}
#[test]
fn test_push0_pick_struct_becomes_dup() {
let ops = vec![
StackOp::Push(PushValue::Int(0)),
StackOp::Pick { depth: 0 },
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Dup));
}
#[test]
fn test_push1_pick_struct_becomes_over() {
let ops = vec![
StackOp::Push(PushValue::Int(1)),
StackOp::Pick { depth: 1 },
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Over));
}
#[test]
fn test_push0_opcode_roll_string_removed() {
let ops = vec![
StackOp::Push(PushValue::Int(0)),
StackOp::Opcode("OP_ROLL".to_string()),
];
let result = optimize_stack_ops(&ops);
assert!(result.is_empty(), "PUSH(0) Opcode(OP_ROLL) should be eliminated, got {:?}", result);
}
#[test]
fn test_push1_opcode_roll_string_becomes_swap() {
let ops = vec![
StackOp::Push(PushValue::Int(1)),
StackOp::Opcode("OP_ROLL".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Swap));
}
#[test]
fn test_push2_opcode_roll_string_becomes_rot() {
let ops = vec![
StackOp::Push(PushValue::Int(2)),
StackOp::Opcode("OP_ROLL".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Rot));
}
#[test]
fn test_push0_opcode_pick_string_becomes_dup() {
let ops = vec![
StackOp::Push(PushValue::Int(0)),
StackOp::Opcode("OP_PICK".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Dup));
}
#[test]
fn test_push1_opcode_pick_string_becomes_over() {
let ops = vec![
StackOp::Push(PushValue::Int(1)),
StackOp::Opcode("OP_PICK".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Over));
}
#[test]
fn test_constant_fold_add() {
let ops = vec![
StackOp::Push(PushValue::Int(3)),
StackOp::Push(PushValue::Int(7)),
StackOp::Opcode("OP_ADD".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Push(PushValue::Int(10))));
}
#[test]
fn test_constant_fold_sub() {
let ops = vec![
StackOp::Push(PushValue::Int(10)),
StackOp::Push(PushValue::Int(3)),
StackOp::Opcode("OP_SUB".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Push(PushValue::Int(7))));
}
#[test]
fn test_constant_fold_mul() {
let ops = vec![
StackOp::Push(PushValue::Int(4)),
StackOp::Push(PushValue::Int(5)),
StackOp::Opcode("OP_MUL".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Push(PushValue::Int(20))));
}
#[test]
fn test_chain_fold_add_add() {
let ops = vec![
StackOp::Dup, StackOp::Push(PushValue::Int(3)),
StackOp::Opcode("OP_ADD".to_string()),
StackOp::Push(PushValue::Int(7)),
StackOp::Opcode("OP_ADD".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 3, "expected DUP PUSH(10) ADD, got {:?}", result);
assert!(matches!(&result[0], StackOp::Dup));
assert!(matches!(&result[1], StackOp::Push(PushValue::Int(10))));
assert!(matches!(&result[2], StackOp::Opcode(c) if c == "OP_ADD"));
}
#[test]
fn test_chain_fold_sub_sub() {
let ops = vec![
StackOp::Dup,
StackOp::Push(PushValue::Int(2)),
StackOp::Opcode("OP_SUB".to_string()),
StackOp::Push(PushValue::Int(3)),
StackOp::Opcode("OP_SUB".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 3, "expected DUP PUSH(5) SUB, got {:?}", result);
assert!(matches!(&result[1], StackOp::Push(PushValue::Int(5))));
assert!(matches!(&result[2], StackOp::Opcode(c) if c == "OP_SUB"));
}
#[test]
fn test_non_optimizable_single_op_passthrough() {
let ops = vec![StackOp::Dup];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], StackOp::Dup));
}
#[test]
fn test_non_optimizable_sequence_passthrough() {
let ops = vec![
StackOp::Dup,
StackOp::Opcode("OP_HASH160".to_string()),
StackOp::Opcode("OP_EQUALVERIFY".to_string()),
StackOp::Opcode("OP_CHECKSIG".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 4, "non-optimizable sequence should pass through unchanged");
assert!(matches!(&result[0], StackOp::Dup));
assert!(matches!(&result[1], StackOp::Opcode(c) if c == "OP_HASH160"));
assert!(matches!(&result[2], StackOp::Opcode(c) if c == "OP_EQUALVERIFY"));
assert!(matches!(&result[3], StackOp::Opcode(c) if c == "OP_CHECKSIG"));
}
#[test]
fn test_large_push_with_roll_not_simplified() {
let ops = vec![
StackOp::Push(PushValue::Int(5)),
StackOp::Opcode("OP_ROLL".to_string()),
];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 2, "PUSH(5) OP_ROLL should pass through unchanged");
}
#[test]
fn test_empty_input() {
let ops: Vec<StackOp> = vec![];
let result = optimize_stack_ops(&ops);
assert!(result.is_empty());
}
#[test]
fn test_nested_if_optimized() {
let ops = vec![StackOp::If {
then_ops: vec![
StackOp::Opcode("OP_EQUAL".to_string()),
StackOp::Opcode("OP_VERIFY".to_string()),
],
else_ops: vec![StackOp::Swap, StackOp::Swap],
}];
let result = optimize_stack_ops(&ops);
assert_eq!(result.len(), 1);
if let StackOp::If { then_ops, else_ops } = &result[0] {
assert_eq!(then_ops.len(), 1, "then branch should be optimized");
assert!(matches!(&then_ops[0], StackOp::Opcode(c) if c == "OP_EQUALVERIFY"));
assert!(else_ops.is_empty(), "else branch SWAP SWAP should be eliminated");
} else {
panic!("expected If, got {:?}", result[0]);
}
}
#[test]
fn test_iterative_optimization() {
let ops = vec![
StackOp::Dup,
StackOp::Push(PushValue::Int(0)),
StackOp::Opcode("OP_ADD".to_string()),
StackOp::Drop,
];
let result = optimize_stack_ops(&ops);
assert!(result.is_empty(), "iterative optimization should remove DUP (PUSH0 ADD -> empty) DROP -> DUP DROP -> empty, got {:?}", result);
}
}