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