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)]
33pub struct EmitResult {
34 pub script_hex: String,
35 pub script_asm: String,
36 pub constructor_slots: Vec<ConstructorSlot>,
37 pub code_separator_index: i64,
38 pub code_separator_indices: Vec<usize>,
39}
40
41struct EmitContext {
46 hex_parts: Vec<String>,
47 asm_parts: Vec<String>,
48 byte_length: usize,
49 constructor_slots: Vec<ConstructorSlot>,
50 code_separator_index: i64,
51 code_separator_indices: Vec<usize>,
52}
53
54impl EmitContext {
55 fn new() -> Self {
56 EmitContext {
57 hex_parts: Vec::new(),
58 asm_parts: Vec::new(),
59 byte_length: 0,
60 constructor_slots: Vec::new(),
61 code_separator_index: -1,
62 code_separator_indices: Vec::new(),
63 }
64 }
65
66 fn append_hex(&mut self, hex: &str) {
67 self.byte_length += hex.len() / 2;
68 self.hex_parts.push(hex.to_string());
69 }
70
71 fn emit_opcode(&mut self, name: &str) -> Result<(), String> {
72 let byte = opcode_byte(name)
73 .ok_or_else(|| format!("unknown opcode: {}", name))?;
74 if name == "OP_CODESEPARATOR" {
75 self.code_separator_index = self.byte_length as i64;
76 self.code_separator_indices.push(self.byte_length);
77 }
78 self.append_hex(&format!("{:02x}", byte));
79 self.asm_parts.push(name.to_string());
80 Ok(())
81 }
82
83 fn emit_push(&mut self, value: &PushValue) {
84 let (h, a) = encode_push_value(value);
85 self.append_hex(&h);
86 self.asm_parts.push(a);
87 }
88
89 fn emit_placeholder(&mut self, param_index: usize, _param_name: &str) {
90 let byte_offset = self.byte_length;
91 self.append_hex("00"); self.asm_parts.push("OP_0".to_string());
93 self.constructor_slots.push(ConstructorSlot {
94 param_index,
95 byte_offset,
96 });
97 }
98
99 fn get_hex(&self) -> String {
100 self.hex_parts.join("")
101 }
102
103 fn get_asm(&self) -> String {
104 self.asm_parts.join(" ")
105 }
106}
107
108pub fn encode_script_number(n: i128) -> Vec<u8> {
115 if n == 0 {
116 return Vec::new();
117 }
118
119 let negative = n < 0;
120 let mut abs = if negative { (-n) as u128 } else { n as u128 };
121
122 let mut bytes = Vec::new();
123 while abs > 0 {
124 bytes.push((abs & 0xff) as u8);
125 abs >>= 8;
126 }
127
128 let last_byte = *bytes.last().unwrap();
129 if last_byte & 0x80 != 0 {
130 bytes.push(if negative { 0x80 } else { 0x00 });
131 } else if negative {
132 let len = bytes.len();
133 bytes[len - 1] = last_byte | 0x80;
134 }
135
136 bytes
137}
138
139pub fn encode_push_data(data: &[u8]) -> Vec<u8> {
145 let len = data.len();
146
147 if len == 0 {
148 return vec![0x00]; }
150
151 if len == 1 {
154 let b = data[0];
155 if b >= 1 && b <= 16 {
156 return vec![0x50 + b]; }
158 if b == 0x81 {
159 return vec![0x4f]; }
161 }
162
163 if len <= 75 {
164 let mut result = vec![len as u8];
165 result.extend_from_slice(data);
166 return result;
167 }
168
169 if len <= 255 {
170 let mut result = vec![0x4c, len as u8]; result.extend_from_slice(data);
172 return result;
173 }
174
175 if len <= 65535 {
176 let mut result = vec![0x4d, (len & 0xff) as u8, ((len >> 8) & 0xff) as u8]; result.extend_from_slice(data);
178 return result;
179 }
180
181 let mut result = vec![
183 0x4e,
184 (len & 0xff) as u8,
185 ((len >> 8) & 0xff) as u8,
186 ((len >> 16) & 0xff) as u8,
187 ((len >> 24) & 0xff) as u8,
188 ];
189 result.extend_from_slice(data);
190 result
191}
192
193fn encode_push_value(value: &PushValue) -> (String, String) {
195 match value {
196 PushValue::Bool(b) => {
197 if *b {
198 ("51".to_string(), "OP_TRUE".to_string())
199 } else {
200 ("00".to_string(), "OP_FALSE".to_string())
201 }
202 }
203 PushValue::Int(n) => encode_push_int(*n),
204 PushValue::Bytes(bytes) => {
205 let encoded = encode_push_data(bytes);
206 let h = hex::encode(&encoded);
207 if bytes.is_empty() {
208 (h, "OP_0".to_string())
209 } else {
210 (h, format!("<{}>", hex::encode(bytes)))
211 }
212 }
213 }
214}
215
216pub fn encode_push_int(n: i128) -> (String, String) {
218 if n == 0 {
219 return ("00".to_string(), "OP_0".to_string());
220 }
221
222 if n == -1 {
223 return ("4f".to_string(), "OP_1NEGATE".to_string());
224 }
225
226 if n >= 1 && n <= 16 {
227 let opcode = 0x50 + n as u8;
228 return (format!("{:02x}", opcode), format!("OP_{}", n));
229 }
230
231 let num_bytes = encode_script_number(n);
232 let encoded = encode_push_data(&num_bytes);
233 (hex::encode(&encoded), format!("<{}>", hex::encode(&num_bytes)))
234}
235
236fn emit_stack_op(op: &StackOp, ctx: &mut EmitContext) -> Result<(), String> {
241 match op {
242 StackOp::Push(value) => {
243 ctx.emit_push(value);
244 Ok(())
245 }
246 StackOp::Dup => ctx.emit_opcode("OP_DUP"),
247 StackOp::Swap => ctx.emit_opcode("OP_SWAP"),
248 StackOp::Roll { .. } => ctx.emit_opcode("OP_ROLL"),
249 StackOp::Pick { .. } => ctx.emit_opcode("OP_PICK"),
250 StackOp::Drop => ctx.emit_opcode("OP_DROP"),
251 StackOp::Nip => ctx.emit_opcode("OP_NIP"),
252 StackOp::Over => ctx.emit_opcode("OP_OVER"),
253 StackOp::Rot => ctx.emit_opcode("OP_ROT"),
254 StackOp::Tuck => ctx.emit_opcode("OP_TUCK"),
255 StackOp::Opcode(code) => ctx.emit_opcode(code),
256 StackOp::If {
257 then_ops,
258 else_ops,
259 } => emit_if(then_ops, else_ops, ctx),
260 StackOp::Placeholder {
261 param_index,
262 param_name,
263 } => {
264 ctx.emit_placeholder(*param_index, param_name);
265 Ok(())
266 }
267 }
268}
269
270fn emit_if(
271 then_ops: &[StackOp],
272 else_ops: &[StackOp],
273 ctx: &mut EmitContext,
274) -> Result<(), String> {
275 ctx.emit_opcode("OP_IF")?;
276
277 for op in then_ops {
278 emit_stack_op(op, ctx)?;
279 }
280
281 if !else_ops.is_empty() {
282 ctx.emit_opcode("OP_ELSE")?;
283 for op in else_ops {
284 emit_stack_op(op, ctx)?;
285 }
286 }
287
288 ctx.emit_opcode("OP_ENDIF")
289}
290
291pub fn emit(methods: &[StackMethod]) -> Result<EmitResult, String> {
307 let mut ctx = EmitContext::new();
308
309 let public_methods: Vec<StackMethod> = methods
311 .iter()
312 .filter(|m| m.name != "constructor")
313 .cloned()
314 .collect();
315
316 if public_methods.is_empty() {
317 return Ok(EmitResult {
318 script_hex: String::new(),
319 script_asm: String::new(),
320 constructor_slots: Vec::new(),
321 code_separator_index: -1,
322 code_separator_indices: Vec::new(),
323 });
324 }
325
326 if public_methods.len() == 1 {
327 for op in &public_methods[0].ops {
328 emit_stack_op(op, &mut ctx)?;
329 }
330 } else {
331 let refs: Vec<&StackMethod> = public_methods.iter().collect();
332 emit_method_dispatch(&refs, &mut ctx)?;
333 }
334
335 Ok(EmitResult {
336 script_hex: ctx.get_hex(),
337 script_asm: ctx.get_asm(),
338 constructor_slots: ctx.constructor_slots,
339 code_separator_index: ctx.code_separator_index,
340 code_separator_indices: ctx.code_separator_indices,
341 })
342}
343
344fn emit_method_dispatch(
345 methods: &[&StackMethod],
346 ctx: &mut EmitContext,
347) -> Result<(), String> {
348 for (i, method) in methods.iter().enumerate() {
349 let is_last = i == methods.len() - 1;
350
351 if !is_last {
352 ctx.emit_opcode("OP_DUP")?;
353 ctx.emit_push(&PushValue::Int(i as i128));
354 ctx.emit_opcode("OP_NUMEQUAL")?;
355 ctx.emit_opcode("OP_IF")?;
356 ctx.emit_opcode("OP_DROP")?;
357 } else {
358 ctx.emit_push(&PushValue::Int(i as i128));
360 ctx.emit_opcode("OP_NUMEQUALVERIFY")?;
361 }
362
363 for op in &method.ops {
364 emit_stack_op(op, ctx)?;
365 }
366
367 if !is_last {
368 ctx.emit_opcode("OP_ELSE")?;
369 }
370 }
371
372 for _ in 0..methods.len() - 1 {
374 ctx.emit_opcode("OP_ENDIF")?;
375 }
376
377 Ok(())
378}
379
380pub fn emit_method(method: &StackMethod) -> Result<EmitResult, String> {
382 let mut ctx = EmitContext::new();
383 for op in &method.ops {
384 emit_stack_op(op, &mut ctx)?;
385 }
386 Ok(EmitResult {
387 script_hex: ctx.get_hex(),
388 script_asm: ctx.get_asm(),
389 constructor_slots: ctx.constructor_slots,
390 code_separator_index: ctx.code_separator_index,
391 code_separator_indices: ctx.code_separator_indices,
392 })
393}
394
395#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn test_emit_placeholder_produces_constructor_slot() {
405 let method = StackMethod {
406 name: "unlock".to_string(),
407 ops: vec![StackOp::Placeholder {
408 param_index: 0,
409 param_name: "pubKeyHash".to_string(),
410 }],
411 max_stack_depth: 1,
412 };
413
414 let result = emit_method(&method).expect("emit should succeed");
415 assert_eq!(
416 result.constructor_slots.len(),
417 1,
418 "should produce exactly one constructor slot"
419 );
420 assert_eq!(result.constructor_slots[0].param_index, 0);
421 assert_eq!(result.constructor_slots[0].byte_offset, 0);
422 }
423
424 #[test]
425 fn test_multiple_placeholders_produce_distinct_byte_offsets() {
426 let method = StackMethod {
427 name: "test".to_string(),
428 ops: vec![
429 StackOp::Placeholder {
430 param_index: 0,
431 param_name: "a".to_string(),
432 },
433 StackOp::Placeholder {
434 param_index: 1,
435 param_name: "b".to_string(),
436 },
437 ],
438 max_stack_depth: 2,
439 };
440
441 let result = emit_method(&method).expect("emit should succeed");
442 assert_eq!(
443 result.constructor_slots.len(),
444 2,
445 "should produce two constructor slots"
446 );
447
448 assert_eq!(result.constructor_slots[0].param_index, 0);
450 assert_eq!(result.constructor_slots[0].byte_offset, 0);
451
452 assert_eq!(result.constructor_slots[1].param_index, 1);
454 assert_eq!(result.constructor_slots[1].byte_offset, 1);
455
456 assert_ne!(
458 result.constructor_slots[0].byte_offset,
459 result.constructor_slots[1].byte_offset
460 );
461 }
462
463 #[test]
464 fn test_placeholder_byte_offset_position_is_op_0() {
465 let method = StackMethod {
466 name: "test".to_string(),
467 ops: vec![
468 StackOp::Push(PushValue::Int(42)), StackOp::Placeholder {
470 param_index: 0,
471 param_name: "x".to_string(),
472 },
473 ],
474 max_stack_depth: 2,
475 };
476
477 let result = emit_method(&method).expect("emit should succeed");
478 assert_eq!(result.constructor_slots.len(), 1);
479
480 let slot = &result.constructor_slots[0];
481 let hex = &result.script_hex;
482
483 let byte_hex = &hex[slot.byte_offset * 2..slot.byte_offset * 2 + 2];
485 assert_eq!(
486 byte_hex, "00",
487 "expected OP_0 at placeholder byte offset {}, got '{}' in hex '{}'",
488 slot.byte_offset, byte_hex, hex
489 );
490 }
491
492 #[test]
493 fn test_emit_single_method_produces_hex_and_asm() {
494 use super::super::optimizer::optimize_stack_ops;
495
496 let method = StackMethod {
497 name: "check".to_string(),
498 ops: vec![
499 StackOp::Push(PushValue::Int(42)),
500 StackOp::Opcode("OP_NUMEQUAL".to_string()),
501 StackOp::Opcode("OP_VERIFY".to_string()),
502 ],
503 max_stack_depth: 1,
504 };
505
506 let optimized_method = StackMethod {
508 name: method.name.clone(),
509 ops: optimize_stack_ops(&method.ops),
510 max_stack_depth: method.max_stack_depth,
511 };
512
513 let result = emit(&[optimized_method]).expect("emit should succeed");
514 assert!(!result.script_hex.is_empty(), "hex should not be empty");
515 assert!(!result.script_asm.is_empty(), "asm should not be empty");
516 assert!(
517 result.script_asm.contains("OP_NUMEQUALVERIFY"),
518 "standalone peephole optimizer should combine OP_NUMEQUAL + OP_VERIFY into OP_NUMEQUALVERIFY, got: {}",
519 result.script_asm
520 );
521 }
522
523 #[test]
524 fn test_emit_empty_methods_produces_empty_output() {
525 let result = emit(&[]).expect("emit with no methods should succeed");
526 assert!(
527 result.script_hex.is_empty(),
528 "empty methods should produce empty hex"
529 );
530 assert!(
531 result.constructor_slots.is_empty(),
532 "empty methods should produce no constructor slots"
533 );
534 }
535
536 #[test]
537 fn test_emit_push_bool_values() {
538 let method = StackMethod {
539 name: "test".to_string(),
540 ops: vec![
541 StackOp::Push(PushValue::Bool(true)),
542 StackOp::Push(PushValue::Bool(false)),
543 ],
544 max_stack_depth: 2,
545 };
546
547 let result = emit_method(&method).expect("emit should succeed");
548 assert!(
550 result.script_hex.starts_with("51"),
551 "true should emit 0x51, got: {}",
552 result.script_hex
553 );
554 assert!(
555 result.script_hex.ends_with("00"),
556 "false should emit 0x00, got: {}",
557 result.script_hex
558 );
559 assert!(result.script_asm.contains("OP_TRUE"));
560 assert!(result.script_asm.contains("OP_FALSE"));
561 }
562
563 #[test]
568 fn test_byte_offset_with_push_data() {
569 let method = StackMethod {
572 name: "check".to_string(),
573 ops: vec![
574 StackOp::Push(PushValue::Int(17)), StackOp::Placeholder {
576 param_index: 0,
577 param_name: "x".to_string(),
578 },
579 StackOp::Opcode("OP_ADD".to_string()),
580 ],
581 max_stack_depth: 2,
582 };
583
584 let result = emit_method(&method).expect("emit should succeed");
585 assert_eq!(
586 result.constructor_slots.len(),
587 1,
588 "expected 1 constructor slot"
589 );
590
591 let slot = &result.constructor_slots[0];
592 assert_eq!(
594 slot.byte_offset, 2,
595 "expected byteOffset=2 (after push 17 = 2 bytes), got {}",
596 slot.byte_offset
597 );
598 }
599
600 #[test]
605 fn test_simple_sequence_hex() {
606 let method = StackMethod {
607 name: "check".to_string(),
608 ops: vec![
609 StackOp::Opcode("OP_DUP".to_string()),
610 StackOp::Opcode("OP_HASH160".to_string()),
611 ],
612 max_stack_depth: 1,
613 };
614
615 let result = emit_method(&method).expect("emit should succeed");
616 assert_eq!(
618 result.script_hex, "76a9",
619 "expected hex '76a9' for DUP+HASH160, got: {}",
620 result.script_hex
621 );
622 }
623
624 #[test]
629 fn test_peephole_optimization_applied() {
630 use super::super::optimizer::optimize_stack_ops;
631
632 let ops = vec![
633 StackOp::Opcode("OP_CHECKSIG".to_string()),
634 StackOp::Opcode("OP_VERIFY".to_string()),
635 StackOp::Opcode("OP_1".to_string()),
636 ];
637
638 let optimized_ops = optimize_stack_ops(&ops);
639 let method = StackMethod {
640 name: "check".to_string(),
641 ops: optimized_ops,
642 max_stack_depth: 1,
643 };
644
645 let result = emit_method(&method).expect("emit should succeed");
646
647 assert_eq!(
649 result.script_hex, "ad51",
650 "expected 'ad51' (CHECKSIGVERIFY + OP_1) after peephole, got: {}",
651 result.script_hex
652 );
653 assert!(
654 result.script_asm.contains("OP_CHECKSIGVERIFY"),
655 "expected OP_CHECKSIGVERIFY in ASM, got: {}",
656 result.script_asm
657 );
658 }
659
660 #[test]
665 fn test_multi_method_dispatch_produces_if_else() {
666 use super::super::stack::lower_to_stack;
667 use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFValue};
668
669 let program = ANFProgram {
670 contract_name: "Multi".to_string(),
671 properties: vec![],
672 methods: vec![
673 ANFMethod {
674 name: "constructor".to_string(),
675 params: vec![],
676 body: vec![],
677 is_public: false,
678 },
679 ANFMethod {
680 name: "m1".to_string(),
681 params: vec![ANFParam {
682 name: "x".to_string(),
683 param_type: "bigint".to_string(),
684 }],
685 body: vec![
686 ANFBinding {
687 name: "t0".to_string(),
688 value: ANFValue::LoadParam { name: "x".to_string() },
689 },
690 ANFBinding {
691 name: "t1".to_string(),
692 value: ANFValue::LoadConst {
693 value: serde_json::Value::Number(serde_json::Number::from(1)),
694 },
695 },
696 ANFBinding {
697 name: "t2".to_string(),
698 value: ANFValue::BinOp {
699 op: "===".to_string(),
700 left: "t0".to_string(),
701 right: "t1".to_string(),
702 result_type: None,
703 },
704 },
705 ANFBinding {
706 name: "t3".to_string(),
707 value: ANFValue::Assert { value: "t2".to_string() },
708 },
709 ],
710 is_public: true,
711 },
712 ANFMethod {
713 name: "m2".to_string(),
714 params: vec![ANFParam {
715 name: "y".to_string(),
716 param_type: "bigint".to_string(),
717 }],
718 body: vec![
719 ANFBinding {
720 name: "t0".to_string(),
721 value: ANFValue::LoadParam { name: "y".to_string() },
722 },
723 ANFBinding {
724 name: "t1".to_string(),
725 value: ANFValue::LoadConst {
726 value: serde_json::Value::Number(serde_json::Number::from(2)),
727 },
728 },
729 ANFBinding {
730 name: "t2".to_string(),
731 value: ANFValue::BinOp {
732 op: "===".to_string(),
733 left: "t0".to_string(),
734 right: "t1".to_string(),
735 result_type: None,
736 },
737 },
738 ANFBinding {
739 name: "t3".to_string(),
740 value: ANFValue::Assert { value: "t2".to_string() },
741 },
742 ],
743 is_public: true,
744 },
745 ],
746 };
747
748 let methods = lower_to_stack(&program).expect("lower_to_stack should succeed");
749
750 use super::super::optimizer::optimize_stack_ops;
752 let optimized: Vec<StackMethod> = methods
753 .iter()
754 .map(|m| StackMethod {
755 name: m.name.clone(),
756 ops: optimize_stack_ops(&m.ops),
757 max_stack_depth: m.max_stack_depth,
758 })
759 .collect();
760
761 let result = emit(&optimized).expect("emit should succeed");
762
763 assert!(
765 result.script_asm.contains("OP_IF"),
766 "expected OP_IF in multi-method dispatch, got: {}",
767 result.script_asm
768 );
769 assert!(
770 result.script_asm.contains("OP_ELSE"),
771 "expected OP_ELSE in multi-method dispatch, got: {}",
772 result.script_asm
773 );
774 assert!(
775 result.script_asm.contains("OP_ENDIF"),
776 "expected OP_ENDIF in multi-method dispatch, got: {}",
777 result.script_asm
778 );
779 }
780
781 #[test]
787 fn test_byte_offset_accounts_for_preceding_opcodes() {
788 let method = StackMethod {
791 name: "check".to_string(),
792 ops: vec![
793 StackOp::Opcode("OP_DUP".to_string()), StackOp::Opcode("OP_HASH160".to_string()), StackOp::Placeholder {
796 param_index: 0,
797 param_name: "pubKeyHash".to_string(),
798 },
799 StackOp::Opcode("OP_EQUALVERIFY".to_string()),
800 StackOp::Opcode("OP_CHECKSIG".to_string()),
801 ],
802 max_stack_depth: 2,
803 };
804
805 let result = emit_method(&method).expect("emit should succeed");
806 assert_eq!(result.constructor_slots.len(), 1, "expected 1 constructor slot");
807
808 let slot = &result.constructor_slots[0];
809 assert_eq!(
811 slot.byte_offset, 2,
812 "expected byteOffset=2 (after OP_DUP + OP_HASH160), got {}",
813 slot.byte_offset
814 );
815 }
816
817 #[test]
823 fn test_full_p2pkh() {
824 use super::super::stack::lower_to_stack;
825 use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFProperty, ANFValue};
826
827 let program = ANFProgram {
828 contract_name: "P2PKH".to_string(),
829 properties: vec![ANFProperty {
830 name: "pubKeyHash".to_string(),
831 prop_type: "Addr".to_string(),
832 readonly: true,
833 initial_value: None,
834 }],
835 methods: vec![ANFMethod {
836 name: "unlock".to_string(),
837 params: vec![
838 ANFParam { name: "sig".to_string(), param_type: "Sig".to_string() },
839 ANFParam { name: "pubKey".to_string(), param_type: "PubKey".to_string() },
840 ],
841 body: vec![
842 ANFBinding {
843 name: "t0".to_string(),
844 value: ANFValue::LoadParam { name: "pubKey".to_string() },
845 },
846 ANFBinding {
847 name: "t1".to_string(),
848 value: ANFValue::Call {
849 func: "hash160".to_string(),
850 args: vec!["t0".to_string()],
851 },
852 },
853 ANFBinding {
854 name: "t2".to_string(),
855 value: ANFValue::LoadProp { name: "pubKeyHash".to_string() },
856 },
857 ANFBinding {
858 name: "t3".to_string(),
859 value: ANFValue::BinOp {
860 op: "===".to_string(),
861 left: "t1".to_string(),
862 right: "t2".to_string(),
863 result_type: Some("bytes".to_string()),
864 },
865 },
866 ANFBinding {
867 name: "t4".to_string(),
868 value: ANFValue::Assert { value: "t3".to_string() },
869 },
870 ANFBinding {
871 name: "t5".to_string(),
872 value: ANFValue::LoadParam { name: "sig".to_string() },
873 },
874 ANFBinding {
875 name: "t6".to_string(),
876 value: ANFValue::LoadParam { name: "pubKey".to_string() },
877 },
878 ANFBinding {
879 name: "t7".to_string(),
880 value: ANFValue::Call {
881 func: "checkSig".to_string(),
882 args: vec!["t5".to_string(), "t6".to_string()],
883 },
884 },
885 ANFBinding {
886 name: "t8".to_string(),
887 value: ANFValue::Assert { value: "t7".to_string() },
888 },
889 ],
890 is_public: true,
891 }],
892 };
893
894 let stack_methods = lower_to_stack(&program).expect("stack lowering should succeed");
895 let result = emit(&stack_methods).expect("emit should succeed");
896
897 assert!(
898 !result.script_hex.is_empty(),
899 "P2PKH should produce non-empty script hex"
900 );
901 assert!(
902 !result.constructor_slots.is_empty(),
903 "P2PKH should have at least one constructor slot for pubKeyHash"
904 );
905 }
906
907 #[test]
912 fn test_m10_integer_17_uses_push_prefix_not_op17() {
913 let method = StackMethod {
914 name: "test".to_string(),
915 ops: vec![StackOp::Push(PushValue::Int(17))],
916 max_stack_depth: 1,
917 };
918 let result = emit_method(&method).expect("emit should succeed");
919 assert!(
921 !result.script_hex.starts_with("61"),
922 "17 should NOT be encoded as OP_17 (0x61); OP_1..OP_16 are for 1–16 only. got: {}",
923 result.script_hex
924 );
925 assert!(
927 result.script_hex.contains("11"),
928 "17 (0x11) should appear in the script hex; got: {}",
929 result.script_hex
930 );
931 }
932
933 #[test]
939 fn test_m12_256_byte_data_uses_pushdata2() {
940 let data = vec![0xabu8; 256];
941 let method = StackMethod {
942 name: "test".to_string(),
943 ops: vec![StackOp::Push(PushValue::Bytes(data))],
944 max_stack_depth: 1,
945 };
946 let result = emit_method(&method).expect("emit should succeed");
947 assert!(
949 result.script_hex.starts_with("4d0001"),
950 "256-byte push should use OP_PUSHDATA2 prefix '4d0001', got: {}",
951 &result.script_hex[..result.script_hex.len().min(12)]
952 );
953 }
954
955 #[test]
960 fn test_m19_sha256_contract_has_op_sha256_in_asm() {
961 use super::super::stack::lower_to_stack;
962 use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFValue};
963
964 let program = ANFProgram {
965 contract_name: "Sha256Test".to_string(),
966 properties: vec![],
967 methods: vec![ANFMethod {
968 name: "check".to_string(),
969 params: vec![ANFParam {
970 name: "data".to_string(),
971 param_type: "ByteString".to_string(),
972 }],
973 body: vec![
974 ANFBinding {
975 name: "t0".to_string(),
976 value: ANFValue::LoadParam { name: "data".to_string() },
977 },
978 ANFBinding {
979 name: "t1".to_string(),
980 value: ANFValue::Call {
981 func: "sha256".to_string(),
982 args: vec!["t0".to_string()],
983 },
984 },
985 ANFBinding {
986 name: "t2".to_string(),
987 value: ANFValue::Assert { value: "t1".to_string() },
988 },
989 ],
990 is_public: true,
991 }],
992 };
993
994 let stack_methods = lower_to_stack(&program).expect("stack lowering should succeed");
995 let result = emit(&stack_methods).expect("emit should succeed");
996 assert!(
997 result.script_asm.contains("OP_SHA256"),
998 "sha256() call should produce OP_SHA256 in ASM; got: {}",
999 result.script_asm
1000 );
1001 }
1002
1003 #[test]
1008 fn test_m21_op_dup_encodes_0x76() {
1009 let method = StackMethod {
1010 name: "test".to_string(),
1011 ops: vec![StackOp::Dup],
1012 max_stack_depth: 1,
1013 };
1014 let result = emit_method(&method).expect("emit should succeed");
1015 assert_eq!(
1016 result.script_hex, "76",
1017 "OP_DUP should encode as 0x76; got: {}",
1018 result.script_hex
1019 );
1020 }
1021
1022 #[test]
1027 fn test_m22_op_swap_encodes_0x7c() {
1028 let method = StackMethod {
1029 name: "test".to_string(),
1030 ops: vec![StackOp::Swap],
1031 max_stack_depth: 2,
1032 };
1033 let result = emit_method(&method).expect("emit should succeed");
1034 assert_eq!(
1035 result.script_hex, "7c",
1036 "OP_SWAP should encode as 0x7c; got: {}",
1037 result.script_hex
1038 );
1039 }
1040
1041 #[test]
1046 fn test_m24_if_without_else_no_op_else() {
1047 let method = StackMethod {
1048 name: "test".to_string(),
1049 ops: vec![StackOp::If {
1050 then_ops: vec![StackOp::Opcode("OP_DROP".to_string())],
1051 else_ops: vec![],
1052 }],
1053 max_stack_depth: 1,
1054 };
1055 let result = emit_method(&method).expect("emit should succeed");
1056 assert!(
1057 !result.script_asm.contains("OP_ELSE"),
1058 "if with empty else branch should NOT contain OP_ELSE; got asm: {}",
1059 result.script_asm
1060 );
1061 assert!(
1062 result.script_asm.contains("OP_IF"),
1063 "should still contain OP_IF; got asm: {}",
1064 result.script_asm
1065 );
1066 }
1067
1068 #[test]
1073 fn test_m25_single_method_no_dispatch() {
1074 use super::super::stack::lower_to_stack;
1075 use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFProperty, ANFValue};
1076
1077 let program = ANFProgram {
1079 contract_name: "Single".to_string(),
1080 properties: vec![ANFProperty {
1081 name: "x".to_string(),
1082 prop_type: "bigint".to_string(),
1083 readonly: true,
1084 initial_value: None,
1085 }],
1086 methods: vec![
1087 ANFMethod {
1088 name: "constructor".to_string(),
1089 params: vec![ANFParam {
1090 name: "x".to_string(),
1091 param_type: "bigint".to_string(),
1092 }],
1093 body: vec![],
1094 is_public: false,
1095 },
1096 ANFMethod {
1097 name: "check".to_string(),
1098 params: vec![ANFParam {
1099 name: "v".to_string(),
1100 param_type: "bigint".to_string(),
1101 }],
1102 body: vec![
1103 ANFBinding {
1104 name: "t0".to_string(),
1105 value: ANFValue::LoadParam { name: "v".to_string() },
1106 },
1107 ANFBinding {
1108 name: "t1".to_string(),
1109 value: ANFValue::LoadProp { name: "x".to_string() },
1110 },
1111 ANFBinding {
1112 name: "t2".to_string(),
1113 value: ANFValue::BinOp {
1114 op: "===".to_string(),
1115 left: "t0".to_string(),
1116 right: "t1".to_string(),
1117 result_type: None,
1118 },
1119 },
1120 ANFBinding {
1121 name: "t3".to_string(),
1122 value: ANFValue::Assert { value: "t2".to_string() },
1123 },
1124 ],
1125 is_public: true,
1126 },
1127 ],
1128 };
1129
1130 let stack_methods = lower_to_stack(&program).expect("stack lowering should succeed");
1131 let result = emit(&stack_methods).expect("emit should succeed");
1132
1133 assert!(
1136 !result.script_asm.contains("OP_IF"),
1137 "single public method should NOT produce OP_IF dispatch; got asm: {}",
1138 result.script_asm
1139 );
1140 }
1141}