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}
38
39// ---------------------------------------------------------------------------
40// Emit context
41// ---------------------------------------------------------------------------
42
43struct EmitContext {
44    hex_parts: Vec<String>,
45    asm_parts: Vec<String>,
46    byte_length: usize,
47    constructor_slots: Vec<ConstructorSlot>,
48}
49
50impl EmitContext {
51    fn new() -> Self {
52        EmitContext {
53            hex_parts: Vec::new(),
54            asm_parts: Vec::new(),
55            byte_length: 0,
56            constructor_slots: Vec::new(),
57        }
58    }
59
60    fn append_hex(&mut self, hex: &str) {
61        self.byte_length += hex.len() / 2;
62        self.hex_parts.push(hex.to_string());
63    }
64
65    fn emit_opcode(&mut self, name: &str) -> Result<(), String> {
66        let byte = opcode_byte(name)
67            .ok_or_else(|| format!("unknown opcode: {}", name))?;
68        self.append_hex(&format!("{:02x}", byte));
69        self.asm_parts.push(name.to_string());
70        Ok(())
71    }
72
73    fn emit_push(&mut self, value: &PushValue) {
74        let (h, a) = encode_push_value(value);
75        self.append_hex(&h);
76        self.asm_parts.push(a);
77    }
78
79    fn emit_placeholder(&mut self, param_index: usize, _param_name: &str) {
80        let byte_offset = self.byte_length;
81        self.append_hex("00"); // OP_0 placeholder byte
82        self.asm_parts.push("OP_0".to_string());
83        self.constructor_slots.push(ConstructorSlot {
84            param_index,
85            byte_offset,
86        });
87    }
88
89    fn get_hex(&self) -> String {
90        self.hex_parts.join("")
91    }
92
93    fn get_asm(&self) -> String {
94        self.asm_parts.join(" ")
95    }
96}
97
98// ---------------------------------------------------------------------------
99// Script number encoding
100// ---------------------------------------------------------------------------
101
102/// Encode an i128 as a Bitcoin Script number (little-endian, sign-magnitude).
103/// Bitcoin Script numbers can be up to 2^252, so i128 is needed.
104pub fn encode_script_number(n: i128) -> Vec<u8> {
105    if n == 0 {
106        return Vec::new();
107    }
108
109    let negative = n < 0;
110    let mut abs = if negative { (-n) as u128 } else { n as u128 };
111
112    let mut bytes = Vec::new();
113    while abs > 0 {
114        bytes.push((abs & 0xff) as u8);
115        abs >>= 8;
116    }
117
118    let last_byte = *bytes.last().unwrap();
119    if last_byte & 0x80 != 0 {
120        bytes.push(if negative { 0x80 } else { 0x00 });
121    } else if negative {
122        let len = bytes.len();
123        bytes[len - 1] = last_byte | 0x80;
124    }
125
126    bytes
127}
128
129// ---------------------------------------------------------------------------
130// Push data encoding
131// ---------------------------------------------------------------------------
132
133/// Encode raw bytes as a Bitcoin Script push-data operation.
134pub fn encode_push_data(data: &[u8]) -> Vec<u8> {
135    let len = data.len();
136
137    if len == 0 {
138        return vec![0x00]; // OP_0
139    }
140
141    // MINIMALDATA: single-byte values 1-16 must use OP_1..OP_16, 0x81 must use OP_1NEGATE.
142    // Note: 0x00 is NOT converted to OP_0 because OP_0 pushes empty [] not [0x00].
143    if len == 1 {
144        let b = data[0];
145        if b >= 1 && b <= 16 {
146            return vec![0x50 + b]; // OP_1 through OP_16
147        }
148        if b == 0x81 {
149            return vec![0x4f]; // OP_1NEGATE
150        }
151    }
152
153    if len <= 75 {
154        let mut result = vec![len as u8];
155        result.extend_from_slice(data);
156        return result;
157    }
158
159    if len <= 255 {
160        let mut result = vec![0x4c, len as u8]; // OP_PUSHDATA1
161        result.extend_from_slice(data);
162        return result;
163    }
164
165    if len <= 65535 {
166        let mut result = vec![0x4d, (len & 0xff) as u8, ((len >> 8) & 0xff) as u8]; // OP_PUSHDATA2
167        result.extend_from_slice(data);
168        return result;
169    }
170
171    // OP_PUSHDATA4
172    let mut result = vec![
173        0x4e,
174        (len & 0xff) as u8,
175        ((len >> 8) & 0xff) as u8,
176        ((len >> 16) & 0xff) as u8,
177        ((len >> 24) & 0xff) as u8,
178    ];
179    result.extend_from_slice(data);
180    result
181}
182
183/// Encode a push value to hex and asm strings.
184fn encode_push_value(value: &PushValue) -> (String, String) {
185    match value {
186        PushValue::Bool(b) => {
187            if *b {
188                ("51".to_string(), "OP_TRUE".to_string())
189            } else {
190                ("00".to_string(), "OP_FALSE".to_string())
191            }
192        }
193        PushValue::Int(n) => encode_push_int(*n),
194        PushValue::Bytes(bytes) => {
195            let encoded = encode_push_data(bytes);
196            let h = hex::encode(&encoded);
197            if bytes.is_empty() {
198                (h, "OP_0".to_string())
199            } else {
200                (h, format!("<{}>", hex::encode(bytes)))
201            }
202        }
203    }
204}
205
206/// Encode an integer push, using small-integer opcodes where possible.
207pub fn encode_push_int(n: i128) -> (String, String) {
208    if n == 0 {
209        return ("00".to_string(), "OP_0".to_string());
210    }
211
212    if n == -1 {
213        return ("4f".to_string(), "OP_1NEGATE".to_string());
214    }
215
216    if n >= 1 && n <= 16 {
217        let opcode = 0x50 + n as u8;
218        return (format!("{:02x}", opcode), format!("OP_{}", n));
219    }
220
221    let num_bytes = encode_script_number(n);
222    let encoded = encode_push_data(&num_bytes);
223    (hex::encode(&encoded), format!("<{}>", hex::encode(&num_bytes)))
224}
225
226// ---------------------------------------------------------------------------
227// Emit a single StackOp
228// ---------------------------------------------------------------------------
229
230fn emit_stack_op(op: &StackOp, ctx: &mut EmitContext) -> Result<(), String> {
231    match op {
232        StackOp::Push(value) => {
233            ctx.emit_push(value);
234            Ok(())
235        }
236        StackOp::Dup => ctx.emit_opcode("OP_DUP"),
237        StackOp::Swap => ctx.emit_opcode("OP_SWAP"),
238        StackOp::Roll { .. } => ctx.emit_opcode("OP_ROLL"),
239        StackOp::Pick { .. } => ctx.emit_opcode("OP_PICK"),
240        StackOp::Drop => ctx.emit_opcode("OP_DROP"),
241        StackOp::Nip => ctx.emit_opcode("OP_NIP"),
242        StackOp::Over => ctx.emit_opcode("OP_OVER"),
243        StackOp::Rot => ctx.emit_opcode("OP_ROT"),
244        StackOp::Tuck => ctx.emit_opcode("OP_TUCK"),
245        StackOp::Opcode(code) => ctx.emit_opcode(code),
246        StackOp::If {
247            then_ops,
248            else_ops,
249        } => emit_if(then_ops, else_ops, ctx),
250        StackOp::Placeholder {
251            param_index,
252            param_name,
253        } => {
254            ctx.emit_placeholder(*param_index, param_name);
255            Ok(())
256        }
257    }
258}
259
260fn emit_if(
261    then_ops: &[StackOp],
262    else_ops: &[StackOp],
263    ctx: &mut EmitContext,
264) -> Result<(), String> {
265    ctx.emit_opcode("OP_IF")?;
266
267    for op in then_ops {
268        emit_stack_op(op, ctx)?;
269    }
270
271    if !else_ops.is_empty() {
272        ctx.emit_opcode("OP_ELSE")?;
273        for op in else_ops {
274            emit_stack_op(op, ctx)?;
275        }
276    }
277
278    ctx.emit_opcode("OP_ENDIF")
279}
280
281// ---------------------------------------------------------------------------
282// Peephole optimization
283// ---------------------------------------------------------------------------
284
285// ---------------------------------------------------------------------------
286// Public API
287// ---------------------------------------------------------------------------
288
289/// Emit a slice of StackMethods as Bitcoin Script hex and ASM.
290///
291/// For contracts with multiple public methods, generates a method dispatch
292/// preamble using OP_IF/OP_ELSE chains.
293///
294/// Note: peephole optimization (VERIFY combinations, SWAP elimination) is
295/// handled by `optimize_stack_ops` in optimizer.rs, which runs before emit.
296pub fn emit(methods: &[StackMethod]) -> Result<EmitResult, String> {
297    let mut ctx = EmitContext::new();
298
299    // Filter to public methods (exclude constructor)
300    let public_methods: Vec<StackMethod> = methods
301        .iter()
302        .filter(|m| m.name != "constructor")
303        .cloned()
304        .collect();
305
306    if public_methods.is_empty() {
307        return Ok(EmitResult {
308            script_hex: String::new(),
309            script_asm: String::new(),
310            constructor_slots: Vec::new(),
311        });
312    }
313
314    if public_methods.len() == 1 {
315        for op in &public_methods[0].ops {
316            emit_stack_op(op, &mut ctx)?;
317        }
318    } else {
319        let refs: Vec<&StackMethod> = public_methods.iter().collect();
320        emit_method_dispatch(&refs, &mut ctx)?;
321    }
322
323    Ok(EmitResult {
324        script_hex: ctx.get_hex(),
325        script_asm: ctx.get_asm(),
326        constructor_slots: ctx.constructor_slots,
327    })
328}
329
330fn emit_method_dispatch(
331    methods: &[&StackMethod],
332    ctx: &mut EmitContext,
333) -> Result<(), String> {
334    for (i, method) in methods.iter().enumerate() {
335        let is_last = i == methods.len() - 1;
336
337        if !is_last {
338            ctx.emit_opcode("OP_DUP")?;
339            ctx.emit_push(&PushValue::Int(i as i128));
340            ctx.emit_opcode("OP_NUMEQUAL")?;
341            ctx.emit_opcode("OP_IF")?;
342            ctx.emit_opcode("OP_DROP")?;
343        } else {
344            ctx.emit_opcode("OP_DROP")?;
345        }
346
347        for op in &method.ops {
348            emit_stack_op(op, ctx)?;
349        }
350
351        if !is_last {
352            ctx.emit_opcode("OP_ELSE")?;
353        }
354    }
355
356    // Close nested OP_IF/OP_ELSE blocks
357    for _ in 0..methods.len() - 1 {
358        ctx.emit_opcode("OP_ENDIF")?;
359    }
360
361    Ok(())
362}
363
364/// Emit a single method's ops. Useful for testing.
365pub fn emit_method(method: &StackMethod) -> Result<EmitResult, String> {
366    let mut ctx = EmitContext::new();
367    for op in &method.ops {
368        emit_stack_op(op, &mut ctx)?;
369    }
370    Ok(EmitResult {
371        script_hex: ctx.get_hex(),
372        script_asm: ctx.get_asm(),
373        constructor_slots: ctx.constructor_slots,
374    })
375}
376
377// ---------------------------------------------------------------------------
378// Tests
379// ---------------------------------------------------------------------------
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_emit_placeholder_produces_constructor_slot() {
387        let method = StackMethod {
388            name: "unlock".to_string(),
389            ops: vec![StackOp::Placeholder {
390                param_index: 0,
391                param_name: "pubKeyHash".to_string(),
392            }],
393            max_stack_depth: 1,
394        };
395
396        let result = emit_method(&method).expect("emit should succeed");
397        assert_eq!(
398            result.constructor_slots.len(),
399            1,
400            "should produce exactly one constructor slot"
401        );
402        assert_eq!(result.constructor_slots[0].param_index, 0);
403        assert_eq!(result.constructor_slots[0].byte_offset, 0);
404    }
405
406    #[test]
407    fn test_multiple_placeholders_produce_distinct_byte_offsets() {
408        let method = StackMethod {
409            name: "test".to_string(),
410            ops: vec![
411                StackOp::Placeholder {
412                    param_index: 0,
413                    param_name: "a".to_string(),
414                },
415                StackOp::Placeholder {
416                    param_index: 1,
417                    param_name: "b".to_string(),
418                },
419            ],
420            max_stack_depth: 2,
421        };
422
423        let result = emit_method(&method).expect("emit should succeed");
424        assert_eq!(
425            result.constructor_slots.len(),
426            2,
427            "should produce two constructor slots"
428        );
429
430        // First placeholder at byte 0
431        assert_eq!(result.constructor_slots[0].param_index, 0);
432        assert_eq!(result.constructor_slots[0].byte_offset, 0);
433
434        // Second placeholder at byte 1 (after the first OP_0 byte)
435        assert_eq!(result.constructor_slots[1].param_index, 1);
436        assert_eq!(result.constructor_slots[1].byte_offset, 1);
437
438        // Byte offsets should be distinct
439        assert_ne!(
440            result.constructor_slots[0].byte_offset,
441            result.constructor_slots[1].byte_offset
442        );
443    }
444
445    #[test]
446    fn test_placeholder_byte_offset_position_is_op_0() {
447        let method = StackMethod {
448            name: "test".to_string(),
449            ops: vec![
450                StackOp::Push(PushValue::Int(42)), // some bytes before
451                StackOp::Placeholder {
452                    param_index: 0,
453                    param_name: "x".to_string(),
454                },
455            ],
456            max_stack_depth: 2,
457        };
458
459        let result = emit_method(&method).expect("emit should succeed");
460        assert_eq!(result.constructor_slots.len(), 1);
461
462        let slot = &result.constructor_slots[0];
463        let hex = &result.script_hex;
464
465        // The byte at the placeholder offset should be "00" (OP_0)
466        let byte_hex = &hex[slot.byte_offset * 2..slot.byte_offset * 2 + 2];
467        assert_eq!(
468            byte_hex, "00",
469            "expected OP_0 at placeholder byte offset {}, got '{}' in hex '{}'",
470            slot.byte_offset, byte_hex, hex
471        );
472    }
473
474    #[test]
475    fn test_emit_single_method_produces_hex_and_asm() {
476        use super::super::optimizer::optimize_stack_ops;
477
478        let method = StackMethod {
479            name: "check".to_string(),
480            ops: vec![
481                StackOp::Push(PushValue::Int(42)),
482                StackOp::Opcode("OP_NUMEQUAL".to_string()),
483                StackOp::Opcode("OP_VERIFY".to_string()),
484            ],
485            max_stack_depth: 1,
486        };
487
488        // Apply peephole optimization before emit (as the compiler pipeline does)
489        let optimized_method = StackMethod {
490            name: method.name.clone(),
491            ops: optimize_stack_ops(&method.ops),
492            max_stack_depth: method.max_stack_depth,
493        };
494
495        let result = emit(&[optimized_method]).expect("emit should succeed");
496        assert!(!result.script_hex.is_empty(), "hex should not be empty");
497        assert!(!result.script_asm.is_empty(), "asm should not be empty");
498        assert!(
499            result.script_asm.contains("OP_NUMEQUALVERIFY"),
500            "standalone peephole optimizer should combine OP_NUMEQUAL + OP_VERIFY into OP_NUMEQUALVERIFY, got: {}",
501            result.script_asm
502        );
503    }
504
505    #[test]
506    fn test_emit_empty_methods_produces_empty_output() {
507        let result = emit(&[]).expect("emit with no methods should succeed");
508        assert!(
509            result.script_hex.is_empty(),
510            "empty methods should produce empty hex"
511        );
512        assert!(
513            result.constructor_slots.is_empty(),
514            "empty methods should produce no constructor slots"
515        );
516    }
517
518    #[test]
519    fn test_emit_push_bool_values() {
520        let method = StackMethod {
521            name: "test".to_string(),
522            ops: vec![
523                StackOp::Push(PushValue::Bool(true)),
524                StackOp::Push(PushValue::Bool(false)),
525            ],
526            max_stack_depth: 2,
527        };
528
529        let result = emit_method(&method).expect("emit should succeed");
530        // OP_TRUE = 0x51, OP_FALSE = 0x00
531        assert!(
532            result.script_hex.starts_with("51"),
533            "true should emit 0x51, got: {}",
534            result.script_hex
535        );
536        assert!(
537            result.script_hex.ends_with("00"),
538            "false should emit 0x00, got: {}",
539            result.script_hex
540        );
541        assert!(result.script_asm.contains("OP_TRUE"));
542        assert!(result.script_asm.contains("OP_FALSE"));
543    }
544}