1use serde::{Deserialize, Serialize};
8
9use super::opcodes::opcode_byte;
10use super::stack::{PushValue, StackMethod, StackOp};
11
12#[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#[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#[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 pub source_map: Vec<SourceMapping>,
56}
57
58struct 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_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 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"); 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
150pub 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
181pub fn encode_push_data(data: &[u8]) -> Vec<u8> {
187 let len = data.len();
188
189 if len == 0 {
190 return vec![0x00]; }
192
193 if len == 1 {
196 let b = data[0];
197 if b >= 1 && b <= 16 {
198 return vec![0x50 + b]; }
200 if b == 0x81 {
201 return vec![0x4f]; }
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]; 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]; result.extend_from_slice(data);
220 return result;
221 }
222
223 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
235fn 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
258pub 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
278fn 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 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
343pub fn emit(methods: &[StackMethod]) -> Result<EmitResult, String> {
359 let mut ctx = EmitContext::new();
360
361 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 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 for _ in 0..methods.len() - 1 {
431 ctx.emit_opcode("OP_ENDIF")?;
432 }
433
434 Ok(())
435}
436
437pub 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#[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 assert_eq!(result.constructor_slots[0].param_index, 0);
511 assert_eq!(result.constructor_slots[0].byte_offset, 0);
512
513 assert_eq!(result.constructor_slots[1].param_index, 1);
515 assert_eq!(result.constructor_slots[1].byte_offset, 1);
516
517 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)), 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 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 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 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 #[test]
633 fn test_byte_offset_with_push_data() {
634 let method = StackMethod {
637 name: "check".to_string(),
638 ops: vec![
639 StackOp::Push(PushValue::Int(17)), 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 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 #[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 assert_eq!(
685 result.script_hex, "76a9",
686 "expected hex '76a9' for DUP+HASH160, got: {}",
687 result.script_hex
688 );
689 }
690
691 #[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 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 #[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 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 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 #[test]
864 fn test_byte_offset_accounts_for_preceding_opcodes() {
865 let method = StackMethod {
868 name: "check".to_string(),
869 ops: vec![
870 StackOp::Opcode("OP_DUP".to_string()), StackOp::Opcode("OP_HASH160".to_string()), 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 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 #[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 #[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 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 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 #[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 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 #[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 #[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 #[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 #[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 #[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 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 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}