1use super::stack::{PushValue, StackOp};
8
9const MAX_ITERATIONS: usize = 100;
10
11pub fn optimize_stack_ops(ops: &[StackOp]) -> Vec<StackOp> {
13 let mut current: Vec<StackOp> = ops.iter().map(optimize_nested_if).collect();
15
16 for _ in 0..MAX_ITERATIONS {
17 let (result, changed) = apply_one_pass(¤t);
18 if !changed {
19 break;
20 }
21 current = result;
22 }
23
24 current
25}
26
27fn optimize_nested_if(op: &StackOp) -> StackOp {
28 match op {
29 StackOp::If {
30 then_ops,
31 else_ops,
32 } => {
33 let optimized_then = optimize_stack_ops(then_ops);
34 let optimized_else = if else_ops.is_empty() {
35 Vec::new()
36 } else {
37 optimize_stack_ops(else_ops)
38 };
39 StackOp::If {
40 then_ops: optimized_then,
41 else_ops: optimized_else,
42 }
43 }
44 other => other.clone(),
45 }
46}
47
48fn apply_one_pass(ops: &[StackOp]) -> (Vec<StackOp>, bool) {
49 let mut result = Vec::new();
50 let mut changed = false;
51 let mut i = 0;
52
53 while i < ops.len() {
54 if i + 3 < ops.len() {
56 if let Some(replacement) =
57 match_window_4(&ops[i], &ops[i + 1], &ops[i + 2], &ops[i + 3])
58 {
59 result.extend(replacement);
60 i += 4;
61 changed = true;
62 continue;
63 }
64 }
65 if i + 2 < ops.len() {
67 if let Some(replacement) = match_window_3(&ops[i], &ops[i + 1], &ops[i + 2]) {
68 result.extend(replacement);
69 i += 3;
70 changed = true;
71 continue;
72 }
73 }
74 if i + 1 < ops.len() {
76 if let Some(replacement) = match_window_2(&ops[i], &ops[i + 1]) {
77 result.extend(replacement);
78 i += 2;
79 changed = true;
80 continue;
81 }
82 }
83
84 result.push(ops[i].clone());
85 i += 1;
86 }
87
88 (result, changed)
89}
90
91fn match_window_3(a: &StackOp, b: &StackOp, c: &StackOp) -> Option<Vec<StackOp>> {
92 if let (StackOp::Push(PushValue::Int(va)), StackOp::Push(PushValue::Int(vb))) = (a, b) {
94 if is_opcode(c, "OP_ADD") {
95 return Some(vec![StackOp::Push(PushValue::Int(va + vb))]);
96 }
97 if is_opcode(c, "OP_SUB") {
98 return Some(vec![StackOp::Push(PushValue::Int(va - vb))]);
99 }
100 if is_opcode(c, "OP_MUL") {
101 return Some(vec![StackOp::Push(PushValue::Int(va * vb))]);
102 }
103 }
104 None
105}
106
107fn match_window_4(
108 a: &StackOp,
109 b: &StackOp,
110 c: &StackOp,
111 d: &StackOp,
112) -> Option<Vec<StackOp>> {
113 if let StackOp::Push(PushValue::Int(va)) = a {
115 if is_opcode(b, "OP_ADD") {
116 if let StackOp::Push(PushValue::Int(vb)) = c {
117 if is_opcode(d, "OP_ADD") {
118 return Some(vec![
119 StackOp::Push(PushValue::Int(va + vb)),
120 StackOp::Opcode("OP_ADD".to_string()),
121 ]);
122 }
123 }
124 }
125 if is_opcode(b, "OP_SUB") {
126 if let StackOp::Push(PushValue::Int(vb)) = c {
127 if is_opcode(d, "OP_SUB") {
128 return Some(vec![
129 StackOp::Push(PushValue::Int(va + vb)),
130 StackOp::Opcode("OP_SUB".to_string()),
131 ]);
132 }
133 }
134 }
135 }
136 None
137}
138
139fn match_window_2(a: &StackOp, b: &StackOp) -> Option<Vec<StackOp>> {
140 if matches!(a, StackOp::Push(_)) && matches!(b, StackOp::Drop) {
142 return Some(vec![]);
143 }
144
145 if matches!(a, StackOp::Dup) && matches!(b, StackOp::Drop) {
147 return Some(vec![]);
148 }
149
150 if matches!(a, StackOp::Swap) && matches!(b, StackOp::Swap) {
152 return Some(vec![]);
153 }
154
155 if is_push_int(a, 1) && is_opcode(b, "OP_ADD") {
157 return Some(vec![StackOp::Opcode("OP_1ADD".to_string())]);
158 }
159
160 if is_push_int(a, 1) && is_opcode(b, "OP_SUB") {
162 return Some(vec![StackOp::Opcode("OP_1SUB".to_string())]);
163 }
164
165 if is_push_int(a, 0) && is_opcode(b, "OP_ADD") {
167 return Some(vec![]);
168 }
169
170 if is_push_int(a, 0) && is_opcode(b, "OP_SUB") {
172 return Some(vec![]);
173 }
174
175 if is_opcode(a, "OP_NOT") && is_opcode(b, "OP_NOT") {
177 return Some(vec![]);
178 }
179
180 if is_opcode(a, "OP_NEGATE") && is_opcode(b, "OP_NEGATE") {
182 return Some(vec![]);
183 }
184
185 if is_opcode(a, "OP_EQUAL") && is_opcode(b, "OP_VERIFY") {
187 return Some(vec![StackOp::Opcode("OP_EQUALVERIFY".to_string())]);
188 }
189
190 if is_opcode(a, "OP_CHECKSIG") && is_opcode(b, "OP_VERIFY") {
192 return Some(vec![StackOp::Opcode("OP_CHECKSIGVERIFY".to_string())]);
193 }
194
195 if is_opcode(a, "OP_NUMEQUAL") && is_opcode(b, "OP_VERIFY") {
197 return Some(vec![StackOp::Opcode("OP_NUMEQUALVERIFY".to_string())]);
198 }
199
200 if is_opcode(a, "OP_CHECKMULTISIG") && is_opcode(b, "OP_VERIFY") {
202 return Some(vec![StackOp::Opcode(
203 "OP_CHECKMULTISIGVERIFY".to_string(),
204 )]);
205 }
206
207 if is_opcode(a, "OP_DUP") && is_opcode(b, "OP_DROP") {
209 return Some(vec![]);
210 }
211
212 if matches!(a, StackOp::Over) && matches!(b, StackOp::Over) {
214 return Some(vec![StackOp::Opcode("OP_2DUP".to_string())]);
215 }
216
217 if matches!(a, StackOp::Drop) && matches!(b, StackOp::Drop) {
219 return Some(vec![StackOp::Opcode("OP_2DROP".to_string())]);
220 }
221
222 if is_push_int(a, 0) && matches!(b, StackOp::Roll { depth: 0 }) {
224 return Some(vec![]);
225 }
226
227 if is_push_int(a, 1) && matches!(b, StackOp::Roll { depth: 1 }) {
229 return Some(vec![StackOp::Swap]);
230 }
231
232 if is_push_int(a, 2) && matches!(b, StackOp::Roll { depth: 2 }) {
234 return Some(vec![StackOp::Rot]);
235 }
236
237 if is_push_int(a, 0) && matches!(b, StackOp::Pick { depth: 0 }) {
239 return Some(vec![StackOp::Dup]);
240 }
241
242 if is_push_int(a, 1) && matches!(b, StackOp::Pick { depth: 1 }) {
244 return Some(vec![StackOp::Over]);
245 }
246
247 if is_push_int(a, 0) && is_opcode(b, "OP_ROLL") {
249 return Some(vec![]);
250 }
251 if is_push_int(a, 1) && is_opcode(b, "OP_ROLL") {
252 return Some(vec![StackOp::Swap]);
253 }
254 if is_push_int(a, 2) && is_opcode(b, "OP_ROLL") {
255 return Some(vec![StackOp::Rot]);
256 }
257 if is_push_int(a, 0) && is_opcode(b, "OP_PICK") {
258 return Some(vec![StackOp::Dup]);
259 }
260 if is_push_int(a, 1) && is_opcode(b, "OP_PICK") {
261 return Some(vec![StackOp::Over]);
262 }
263
264 if is_opcode(a, "OP_SHA256") && is_opcode(b, "OP_SHA256") {
266 return Some(vec![StackOp::Opcode("OP_HASH256".to_string())]);
267 }
268
269 if is_push_int(a, 0) && is_opcode(b, "OP_NUMEQUAL") {
271 return Some(vec![StackOp::Opcode("OP_NOT".to_string())]);
272 }
273
274 None
275}
276
277fn is_push_int(op: &StackOp, n: i128) -> bool {
278 matches!(op, StackOp::Push(PushValue::Int(v)) if *v == n)
279}
280
281fn is_opcode(op: &StackOp, code: &str) -> bool {
282 matches!(op, StackOp::Opcode(c) if c == code)
283}
284
285#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
298 fn test_swap_swap_removed() {
299 let ops = vec![StackOp::Swap, StackOp::Swap];
300 let result = optimize_stack_ops(&ops);
301 assert!(result.is_empty(), "SWAP SWAP should be eliminated, got {:?}", result);
302 }
303
304 #[test]
305 fn test_dup_drop_removed() {
306 let ops = vec![StackOp::Dup, StackOp::Drop];
307 let result = optimize_stack_ops(&ops);
308 assert!(result.is_empty(), "DUP DROP should be eliminated, got {:?}", result);
309 }
310
311 #[test]
312 fn test_push_drop_removed() {
313 let ops = vec![StackOp::Push(PushValue::Int(42)), StackOp::Drop];
314 let result = optimize_stack_ops(&ops);
315 assert!(result.is_empty(), "PUSH DROP should be eliminated, got {:?}", result);
316 }
317
318 #[test]
319 fn test_not_not_removed() {
320 let ops = vec![
321 StackOp::Opcode("OP_NOT".to_string()),
322 StackOp::Opcode("OP_NOT".to_string()),
323 ];
324 let result = optimize_stack_ops(&ops);
325 assert!(result.is_empty(), "NOT NOT should be eliminated, got {:?}", result);
326 }
327
328 #[test]
329 fn test_negate_negate_removed() {
330 let ops = vec![
331 StackOp::Opcode("OP_NEGATE".to_string()),
332 StackOp::Opcode("OP_NEGATE".to_string()),
333 ];
334 let result = optimize_stack_ops(&ops);
335 assert!(result.is_empty(), "NEGATE NEGATE should be eliminated, got {:?}", result);
336 }
337
338 #[test]
339 fn test_equal_verify_combined() {
340 let ops = vec![
341 StackOp::Opcode("OP_EQUAL".to_string()),
342 StackOp::Opcode("OP_VERIFY".to_string()),
343 ];
344 let result = optimize_stack_ops(&ops);
345 assert_eq!(result.len(), 1);
346 assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_EQUALVERIFY"));
347 }
348
349 #[test]
350 fn test_numequal_verify_combined() {
351 let ops = vec![
352 StackOp::Opcode("OP_NUMEQUAL".to_string()),
353 StackOp::Opcode("OP_VERIFY".to_string()),
354 ];
355 let result = optimize_stack_ops(&ops);
356 assert_eq!(result.len(), 1);
357 assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_NUMEQUALVERIFY"));
358 }
359
360 #[test]
361 fn test_checksig_verify_combined() {
362 let ops = vec![
363 StackOp::Opcode("OP_CHECKSIG".to_string()),
364 StackOp::Opcode("OP_VERIFY".to_string()),
365 ];
366 let result = optimize_stack_ops(&ops);
367 assert_eq!(result.len(), 1);
368 assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_CHECKSIGVERIFY"));
369 }
370
371 #[test]
372 fn test_push1_add_becomes_1add() {
373 let ops = vec![
374 StackOp::Push(PushValue::Int(1)),
375 StackOp::Opcode("OP_ADD".to_string()),
376 ];
377 let result = optimize_stack_ops(&ops);
378 assert_eq!(result.len(), 1);
379 assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_1ADD"));
380 }
381
382 #[test]
383 fn test_push1_sub_becomes_1sub() {
384 let ops = vec![
385 StackOp::Push(PushValue::Int(1)),
386 StackOp::Opcode("OP_SUB".to_string()),
387 ];
388 let result = optimize_stack_ops(&ops);
389 assert_eq!(result.len(), 1);
390 assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_1SUB"));
391 }
392
393 #[test]
394 fn test_push0_add_removed() {
395 let ops = vec![
396 StackOp::Push(PushValue::Int(0)),
397 StackOp::Opcode("OP_ADD".to_string()),
398 ];
399 let result = optimize_stack_ops(&ops);
400 assert!(result.is_empty(), "PUSH(0) ADD should be eliminated, got {:?}", result);
401 }
402
403 #[test]
404 fn test_push0_sub_removed() {
405 let ops = vec![
406 StackOp::Push(PushValue::Int(0)),
407 StackOp::Opcode("OP_SUB".to_string()),
408 ];
409 let result = optimize_stack_ops(&ops);
410 assert!(result.is_empty(), "PUSH(0) SUB should be eliminated, got {:?}", result);
411 }
412
413 #[test]
414 fn test_over_over_becomes_2dup() {
415 let ops = vec![StackOp::Over, StackOp::Over];
416 let result = optimize_stack_ops(&ops);
417 assert_eq!(result.len(), 1);
418 assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_2DUP"));
419 }
420
421 #[test]
422 fn test_drop_drop_becomes_2drop() {
423 let ops = vec![StackOp::Drop, StackOp::Drop];
424 let result = optimize_stack_ops(&ops);
425 assert_eq!(result.len(), 1);
426 assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_2DROP"));
427 }
428
429 #[test]
430 fn test_sha256_sha256_becomes_hash256() {
431 let ops = vec![
432 StackOp::Opcode("OP_SHA256".to_string()),
433 StackOp::Opcode("OP_SHA256".to_string()),
434 ];
435 let result = optimize_stack_ops(&ops);
436 assert_eq!(result.len(), 1);
437 assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_HASH256"));
438 }
439
440 #[test]
441 fn test_push0_numequal_becomes_not() {
442 let ops = vec![
443 StackOp::Push(PushValue::Int(0)),
444 StackOp::Opcode("OP_NUMEQUAL".to_string()),
445 ];
446 let result = optimize_stack_ops(&ops);
447 assert_eq!(result.len(), 1);
448 assert!(matches!(&result[0], StackOp::Opcode(c) if c == "OP_NOT"));
449 }
450
451 #[test]
456 fn test_push0_roll_struct_removed() {
457 let ops = vec![
458 StackOp::Push(PushValue::Int(0)),
459 StackOp::Roll { depth: 0 },
460 ];
461 let result = optimize_stack_ops(&ops);
462 assert!(result.is_empty(), "PUSH(0) Roll{{0}} should be eliminated, got {:?}", result);
463 }
464
465 #[test]
466 fn test_push1_roll_struct_becomes_swap() {
467 let ops = vec![
468 StackOp::Push(PushValue::Int(1)),
469 StackOp::Roll { depth: 1 },
470 ];
471 let result = optimize_stack_ops(&ops);
472 assert_eq!(result.len(), 1);
473 assert!(matches!(&result[0], StackOp::Swap));
474 }
475
476 #[test]
477 fn test_push2_roll_struct_becomes_rot() {
478 let ops = vec![
479 StackOp::Push(PushValue::Int(2)),
480 StackOp::Roll { depth: 2 },
481 ];
482 let result = optimize_stack_ops(&ops);
483 assert_eq!(result.len(), 1);
484 assert!(matches!(&result[0], StackOp::Rot));
485 }
486
487 #[test]
488 fn test_push0_pick_struct_becomes_dup() {
489 let ops = vec![
490 StackOp::Push(PushValue::Int(0)),
491 StackOp::Pick { depth: 0 },
492 ];
493 let result = optimize_stack_ops(&ops);
494 assert_eq!(result.len(), 1);
495 assert!(matches!(&result[0], StackOp::Dup));
496 }
497
498 #[test]
499 fn test_push1_pick_struct_becomes_over() {
500 let ops = vec![
501 StackOp::Push(PushValue::Int(1)),
502 StackOp::Pick { depth: 1 },
503 ];
504 let result = optimize_stack_ops(&ops);
505 assert_eq!(result.len(), 1);
506 assert!(matches!(&result[0], StackOp::Over));
507 }
508
509 #[test]
514 fn test_push0_opcode_roll_string_removed() {
515 let ops = vec![
516 StackOp::Push(PushValue::Int(0)),
517 StackOp::Opcode("OP_ROLL".to_string()),
518 ];
519 let result = optimize_stack_ops(&ops);
520 assert!(result.is_empty(), "PUSH(0) Opcode(OP_ROLL) should be eliminated, got {:?}", result);
521 }
522
523 #[test]
524 fn test_push1_opcode_roll_string_becomes_swap() {
525 let ops = vec![
526 StackOp::Push(PushValue::Int(1)),
527 StackOp::Opcode("OP_ROLL".to_string()),
528 ];
529 let result = optimize_stack_ops(&ops);
530 assert_eq!(result.len(), 1);
531 assert!(matches!(&result[0], StackOp::Swap));
532 }
533
534 #[test]
535 fn test_push2_opcode_roll_string_becomes_rot() {
536 let ops = vec![
537 StackOp::Push(PushValue::Int(2)),
538 StackOp::Opcode("OP_ROLL".to_string()),
539 ];
540 let result = optimize_stack_ops(&ops);
541 assert_eq!(result.len(), 1);
542 assert!(matches!(&result[0], StackOp::Rot));
543 }
544
545 #[test]
546 fn test_push0_opcode_pick_string_becomes_dup() {
547 let ops = vec![
548 StackOp::Push(PushValue::Int(0)),
549 StackOp::Opcode("OP_PICK".to_string()),
550 ];
551 let result = optimize_stack_ops(&ops);
552 assert_eq!(result.len(), 1);
553 assert!(matches!(&result[0], StackOp::Dup));
554 }
555
556 #[test]
557 fn test_push1_opcode_pick_string_becomes_over() {
558 let ops = vec![
559 StackOp::Push(PushValue::Int(1)),
560 StackOp::Opcode("OP_PICK".to_string()),
561 ];
562 let result = optimize_stack_ops(&ops);
563 assert_eq!(result.len(), 1);
564 assert!(matches!(&result[0], StackOp::Over));
565 }
566
567 #[test]
572 fn test_constant_fold_add() {
573 let ops = vec![
574 StackOp::Push(PushValue::Int(3)),
575 StackOp::Push(PushValue::Int(7)),
576 StackOp::Opcode("OP_ADD".to_string()),
577 ];
578 let result = optimize_stack_ops(&ops);
579 assert_eq!(result.len(), 1);
580 assert!(matches!(&result[0], StackOp::Push(PushValue::Int(10))));
581 }
582
583 #[test]
584 fn test_constant_fold_sub() {
585 let ops = vec![
586 StackOp::Push(PushValue::Int(10)),
587 StackOp::Push(PushValue::Int(3)),
588 StackOp::Opcode("OP_SUB".to_string()),
589 ];
590 let result = optimize_stack_ops(&ops);
591 assert_eq!(result.len(), 1);
592 assert!(matches!(&result[0], StackOp::Push(PushValue::Int(7))));
593 }
594
595 #[test]
596 fn test_constant_fold_mul() {
597 let ops = vec![
598 StackOp::Push(PushValue::Int(4)),
599 StackOp::Push(PushValue::Int(5)),
600 StackOp::Opcode("OP_MUL".to_string()),
601 ];
602 let result = optimize_stack_ops(&ops);
603 assert_eq!(result.len(), 1);
604 assert!(matches!(&result[0], StackOp::Push(PushValue::Int(20))));
605 }
606
607 #[test]
612 fn test_chain_fold_add_add() {
613 let ops = vec![
615 StackOp::Dup, StackOp::Push(PushValue::Int(3)),
617 StackOp::Opcode("OP_ADD".to_string()),
618 StackOp::Push(PushValue::Int(7)),
619 StackOp::Opcode("OP_ADD".to_string()),
620 ];
621 let result = optimize_stack_ops(&ops);
622 assert_eq!(result.len(), 3, "expected DUP PUSH(10) ADD, got {:?}", result);
623 assert!(matches!(&result[0], StackOp::Dup));
624 assert!(matches!(&result[1], StackOp::Push(PushValue::Int(10))));
625 assert!(matches!(&result[2], StackOp::Opcode(c) if c == "OP_ADD"));
626 }
627
628 #[test]
629 fn test_chain_fold_sub_sub() {
630 let ops = vec![
631 StackOp::Dup,
632 StackOp::Push(PushValue::Int(2)),
633 StackOp::Opcode("OP_SUB".to_string()),
634 StackOp::Push(PushValue::Int(3)),
635 StackOp::Opcode("OP_SUB".to_string()),
636 ];
637 let result = optimize_stack_ops(&ops);
638 assert_eq!(result.len(), 3, "expected DUP PUSH(5) SUB, got {:?}", result);
639 assert!(matches!(&result[1], StackOp::Push(PushValue::Int(5))));
640 assert!(matches!(&result[2], StackOp::Opcode(c) if c == "OP_SUB"));
641 }
642
643 #[test]
648 fn test_non_optimizable_single_op_passthrough() {
649 let ops = vec![StackOp::Dup];
650 let result = optimize_stack_ops(&ops);
651 assert_eq!(result.len(), 1);
652 assert!(matches!(&result[0], StackOp::Dup));
653 }
654
655 #[test]
656 fn test_non_optimizable_sequence_passthrough() {
657 let ops = vec![
658 StackOp::Dup,
659 StackOp::Opcode("OP_HASH160".to_string()),
660 StackOp::Opcode("OP_EQUALVERIFY".to_string()),
661 StackOp::Opcode("OP_CHECKSIG".to_string()),
662 ];
663 let result = optimize_stack_ops(&ops);
664 assert_eq!(result.len(), 4, "non-optimizable sequence should pass through unchanged");
665 assert!(matches!(&result[0], StackOp::Dup));
666 assert!(matches!(&result[1], StackOp::Opcode(c) if c == "OP_HASH160"));
667 assert!(matches!(&result[2], StackOp::Opcode(c) if c == "OP_EQUALVERIFY"));
668 assert!(matches!(&result[3], StackOp::Opcode(c) if c == "OP_CHECKSIG"));
669 }
670
671 #[test]
672 fn test_large_push_with_roll_not_simplified() {
673 let ops = vec![
675 StackOp::Push(PushValue::Int(5)),
676 StackOp::Opcode("OP_ROLL".to_string()),
677 ];
678 let result = optimize_stack_ops(&ops);
679 assert_eq!(result.len(), 2, "PUSH(5) OP_ROLL should pass through unchanged");
680 }
681
682 #[test]
683 fn test_empty_input() {
684 let ops: Vec<StackOp> = vec![];
685 let result = optimize_stack_ops(&ops);
686 assert!(result.is_empty());
687 }
688
689 #[test]
694 fn test_nested_if_optimized() {
695 let ops = vec![StackOp::If {
696 then_ops: vec![
697 StackOp::Opcode("OP_EQUAL".to_string()),
698 StackOp::Opcode("OP_VERIFY".to_string()),
699 ],
700 else_ops: vec![StackOp::Swap, StackOp::Swap],
701 }];
702 let result = optimize_stack_ops(&ops);
703 assert_eq!(result.len(), 1);
704 if let StackOp::If { then_ops, else_ops } = &result[0] {
705 assert_eq!(then_ops.len(), 1, "then branch should be optimized");
706 assert!(matches!(&then_ops[0], StackOp::Opcode(c) if c == "OP_EQUALVERIFY"));
707 assert!(else_ops.is_empty(), "else branch SWAP SWAP should be eliminated");
708 } else {
709 panic!("expected If, got {:?}", result[0]);
710 }
711 }
712
713 #[test]
718 fn test_iterative_optimization() {
719 let ops = vec![
722 StackOp::Dup,
723 StackOp::Push(PushValue::Int(0)),
724 StackOp::Opcode("OP_ADD".to_string()),
725 StackOp::Drop,
726 ];
727 let result = optimize_stack_ops(&ops);
728 assert!(result.is_empty(), "iterative optimization should remove DUP (PUSH0 ADD -> empty) DROP -> DUP DROP -> empty, got {:?}", result);
729 }
730}