Skip to main content

runar_compiler_rust/codegen/
emit.rs

1//! Pass 6: Emit -- converts Stack IR to Bitcoin Script bytes (hex string).
2//!
3//! Walks the StackOp list and encodes each operation as one or more Bitcoin
4//! Script opcodes, producing both a hex-encoded script and a human-readable
5//! ASM representation.
6
7use serde::{Deserialize, Serialize};
8
9use super::opcodes::opcode_byte;
10use super::stack::{PushValue, StackMethod, StackOp};
11
12// ---------------------------------------------------------------------------
13// ConstructorSlot
14// ---------------------------------------------------------------------------
15
16/// Records the byte offset of a constructor parameter placeholder in the
17/// emitted script. The SDK uses these offsets to splice in real values at
18/// deployment time.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ConstructorSlot {
21    #[serde(rename = "paramIndex")]
22    pub param_index: usize,
23    #[serde(rename = "byteOffset")]
24    pub byte_offset: usize,
25}
26
27// ---------------------------------------------------------------------------
28// EmitResult
29// ---------------------------------------------------------------------------
30
31/// The output of the emission pass.
32#[derive(Debug, Clone)]
33pub struct EmitResult {
34    pub script_hex: String,
35    pub script_asm: String,
36    pub constructor_slots: Vec<ConstructorSlot>,
37    pub code_separator_index: i64,
38    pub code_separator_indices: Vec<usize>,
39}
40
41// ---------------------------------------------------------------------------
42// Emit context
43// ---------------------------------------------------------------------------
44
45struct EmitContext {
46    hex_parts: Vec<String>,
47    asm_parts: Vec<String>,
48    byte_length: usize,
49    constructor_slots: Vec<ConstructorSlot>,
50    code_separator_index: i64,
51    code_separator_indices: Vec<usize>,
52}
53
54impl EmitContext {
55    fn new() -> Self {
56        EmitContext {
57            hex_parts: Vec::new(),
58            asm_parts: Vec::new(),
59            byte_length: 0,
60            constructor_slots: Vec::new(),
61            code_separator_index: -1,
62            code_separator_indices: Vec::new(),
63        }
64    }
65
66    fn append_hex(&mut self, hex: &str) {
67        self.byte_length += hex.len() / 2;
68        self.hex_parts.push(hex.to_string());
69    }
70
71    fn emit_opcode(&mut self, name: &str) -> Result<(), String> {
72        let byte = opcode_byte(name)
73            .ok_or_else(|| format!("unknown opcode: {}", name))?;
74        if name == "OP_CODESEPARATOR" {
75            self.code_separator_index = self.byte_length as i64;
76            self.code_separator_indices.push(self.byte_length);
77        }
78        self.append_hex(&format!("{:02x}", byte));
79        self.asm_parts.push(name.to_string());
80        Ok(())
81    }
82
83    fn emit_push(&mut self, value: &PushValue) {
84        let (h, a) = encode_push_value(value);
85        self.append_hex(&h);
86        self.asm_parts.push(a);
87    }
88
89    fn emit_placeholder(&mut self, param_index: usize, _param_name: &str) {
90        let byte_offset = self.byte_length;
91        self.append_hex("00"); // OP_0 placeholder byte
92        self.asm_parts.push("OP_0".to_string());
93        self.constructor_slots.push(ConstructorSlot {
94            param_index,
95            byte_offset,
96        });
97    }
98
99    fn get_hex(&self) -> String {
100        self.hex_parts.join("")
101    }
102
103    fn get_asm(&self) -> String {
104        self.asm_parts.join(" ")
105    }
106}
107
108// ---------------------------------------------------------------------------
109// Script number encoding
110// ---------------------------------------------------------------------------
111
112/// Encode an i128 as a Bitcoin Script number (little-endian, sign-magnitude).
113/// Bitcoin Script numbers can be up to 2^252, so i128 is needed.
114pub fn encode_script_number(n: i128) -> Vec<u8> {
115    if n == 0 {
116        return Vec::new();
117    }
118
119    let negative = n < 0;
120    let mut abs = if negative { (-n) as u128 } else { n as u128 };
121
122    let mut bytes = Vec::new();
123    while abs > 0 {
124        bytes.push((abs & 0xff) as u8);
125        abs >>= 8;
126    }
127
128    let last_byte = *bytes.last().unwrap();
129    if last_byte & 0x80 != 0 {
130        bytes.push(if negative { 0x80 } else { 0x00 });
131    } else if negative {
132        let len = bytes.len();
133        bytes[len - 1] = last_byte | 0x80;
134    }
135
136    bytes
137}
138
139// ---------------------------------------------------------------------------
140// Push data encoding
141// ---------------------------------------------------------------------------
142
143/// Encode raw bytes as a Bitcoin Script push-data operation.
144pub fn encode_push_data(data: &[u8]) -> Vec<u8> {
145    let len = data.len();
146
147    if len == 0 {
148        return vec![0x00]; // OP_0
149    }
150
151    // MINIMALDATA: single-byte values 1-16 must use OP_1..OP_16, 0x81 must use OP_1NEGATE.
152    // Note: 0x00 is NOT converted to OP_0 because OP_0 pushes empty [] not [0x00].
153    if len == 1 {
154        let b = data[0];
155        if b >= 1 && b <= 16 {
156            return vec![0x50 + b]; // OP_1 through OP_16
157        }
158        if b == 0x81 {
159            return vec![0x4f]; // OP_1NEGATE
160        }
161    }
162
163    if len <= 75 {
164        let mut result = vec![len as u8];
165        result.extend_from_slice(data);
166        return result;
167    }
168
169    if len <= 255 {
170        let mut result = vec![0x4c, len as u8]; // OP_PUSHDATA1
171        result.extend_from_slice(data);
172        return result;
173    }
174
175    if len <= 65535 {
176        let mut result = vec![0x4d, (len & 0xff) as u8, ((len >> 8) & 0xff) as u8]; // OP_PUSHDATA2
177        result.extend_from_slice(data);
178        return result;
179    }
180
181    // OP_PUSHDATA4
182    let mut result = vec![
183        0x4e,
184        (len & 0xff) as u8,
185        ((len >> 8) & 0xff) as u8,
186        ((len >> 16) & 0xff) as u8,
187        ((len >> 24) & 0xff) as u8,
188    ];
189    result.extend_from_slice(data);
190    result
191}
192
193/// Encode a push value to hex and asm strings.
194fn encode_push_value(value: &PushValue) -> (String, String) {
195    match value {
196        PushValue::Bool(b) => {
197            if *b {
198                ("51".to_string(), "OP_TRUE".to_string())
199            } else {
200                ("00".to_string(), "OP_FALSE".to_string())
201            }
202        }
203        PushValue::Int(n) => encode_push_int(*n),
204        PushValue::Bytes(bytes) => {
205            let encoded = encode_push_data(bytes);
206            let h = hex::encode(&encoded);
207            if bytes.is_empty() {
208                (h, "OP_0".to_string())
209            } else {
210                (h, format!("<{}>", hex::encode(bytes)))
211            }
212        }
213    }
214}
215
216/// Encode an integer push, using small-integer opcodes where possible.
217pub fn encode_push_int(n: i128) -> (String, String) {
218    if n == 0 {
219        return ("00".to_string(), "OP_0".to_string());
220    }
221
222    if n == -1 {
223        return ("4f".to_string(), "OP_1NEGATE".to_string());
224    }
225
226    if n >= 1 && n <= 16 {
227        let opcode = 0x50 + n as u8;
228        return (format!("{:02x}", opcode), format!("OP_{}", n));
229    }
230
231    let num_bytes = encode_script_number(n);
232    let encoded = encode_push_data(&num_bytes);
233    (hex::encode(&encoded), format!("<{}>", hex::encode(&num_bytes)))
234}
235
236// ---------------------------------------------------------------------------
237// Emit a single StackOp
238// ---------------------------------------------------------------------------
239
240fn emit_stack_op(op: &StackOp, ctx: &mut EmitContext) -> Result<(), String> {
241    match op {
242        StackOp::Push(value) => {
243            ctx.emit_push(value);
244            Ok(())
245        }
246        StackOp::Dup => ctx.emit_opcode("OP_DUP"),
247        StackOp::Swap => ctx.emit_opcode("OP_SWAP"),
248        StackOp::Roll { .. } => ctx.emit_opcode("OP_ROLL"),
249        StackOp::Pick { .. } => ctx.emit_opcode("OP_PICK"),
250        StackOp::Drop => ctx.emit_opcode("OP_DROP"),
251        StackOp::Nip => ctx.emit_opcode("OP_NIP"),
252        StackOp::Over => ctx.emit_opcode("OP_OVER"),
253        StackOp::Rot => ctx.emit_opcode("OP_ROT"),
254        StackOp::Tuck => ctx.emit_opcode("OP_TUCK"),
255        StackOp::Opcode(code) => ctx.emit_opcode(code),
256        StackOp::If {
257            then_ops,
258            else_ops,
259        } => emit_if(then_ops, else_ops, ctx),
260        StackOp::Placeholder {
261            param_index,
262            param_name,
263        } => {
264            ctx.emit_placeholder(*param_index, param_name);
265            Ok(())
266        }
267    }
268}
269
270fn emit_if(
271    then_ops: &[StackOp],
272    else_ops: &[StackOp],
273    ctx: &mut EmitContext,
274) -> Result<(), String> {
275    ctx.emit_opcode("OP_IF")?;
276
277    for op in then_ops {
278        emit_stack_op(op, ctx)?;
279    }
280
281    if !else_ops.is_empty() {
282        ctx.emit_opcode("OP_ELSE")?;
283        for op in else_ops {
284            emit_stack_op(op, ctx)?;
285        }
286    }
287
288    ctx.emit_opcode("OP_ENDIF")
289}
290
291// ---------------------------------------------------------------------------
292// Peephole optimization
293// ---------------------------------------------------------------------------
294
295// ---------------------------------------------------------------------------
296// Public API
297// ---------------------------------------------------------------------------
298
299/// Emit a slice of StackMethods as Bitcoin Script hex and ASM.
300///
301/// For contracts with multiple public methods, generates a method dispatch
302/// preamble using OP_IF/OP_ELSE chains.
303///
304/// Note: peephole optimization (VERIFY combinations, SWAP elimination) is
305/// handled by `optimize_stack_ops` in optimizer.rs, which runs before emit.
306pub fn emit(methods: &[StackMethod]) -> Result<EmitResult, String> {
307    let mut ctx = EmitContext::new();
308
309    // Filter to public methods (exclude constructor)
310    let public_methods: Vec<StackMethod> = methods
311        .iter()
312        .filter(|m| m.name != "constructor")
313        .cloned()
314        .collect();
315
316    if public_methods.is_empty() {
317        return Ok(EmitResult {
318            script_hex: String::new(),
319            script_asm: String::new(),
320            constructor_slots: Vec::new(),
321            code_separator_index: -1,
322            code_separator_indices: Vec::new(),
323        });
324    }
325
326    if public_methods.len() == 1 {
327        for op in &public_methods[0].ops {
328            emit_stack_op(op, &mut ctx)?;
329        }
330    } else {
331        let refs: Vec<&StackMethod> = public_methods.iter().collect();
332        emit_method_dispatch(&refs, &mut ctx)?;
333    }
334
335    Ok(EmitResult {
336        script_hex: ctx.get_hex(),
337        script_asm: ctx.get_asm(),
338        constructor_slots: ctx.constructor_slots,
339        code_separator_index: ctx.code_separator_index,
340        code_separator_indices: ctx.code_separator_indices,
341    })
342}
343
344fn emit_method_dispatch(
345    methods: &[&StackMethod],
346    ctx: &mut EmitContext,
347) -> Result<(), String> {
348    for (i, method) in methods.iter().enumerate() {
349        let is_last = i == methods.len() - 1;
350
351        if !is_last {
352            ctx.emit_opcode("OP_DUP")?;
353            ctx.emit_push(&PushValue::Int(i as i128));
354            ctx.emit_opcode("OP_NUMEQUAL")?;
355            ctx.emit_opcode("OP_IF")?;
356            ctx.emit_opcode("OP_DROP")?;
357        } else {
358            // Last method — verify the index matches (fail-closed for invalid selectors)
359            ctx.emit_push(&PushValue::Int(i as i128));
360            ctx.emit_opcode("OP_NUMEQUALVERIFY")?;
361        }
362
363        for op in &method.ops {
364            emit_stack_op(op, ctx)?;
365        }
366
367        if !is_last {
368            ctx.emit_opcode("OP_ELSE")?;
369        }
370    }
371
372    // Close nested OP_IF/OP_ELSE blocks
373    for _ in 0..methods.len() - 1 {
374        ctx.emit_opcode("OP_ENDIF")?;
375    }
376
377    Ok(())
378}
379
380/// Emit a single method's ops. Useful for testing.
381pub fn emit_method(method: &StackMethod) -> Result<EmitResult, String> {
382    let mut ctx = EmitContext::new();
383    for op in &method.ops {
384        emit_stack_op(op, &mut ctx)?;
385    }
386    Ok(EmitResult {
387        script_hex: ctx.get_hex(),
388        script_asm: ctx.get_asm(),
389        constructor_slots: ctx.constructor_slots,
390        code_separator_index: ctx.code_separator_index,
391        code_separator_indices: ctx.code_separator_indices,
392    })
393}
394
395// ---------------------------------------------------------------------------
396// Tests
397// ---------------------------------------------------------------------------
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn test_emit_placeholder_produces_constructor_slot() {
405        let method = StackMethod {
406            name: "unlock".to_string(),
407            ops: vec![StackOp::Placeholder {
408                param_index: 0,
409                param_name: "pubKeyHash".to_string(),
410            }],
411            max_stack_depth: 1,
412        };
413
414        let result = emit_method(&method).expect("emit should succeed");
415        assert_eq!(
416            result.constructor_slots.len(),
417            1,
418            "should produce exactly one constructor slot"
419        );
420        assert_eq!(result.constructor_slots[0].param_index, 0);
421        assert_eq!(result.constructor_slots[0].byte_offset, 0);
422    }
423
424    #[test]
425    fn test_multiple_placeholders_produce_distinct_byte_offsets() {
426        let method = StackMethod {
427            name: "test".to_string(),
428            ops: vec![
429                StackOp::Placeholder {
430                    param_index: 0,
431                    param_name: "a".to_string(),
432                },
433                StackOp::Placeholder {
434                    param_index: 1,
435                    param_name: "b".to_string(),
436                },
437            ],
438            max_stack_depth: 2,
439        };
440
441        let result = emit_method(&method).expect("emit should succeed");
442        assert_eq!(
443            result.constructor_slots.len(),
444            2,
445            "should produce two constructor slots"
446        );
447
448        // First placeholder at byte 0
449        assert_eq!(result.constructor_slots[0].param_index, 0);
450        assert_eq!(result.constructor_slots[0].byte_offset, 0);
451
452        // Second placeholder at byte 1 (after the first OP_0 byte)
453        assert_eq!(result.constructor_slots[1].param_index, 1);
454        assert_eq!(result.constructor_slots[1].byte_offset, 1);
455
456        // Byte offsets should be distinct
457        assert_ne!(
458            result.constructor_slots[0].byte_offset,
459            result.constructor_slots[1].byte_offset
460        );
461    }
462
463    #[test]
464    fn test_placeholder_byte_offset_position_is_op_0() {
465        let method = StackMethod {
466            name: "test".to_string(),
467            ops: vec![
468                StackOp::Push(PushValue::Int(42)), // some bytes before
469                StackOp::Placeholder {
470                    param_index: 0,
471                    param_name: "x".to_string(),
472                },
473            ],
474            max_stack_depth: 2,
475        };
476
477        let result = emit_method(&method).expect("emit should succeed");
478        assert_eq!(result.constructor_slots.len(), 1);
479
480        let slot = &result.constructor_slots[0];
481        let hex = &result.script_hex;
482
483        // The byte at the placeholder offset should be "00" (OP_0)
484        let byte_hex = &hex[slot.byte_offset * 2..slot.byte_offset * 2 + 2];
485        assert_eq!(
486            byte_hex, "00",
487            "expected OP_0 at placeholder byte offset {}, got '{}' in hex '{}'",
488            slot.byte_offset, byte_hex, hex
489        );
490    }
491
492    #[test]
493    fn test_emit_single_method_produces_hex_and_asm() {
494        use super::super::optimizer::optimize_stack_ops;
495
496        let method = StackMethod {
497            name: "check".to_string(),
498            ops: vec![
499                StackOp::Push(PushValue::Int(42)),
500                StackOp::Opcode("OP_NUMEQUAL".to_string()),
501                StackOp::Opcode("OP_VERIFY".to_string()),
502            ],
503            max_stack_depth: 1,
504        };
505
506        // Apply peephole optimization before emit (as the compiler pipeline does)
507        let optimized_method = StackMethod {
508            name: method.name.clone(),
509            ops: optimize_stack_ops(&method.ops),
510            max_stack_depth: method.max_stack_depth,
511        };
512
513        let result = emit(&[optimized_method]).expect("emit should succeed");
514        assert!(!result.script_hex.is_empty(), "hex should not be empty");
515        assert!(!result.script_asm.is_empty(), "asm should not be empty");
516        assert!(
517            result.script_asm.contains("OP_NUMEQUALVERIFY"),
518            "standalone peephole optimizer should combine OP_NUMEQUAL + OP_VERIFY into OP_NUMEQUALVERIFY, got: {}",
519            result.script_asm
520        );
521    }
522
523    #[test]
524    fn test_emit_empty_methods_produces_empty_output() {
525        let result = emit(&[]).expect("emit with no methods should succeed");
526        assert!(
527            result.script_hex.is_empty(),
528            "empty methods should produce empty hex"
529        );
530        assert!(
531            result.constructor_slots.is_empty(),
532            "empty methods should produce no constructor slots"
533        );
534    }
535
536    #[test]
537    fn test_emit_push_bool_values() {
538        let method = StackMethod {
539            name: "test".to_string(),
540            ops: vec![
541                StackOp::Push(PushValue::Bool(true)),
542                StackOp::Push(PushValue::Bool(false)),
543            ],
544            max_stack_depth: 2,
545        };
546
547        let result = emit_method(&method).expect("emit should succeed");
548        // OP_TRUE = 0x51, OP_FALSE = 0x00
549        assert!(
550            result.script_hex.starts_with("51"),
551            "true should emit 0x51, got: {}",
552            result.script_hex
553        );
554        assert!(
555            result.script_hex.ends_with("00"),
556            "false should emit 0x00, got: {}",
557            result.script_hex
558        );
559        assert!(result.script_asm.contains("OP_TRUE"));
560        assert!(result.script_asm.contains("OP_FALSE"));
561    }
562
563    // -----------------------------------------------------------------------
564    // Test: byte offset accounts for push-data (placeholder after push has offset > 1)
565    // -----------------------------------------------------------------------
566
567    #[test]
568    fn test_byte_offset_with_push_data() {
569        // Push the number 17 — encoded as 0x01 0x11 (2 bytes: length prefix + value)
570        // Then a placeholder at offset 2
571        let method = StackMethod {
572            name: "check".to_string(),
573            ops: vec![
574                StackOp::Push(PushValue::Int(17)), // 2 bytes: 01 11
575                StackOp::Placeholder {
576                    param_index: 0,
577                    param_name: "x".to_string(),
578                },
579                StackOp::Opcode("OP_ADD".to_string()),
580            ],
581            max_stack_depth: 2,
582        };
583
584        let result = emit_method(&method).expect("emit should succeed");
585        assert_eq!(
586            result.constructor_slots.len(),
587            1,
588            "expected 1 constructor slot"
589        );
590
591        let slot = &result.constructor_slots[0];
592        // Push 17 takes 2 bytes (0x01 length prefix + 0x11 value), so placeholder is at offset 2
593        assert_eq!(
594            slot.byte_offset, 2,
595            "expected byteOffset=2 (after push 17 = 2 bytes), got {}",
596            slot.byte_offset
597        );
598    }
599
600    // -----------------------------------------------------------------------
601    // Test: simple opcode sequence produces correct hex
602    // -----------------------------------------------------------------------
603
604    #[test]
605    fn test_simple_sequence_hex() {
606        let method = StackMethod {
607            name: "check".to_string(),
608            ops: vec![
609                StackOp::Opcode("OP_DUP".to_string()),
610                StackOp::Opcode("OP_HASH160".to_string()),
611            ],
612            max_stack_depth: 1,
613        };
614
615        let result = emit_method(&method).expect("emit should succeed");
616        // OP_DUP = 0x76, OP_HASH160 = 0xa9
617        assert_eq!(
618            result.script_hex, "76a9",
619            "expected hex '76a9' for DUP+HASH160, got: {}",
620            result.script_hex
621        );
622    }
623
624    // -----------------------------------------------------------------------
625    // Test: CHECKSIG + VERIFY becomes CHECKSIGVERIFY via peephole optimization
626    // -----------------------------------------------------------------------
627
628    #[test]
629    fn test_peephole_optimization_applied() {
630        use super::super::optimizer::optimize_stack_ops;
631
632        let ops = vec![
633            StackOp::Opcode("OP_CHECKSIG".to_string()),
634            StackOp::Opcode("OP_VERIFY".to_string()),
635            StackOp::Opcode("OP_1".to_string()),
636        ];
637
638        let optimized_ops = optimize_stack_ops(&ops);
639        let method = StackMethod {
640            name: "check".to_string(),
641            ops: optimized_ops,
642            max_stack_depth: 1,
643        };
644
645        let result = emit_method(&method).expect("emit should succeed");
646
647        // After peephole: CHECKSIG + VERIFY -> CHECKSIGVERIFY (0xad), then OP_1 (0x51)
648        assert_eq!(
649            result.script_hex, "ad51",
650            "expected 'ad51' (CHECKSIGVERIFY + OP_1) after peephole, got: {}",
651            result.script_hex
652        );
653        assert!(
654            result.script_asm.contains("OP_CHECKSIGVERIFY"),
655            "expected OP_CHECKSIGVERIFY in ASM, got: {}",
656            result.script_asm
657        );
658    }
659
660    // -----------------------------------------------------------------------
661    // Test: multi-method contract emits OP_IF / OP_ELSE / OP_ENDIF
662    // -----------------------------------------------------------------------
663
664    #[test]
665    fn test_multi_method_dispatch_produces_if_else() {
666        use super::super::stack::lower_to_stack;
667        use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFValue};
668
669        let program = ANFProgram {
670            contract_name: "Multi".to_string(),
671            properties: vec![],
672            methods: vec![
673                ANFMethod {
674                    name: "constructor".to_string(),
675                    params: vec![],
676                    body: vec![],
677                    is_public: false,
678                },
679                ANFMethod {
680                    name: "m1".to_string(),
681                    params: vec![ANFParam {
682                        name: "x".to_string(),
683                        param_type: "bigint".to_string(),
684                    }],
685                    body: vec![
686                        ANFBinding {
687                            name: "t0".to_string(),
688                            value: ANFValue::LoadParam { name: "x".to_string() },
689                        },
690                        ANFBinding {
691                            name: "t1".to_string(),
692                            value: ANFValue::LoadConst {
693                                value: serde_json::Value::Number(serde_json::Number::from(1)),
694                            },
695                        },
696                        ANFBinding {
697                            name: "t2".to_string(),
698                            value: ANFValue::BinOp {
699                                op: "===".to_string(),
700                                left: "t0".to_string(),
701                                right: "t1".to_string(),
702                                result_type: None,
703                            },
704                        },
705                        ANFBinding {
706                            name: "t3".to_string(),
707                            value: ANFValue::Assert { value: "t2".to_string() },
708                        },
709                    ],
710                    is_public: true,
711                },
712                ANFMethod {
713                    name: "m2".to_string(),
714                    params: vec![ANFParam {
715                        name: "y".to_string(),
716                        param_type: "bigint".to_string(),
717                    }],
718                    body: vec![
719                        ANFBinding {
720                            name: "t0".to_string(),
721                            value: ANFValue::LoadParam { name: "y".to_string() },
722                        },
723                        ANFBinding {
724                            name: "t1".to_string(),
725                            value: ANFValue::LoadConst {
726                                value: serde_json::Value::Number(serde_json::Number::from(2)),
727                            },
728                        },
729                        ANFBinding {
730                            name: "t2".to_string(),
731                            value: ANFValue::BinOp {
732                                op: "===".to_string(),
733                                left: "t0".to_string(),
734                                right: "t1".to_string(),
735                                result_type: None,
736                            },
737                        },
738                        ANFBinding {
739                            name: "t3".to_string(),
740                            value: ANFValue::Assert { value: "t2".to_string() },
741                        },
742                    ],
743                    is_public: true,
744                },
745            ],
746        };
747
748        let methods = lower_to_stack(&program).expect("lower_to_stack should succeed");
749
750        // Apply peephole optimization as the compiler pipeline does
751        use super::super::optimizer::optimize_stack_ops;
752        let optimized: Vec<StackMethod> = methods
753            .iter()
754            .map(|m| StackMethod {
755                name: m.name.clone(),
756                ops: optimize_stack_ops(&m.ops),
757                max_stack_depth: m.max_stack_depth,
758            })
759            .collect();
760
761        let result = emit(&optimized).expect("emit should succeed");
762
763        // Multi-method dispatch should emit OP_IF / OP_ELSE / OP_ENDIF
764        assert!(
765            result.script_asm.contains("OP_IF"),
766            "expected OP_IF in multi-method dispatch, got: {}",
767            result.script_asm
768        );
769        assert!(
770            result.script_asm.contains("OP_ELSE"),
771            "expected OP_ELSE in multi-method dispatch, got: {}",
772            result.script_asm
773        );
774        assert!(
775            result.script_asm.contains("OP_ENDIF"),
776            "expected OP_ENDIF in multi-method dispatch, got: {}",
777            result.script_asm
778        );
779    }
780
781    // -----------------------------------------------------------------------
782    // Test: byte offset accounts for preceding single-byte opcodes
783    // Mirrors Go TestEmit_ByteOffsetAccountsForPrecedingOpcodes
784    // -----------------------------------------------------------------------
785
786    #[test]
787    fn test_byte_offset_accounts_for_preceding_opcodes() {
788        // OP_DUP (1 byte: 0x76) + OP_HASH160 (1 byte: 0xa9) before Placeholder
789        // => placeholder should be at byte offset 2
790        let method = StackMethod {
791            name: "check".to_string(),
792            ops: vec![
793                StackOp::Opcode("OP_DUP".to_string()),       // 1 byte
794                StackOp::Opcode("OP_HASH160".to_string()),   // 1 byte
795                StackOp::Placeholder {
796                    param_index: 0,
797                    param_name: "pubKeyHash".to_string(),
798                },
799                StackOp::Opcode("OP_EQUALVERIFY".to_string()),
800                StackOp::Opcode("OP_CHECKSIG".to_string()),
801            ],
802            max_stack_depth: 2,
803        };
804
805        let result = emit_method(&method).expect("emit should succeed");
806        assert_eq!(result.constructor_slots.len(), 1, "expected 1 constructor slot");
807
808        let slot = &result.constructor_slots[0];
809        // OP_DUP (1 byte) + OP_HASH160 (1 byte) = 2 bytes before placeholder
810        assert_eq!(
811            slot.byte_offset, 2,
812            "expected byteOffset=2 (after OP_DUP + OP_HASH160), got {}",
813            slot.byte_offset
814        );
815    }
816
817    // -----------------------------------------------------------------------
818    // Test: full P2PKH pipeline produces non-empty script and constructor slots
819    // Mirrors Go TestEmit_FullP2PKH
820    // -----------------------------------------------------------------------
821
822    #[test]
823    fn test_full_p2pkh() {
824        use super::super::stack::lower_to_stack;
825        use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFProperty, ANFValue};
826
827        let program = ANFProgram {
828            contract_name: "P2PKH".to_string(),
829            properties: vec![ANFProperty {
830                name: "pubKeyHash".to_string(),
831                prop_type: "Addr".to_string(),
832                readonly: true,
833                initial_value: None,
834            }],
835            methods: vec![ANFMethod {
836                name: "unlock".to_string(),
837                params: vec![
838                    ANFParam { name: "sig".to_string(), param_type: "Sig".to_string() },
839                    ANFParam { name: "pubKey".to_string(), param_type: "PubKey".to_string() },
840                ],
841                body: vec![
842                    ANFBinding {
843                        name: "t0".to_string(),
844                        value: ANFValue::LoadParam { name: "pubKey".to_string() },
845                    },
846                    ANFBinding {
847                        name: "t1".to_string(),
848                        value: ANFValue::Call {
849                            func: "hash160".to_string(),
850                            args: vec!["t0".to_string()],
851                        },
852                    },
853                    ANFBinding {
854                        name: "t2".to_string(),
855                        value: ANFValue::LoadProp { name: "pubKeyHash".to_string() },
856                    },
857                    ANFBinding {
858                        name: "t3".to_string(),
859                        value: ANFValue::BinOp {
860                            op: "===".to_string(),
861                            left: "t1".to_string(),
862                            right: "t2".to_string(),
863                            result_type: Some("bytes".to_string()),
864                        },
865                    },
866                    ANFBinding {
867                        name: "t4".to_string(),
868                        value: ANFValue::Assert { value: "t3".to_string() },
869                    },
870                    ANFBinding {
871                        name: "t5".to_string(),
872                        value: ANFValue::LoadParam { name: "sig".to_string() },
873                    },
874                    ANFBinding {
875                        name: "t6".to_string(),
876                        value: ANFValue::LoadParam { name: "pubKey".to_string() },
877                    },
878                    ANFBinding {
879                        name: "t7".to_string(),
880                        value: ANFValue::Call {
881                            func: "checkSig".to_string(),
882                            args: vec!["t5".to_string(), "t6".to_string()],
883                        },
884                    },
885                    ANFBinding {
886                        name: "t8".to_string(),
887                        value: ANFValue::Assert { value: "t7".to_string() },
888                    },
889                ],
890                is_public: true,
891            }],
892        };
893
894        let stack_methods = lower_to_stack(&program).expect("stack lowering should succeed");
895        let result = emit(&stack_methods).expect("emit should succeed");
896
897        assert!(
898            !result.script_hex.is_empty(),
899            "P2PKH should produce non-empty script hex"
900        );
901        assert!(
902            !result.constructor_slots.is_empty(),
903            "P2PKH should have at least one constructor slot for pubKeyHash"
904        );
905    }
906
907    // -----------------------------------------------------------------------
908    // M10: integers 17+ use push prefix (not OP_17 opcode)
909    // -----------------------------------------------------------------------
910
911    #[test]
912    fn test_m10_integer_17_uses_push_prefix_not_op17() {
913        let method = StackMethod {
914            name: "test".to_string(),
915            ops: vec![StackOp::Push(PushValue::Int(17))],
916            max_stack_depth: 1,
917        };
918        let result = emit_method(&method).expect("emit should succeed");
919        // OP_17 would be 0x61. A push-data encoded 17 would be "0111" (length 1, value 0x11).
920        assert!(
921            !result.script_hex.starts_with("61"),
922            "17 should NOT be encoded as OP_17 (0x61); OP_1..OP_16 are for 1–16 only. got: {}",
923            result.script_hex
924        );
925        // Should use push-data prefix: 01 followed by the value byte 11
926        assert!(
927            result.script_hex.contains("11"),
928            "17 (0x11) should appear in the script hex; got: {}",
929            result.script_hex
930        );
931    }
932
933    // -----------------------------------------------------------------------
934    // M12: 256-byte data → OP_PUSHDATA2
935    // 256-byte array → hex starts with "4d0001"
936    // -----------------------------------------------------------------------
937
938    #[test]
939    fn test_m12_256_byte_data_uses_pushdata2() {
940        let data = vec![0xabu8; 256];
941        let method = StackMethod {
942            name: "test".to_string(),
943            ops: vec![StackOp::Push(PushValue::Bytes(data))],
944            max_stack_depth: 1,
945        };
946        let result = emit_method(&method).expect("emit should succeed");
947        // OP_PUSHDATA2 = 0x4d, followed by length in 2 bytes LE: 256 = 0x0001 LE = 00 01
948        assert!(
949            result.script_hex.starts_with("4d0001"),
950            "256-byte push should use OP_PUSHDATA2 prefix '4d0001', got: {}",
951            &result.script_hex[..result.script_hex.len().min(12)]
952        );
953    }
954
955    // -----------------------------------------------------------------------
956    // M19: sha256 contract has OP_SHA256 in ASM
957    // -----------------------------------------------------------------------
958
959    #[test]
960    fn test_m19_sha256_contract_has_op_sha256_in_asm() {
961        use super::super::stack::lower_to_stack;
962        use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFValue};
963
964        let program = ANFProgram {
965            contract_name: "Sha256Test".to_string(),
966            properties: vec![],
967            methods: vec![ANFMethod {
968                name: "check".to_string(),
969                params: vec![ANFParam {
970                    name: "data".to_string(),
971                    param_type: "ByteString".to_string(),
972                }],
973                body: vec![
974                    ANFBinding {
975                        name: "t0".to_string(),
976                        value: ANFValue::LoadParam { name: "data".to_string() },
977                    },
978                    ANFBinding {
979                        name: "t1".to_string(),
980                        value: ANFValue::Call {
981                            func: "sha256".to_string(),
982                            args: vec!["t0".to_string()],
983                        },
984                    },
985                    ANFBinding {
986                        name: "t2".to_string(),
987                        value: ANFValue::Assert { value: "t1".to_string() },
988                    },
989                ],
990                is_public: true,
991            }],
992        };
993
994        let stack_methods = lower_to_stack(&program).expect("stack lowering should succeed");
995        let result = emit(&stack_methods).expect("emit should succeed");
996        assert!(
997            result.script_asm.contains("OP_SHA256"),
998            "sha256() call should produce OP_SHA256 in ASM; got: {}",
999            result.script_asm
1000        );
1001    }
1002
1003    // -----------------------------------------------------------------------
1004    // M21: OP_DUP encodes 0x76
1005    // -----------------------------------------------------------------------
1006
1007    #[test]
1008    fn test_m21_op_dup_encodes_0x76() {
1009        let method = StackMethod {
1010            name: "test".to_string(),
1011            ops: vec![StackOp::Dup],
1012            max_stack_depth: 1,
1013        };
1014        let result = emit_method(&method).expect("emit should succeed");
1015        assert_eq!(
1016            result.script_hex, "76",
1017            "OP_DUP should encode as 0x76; got: {}",
1018            result.script_hex
1019        );
1020    }
1021
1022    // -----------------------------------------------------------------------
1023    // M22: OP_SWAP encodes 0x7c
1024    // -----------------------------------------------------------------------
1025
1026    #[test]
1027    fn test_m22_op_swap_encodes_0x7c() {
1028        let method = StackMethod {
1029            name: "test".to_string(),
1030            ops: vec![StackOp::Swap],
1031            max_stack_depth: 2,
1032        };
1033        let result = emit_method(&method).expect("emit should succeed");
1034        assert_eq!(
1035            result.script_hex, "7c",
1036            "OP_SWAP should encode as 0x7c; got: {}",
1037            result.script_hex
1038        );
1039    }
1040
1041    // -----------------------------------------------------------------------
1042    // M24: if without else → no OP_ELSE
1043    // -----------------------------------------------------------------------
1044
1045    #[test]
1046    fn test_m24_if_without_else_no_op_else() {
1047        let method = StackMethod {
1048            name: "test".to_string(),
1049            ops: vec![StackOp::If {
1050                then_ops: vec![StackOp::Opcode("OP_DROP".to_string())],
1051                else_ops: vec![],
1052            }],
1053            max_stack_depth: 1,
1054        };
1055        let result = emit_method(&method).expect("emit should succeed");
1056        assert!(
1057            !result.script_asm.contains("OP_ELSE"),
1058            "if with empty else branch should NOT contain OP_ELSE; got asm: {}",
1059            result.script_asm
1060        );
1061        assert!(
1062            result.script_asm.contains("OP_IF"),
1063            "should still contain OP_IF; got asm: {}",
1064            result.script_asm
1065        );
1066    }
1067
1068    // -----------------------------------------------------------------------
1069    // M25: single method → no dispatch (no OP_IF for method selection)
1070    // -----------------------------------------------------------------------
1071
1072    #[test]
1073    fn test_m25_single_method_no_dispatch() {
1074        use super::super::stack::lower_to_stack;
1075        use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFProperty, ANFValue};
1076
1077        // A program with a single public method (plus constructor) — no method dispatch needed
1078        let program = ANFProgram {
1079            contract_name: "Single".to_string(),
1080            properties: vec![ANFProperty {
1081                name: "x".to_string(),
1082                prop_type: "bigint".to_string(),
1083                readonly: true,
1084                initial_value: None,
1085            }],
1086            methods: vec![
1087                ANFMethod {
1088                    name: "constructor".to_string(),
1089                    params: vec![ANFParam {
1090                        name: "x".to_string(),
1091                        param_type: "bigint".to_string(),
1092                    }],
1093                    body: vec![],
1094                    is_public: false,
1095                },
1096                ANFMethod {
1097                    name: "check".to_string(),
1098                    params: vec![ANFParam {
1099                        name: "v".to_string(),
1100                        param_type: "bigint".to_string(),
1101                    }],
1102                    body: vec![
1103                        ANFBinding {
1104                            name: "t0".to_string(),
1105                            value: ANFValue::LoadParam { name: "v".to_string() },
1106                        },
1107                        ANFBinding {
1108                            name: "t1".to_string(),
1109                            value: ANFValue::LoadProp { name: "x".to_string() },
1110                        },
1111                        ANFBinding {
1112                            name: "t2".to_string(),
1113                            value: ANFValue::BinOp {
1114                                op: "===".to_string(),
1115                                left: "t0".to_string(),
1116                                right: "t1".to_string(),
1117                                result_type: None,
1118                            },
1119                        },
1120                        ANFBinding {
1121                            name: "t3".to_string(),
1122                            value: ANFValue::Assert { value: "t2".to_string() },
1123                        },
1124                    ],
1125                    is_public: true,
1126                },
1127            ],
1128        };
1129
1130        let stack_methods = lower_to_stack(&program).expect("stack lowering should succeed");
1131        let result = emit(&stack_methods).expect("emit should succeed");
1132
1133        // With a single public method, there should be no OP_IF method dispatch
1134        // (the dispatch table is only needed for 2+ public methods)
1135        assert!(
1136            !result.script_asm.contains("OP_IF"),
1137            "single public method should NOT produce OP_IF dispatch; got asm: {}",
1138            result.script_asm
1139        );
1140    }
1141}