Skip to main content

runar_compiler_rust/codegen/
optimizer.rs

1//! Peephole optimizer -- runs on Stack IR before emission.
2//!
3//! Scans for short sequences of stack operations that can be replaced with
4//! fewer or cheaper opcodes. Applies rules iteratively until a fixed point
5//! is reached. Mirrors the TypeScript peephole optimizer.
6
7use super::stack::{PushValue, StackOp};
8
9const MAX_ITERATIONS: usize = 100;
10
11/// Apply peephole optimization to a list of stack ops.
12pub fn optimize_stack_ops(ops: &[StackOp]) -> Vec<StackOp> {
13    // First, recursively optimize nested if-blocks
14    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(&current);
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        // Try 4-op window
55        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        // Try 3-op window
66        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        // Existing 2-op window
75        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    // Constant folding: PUSH(a) PUSH(b) ADD → PUSH(a+b)
93    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    // Chain folding: PUSH(a) ADD PUSH(b) ADD → PUSH(a+b) ADD
114    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    // PUSH x, DROP -> remove both
141    if matches!(a, StackOp::Push(_)) && matches!(b, StackOp::Drop) {
142        return Some(vec![]);
143    }
144
145    // DUP, DROP -> remove both
146    if matches!(a, StackOp::Dup) && matches!(b, StackOp::Drop) {
147        return Some(vec![]);
148    }
149
150    // SWAP, SWAP -> remove both
151    if matches!(a, StackOp::Swap) && matches!(b, StackOp::Swap) {
152        return Some(vec![]);
153    }
154
155    // PUSH 1, OP_ADD -> OP_1ADD
156    if is_push_int(a, 1) && is_opcode(b, "OP_ADD") {
157        return Some(vec![StackOp::Opcode("OP_1ADD".to_string())]);
158    }
159
160    // PUSH 1, OP_SUB -> OP_1SUB
161    if is_push_int(a, 1) && is_opcode(b, "OP_SUB") {
162        return Some(vec![StackOp::Opcode("OP_1SUB".to_string())]);
163    }
164
165    // PUSH 0, OP_ADD -> remove both
166    if is_push_int(a, 0) && is_opcode(b, "OP_ADD") {
167        return Some(vec![]);
168    }
169
170    // PUSH 0, OP_SUB -> remove both
171    if is_push_int(a, 0) && is_opcode(b, "OP_SUB") {
172        return Some(vec![]);
173    }
174
175    // OP_NOT, OP_NOT -> remove both
176    if is_opcode(a, "OP_NOT") && is_opcode(b, "OP_NOT") {
177        return Some(vec![]);
178    }
179
180    // OP_NEGATE, OP_NEGATE -> remove both
181    if is_opcode(a, "OP_NEGATE") && is_opcode(b, "OP_NEGATE") {
182        return Some(vec![]);
183    }
184
185    // OP_EQUAL, OP_VERIFY -> OP_EQUALVERIFY
186    if is_opcode(a, "OP_EQUAL") && is_opcode(b, "OP_VERIFY") {
187        return Some(vec![StackOp::Opcode("OP_EQUALVERIFY".to_string())]);
188    }
189
190    // OP_CHECKSIG, OP_VERIFY -> OP_CHECKSIGVERIFY
191    if is_opcode(a, "OP_CHECKSIG") && is_opcode(b, "OP_VERIFY") {
192        return Some(vec![StackOp::Opcode("OP_CHECKSIGVERIFY".to_string())]);
193    }
194
195    // OP_NUMEQUAL, OP_VERIFY -> OP_NUMEQUALVERIFY
196    if is_opcode(a, "OP_NUMEQUAL") && is_opcode(b, "OP_VERIFY") {
197        return Some(vec![StackOp::Opcode("OP_NUMEQUALVERIFY".to_string())]);
198    }
199
200    // OP_CHECKMULTISIG, OP_VERIFY -> OP_CHECKMULTISIGVERIFY
201    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    // OP_DUP, OP_DROP -> remove both
208    if is_opcode(a, "OP_DUP") && is_opcode(b, "OP_DROP") {
209        return Some(vec![]);
210    }
211
212    // OP_OVER, OP_OVER -> OP_2DUP
213    if matches!(a, StackOp::Over) && matches!(b, StackOp::Over) {
214        return Some(vec![StackOp::Opcode("OP_2DUP".to_string())]);
215    }
216
217    // OP_DROP, OP_DROP -> OP_2DROP
218    if matches!(a, StackOp::Drop) && matches!(b, StackOp::Drop) {
219        return Some(vec![StackOp::Opcode("OP_2DROP".to_string())]);
220    }
221
222    // PUSH(0) + Roll{depth:0} → remove both
223    if is_push_int(a, 0) && matches!(b, StackOp::Roll { depth: 0 }) {
224        return Some(vec![]);
225    }
226
227    // PUSH(1) + Roll{depth:1} → Swap
228    if is_push_int(a, 1) && matches!(b, StackOp::Roll { depth: 1 }) {
229        return Some(vec![StackOp::Swap]);
230    }
231
232    // PUSH(2) + Roll{depth:2} → Rot
233    if is_push_int(a, 2) && matches!(b, StackOp::Roll { depth: 2 }) {
234        return Some(vec![StackOp::Rot]);
235    }
236
237    // PUSH(0) + Pick{depth:0} → Dup
238    if is_push_int(a, 0) && matches!(b, StackOp::Pick { depth: 0 }) {
239        return Some(vec![StackOp::Dup]);
240    }
241
242    // PUSH(1) + Pick{depth:1} → Over
243    if is_push_int(a, 1) && matches!(b, StackOp::Pick { depth: 1 }) {
244        return Some(vec![StackOp::Over]);
245    }
246
247    // Also match Push + Opcode("OP_ROLL"/"OP_PICK") (used by SLH-DSA codegen)
248    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    // SHA256 + SHA256 → HASH256
265    if is_opcode(a, "OP_SHA256") && is_opcode(b, "OP_SHA256") {
266        return Some(vec![StackOp::Opcode("OP_HASH256".to_string())]);
267    }
268
269    // PUSH 0 + NUMEQUAL → NOT
270    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// ---------------------------------------------------------------------------
286// Tests
287// ---------------------------------------------------------------------------
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    // -----------------------------------------------------------------------
294    // 2-op optimizations
295    // -----------------------------------------------------------------------
296
297    #[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    // -----------------------------------------------------------------------
452    // PUSH + ROLL/PICK typed struct form
453    // -----------------------------------------------------------------------
454
455    #[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    // -----------------------------------------------------------------------
510    // Opcode("OP_ROLL") / Opcode("OP_PICK") string form (SLH-DSA codegen)
511    // -----------------------------------------------------------------------
512
513    #[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    // -----------------------------------------------------------------------
568    // 3-op optimizations (constant folding)
569    // -----------------------------------------------------------------------
570
571    #[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    // -----------------------------------------------------------------------
608    // 4-op optimizations (chain folding)
609    // -----------------------------------------------------------------------
610
611    #[test]
612    fn test_chain_fold_add_add() {
613        // x PUSH(3) ADD PUSH(7) ADD -> x PUSH(10) ADD
614        let ops = vec![
615            StackOp::Dup, // stand-in for some value on stack
616            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    // -----------------------------------------------------------------------
644    // Non-optimizable sequences pass through unchanged
645    // -----------------------------------------------------------------------
646
647    #[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        // PUSH(5) + OP_ROLL should NOT be simplified (only 0/1/2 are special-cased)
674        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    // -----------------------------------------------------------------------
690    // Nested IF optimization
691    // -----------------------------------------------------------------------
692
693    #[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    // -----------------------------------------------------------------------
714    // Iterative convergence
715    // -----------------------------------------------------------------------
716
717    #[test]
718    fn test_iterative_optimization() {
719        // After first pass: PUSH(0) ADD is removed, leaving DUP DROP which is
720        // removed on the second pass
721        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}