Skip to main content

runar_compiler_rust/codegen/
stack.rs

1//! Pass 5: Stack Lower -- converts ANF IR to Stack IR.
2//!
3//! The fundamental challenge: ANF uses named temporaries but Bitcoin Script
4//! operates on an anonymous stack. We maintain a "stack map" that tracks
5//! which named value lives at which stack position, then emit PICK/ROLL/DUP
6//! operations to shuttle values to the top when they are needed.
7//!
8//! This matches the TypeScript reference compiler and aligned Go compiler:
9//! - Private methods are inlined at call sites rather than compiled separately
10//! - Constructor is skipped
11//! - @ref: aliases are handled via PICK (non-consuming copy)
12//! - @this is a compile-time placeholder (push 0)
13//! - super() is a no-op at stack level
14
15use std::collections::{HashMap, HashSet};
16
17use crate::ir::{ANFBinding, ANFMethod, ANFProgram, ANFProperty, ANFValue, ConstValue};
18
19// ---------------------------------------------------------------------------
20// Constants
21// ---------------------------------------------------------------------------
22
23const MAX_STACK_DEPTH: usize = 800;
24
25// ---------------------------------------------------------------------------
26// Stack IR types
27// ---------------------------------------------------------------------------
28
29/// A single stack-machine operation.
30#[derive(Debug, Clone)]
31pub enum StackOp {
32    Push(PushValue),
33    Dup,
34    Swap,
35    Roll { depth: usize },
36    Pick { depth: usize },
37    Drop,
38    Nip,
39    Over,
40    Rot,
41    Tuck,
42    Opcode(String),
43    If {
44        then_ops: Vec<StackOp>,
45        else_ops: Vec<StackOp>,
46    },
47    Placeholder {
48        param_index: usize,
49        param_name: String,
50    },
51    PushCodeSepIndex,
52}
53
54/// Typed value for push operations.
55#[derive(Debug, Clone)]
56pub enum PushValue {
57    Bool(bool),
58    Int(i128),
59    Bytes(Vec<u8>),
60}
61
62/// A stack-lowered method.
63#[derive(Debug, Clone)]
64pub struct StackMethod {
65    pub name: String,
66    pub ops: Vec<StackOp>,
67    pub max_stack_depth: usize,
68    /// Parallel to `ops`: optional source location for each stack operation.
69    /// Used for generating source maps in the emit phase.
70    pub source_locs: Vec<Option<crate::ir::SourceLocation>>,
71}
72
73// ---------------------------------------------------------------------------
74// Builtin function -> opcode mapping
75// ---------------------------------------------------------------------------
76
77fn is_ec_builtin(name: &str) -> bool {
78    matches!(
79        name,
80        "ecAdd"
81            | "ecMul"
82            | "ecMulGen"
83            | "ecNegate"
84            | "ecOnCurve"
85            | "ecModReduce"
86            | "ecEncodeCompressed"
87            | "ecMakePoint"
88            | "ecPointX"
89            | "ecPointY"
90    )
91}
92
93fn builtin_opcodes(name: &str) -> Option<Vec<&'static str>> {
94    match name {
95        "sha256" => Some(vec!["OP_SHA256"]),
96        "ripemd160" => Some(vec!["OP_RIPEMD160"]),
97        "hash160" => Some(vec!["OP_HASH160"]),
98        "hash256" => Some(vec!["OP_HASH256"]),
99        "checkSig" => Some(vec!["OP_CHECKSIG"]),
100        "checkMultiSig" => Some(vec!["OP_CHECKMULTISIG"]),
101        "len" => Some(vec!["OP_SIZE"]),
102        "cat" => Some(vec!["OP_CAT"]),
103        "num2bin" => Some(vec!["OP_NUM2BIN"]),
104        "bin2num" => Some(vec!["OP_BIN2NUM"]),
105        "abs" => Some(vec!["OP_ABS"]),
106        "min" => Some(vec!["OP_MIN"]),
107        "max" => Some(vec!["OP_MAX"]),
108        "within" => Some(vec!["OP_WITHIN"]),
109        "split" => Some(vec!["OP_SPLIT"]),
110        "left" => Some(vec!["OP_SPLIT", "OP_DROP"]),
111        "int2str" => Some(vec!["OP_NUM2BIN"]),
112        "bool" => Some(vec!["OP_0NOTEQUAL"]),
113        "unpack" => Some(vec!["OP_BIN2NUM"]),
114        _ => None,
115    }
116}
117
118fn binop_opcodes(op: &str) -> Option<Vec<&'static str>> {
119    match op {
120        "+" => Some(vec!["OP_ADD"]),
121        "-" => Some(vec!["OP_SUB"]),
122        "*" => Some(vec!["OP_MUL"]),
123        "/" => Some(vec!["OP_DIV"]),
124        "%" => Some(vec!["OP_MOD"]),
125        "===" => Some(vec!["OP_NUMEQUAL"]),
126        "!==" => Some(vec!["OP_NUMEQUAL", "OP_NOT"]),
127        "<" => Some(vec!["OP_LESSTHAN"]),
128        ">" => Some(vec!["OP_GREATERTHAN"]),
129        "<=" => Some(vec!["OP_LESSTHANOREQUAL"]),
130        ">=" => Some(vec!["OP_GREATERTHANOREQUAL"]),
131        "&&" => Some(vec!["OP_BOOLAND"]),
132        "||" => Some(vec!["OP_BOOLOR"]),
133        "&" => Some(vec!["OP_AND"]),
134        "|" => Some(vec!["OP_OR"]),
135        "^" => Some(vec!["OP_XOR"]),
136        "<<" => Some(vec!["OP_LSHIFT"]),
137        ">>" => Some(vec!["OP_RSHIFT"]),
138        _ => None,
139    }
140}
141
142fn unaryop_opcodes(op: &str) -> Option<Vec<&'static str>> {
143    match op {
144        "!" => Some(vec!["OP_NOT"]),
145        "-" => Some(vec!["OP_NEGATE"]),
146        "~" => Some(vec!["OP_INVERT"]),
147        _ => None,
148    }
149}
150
151// ---------------------------------------------------------------------------
152// Stack map
153// ---------------------------------------------------------------------------
154
155/// Tracks named values on the stack. Index 0 is the bottom; last is the top.
156/// Empty string means anonymous/consumed slot.
157#[derive(Debug, Clone)]
158struct StackMap {
159    slots: Vec<String>,
160}
161
162impl StackMap {
163    fn new(initial: &[String]) -> Self {
164        StackMap {
165            slots: initial.to_vec(),
166        }
167    }
168
169    fn depth(&self) -> usize {
170        self.slots.len()
171    }
172
173    fn push(&mut self, name: &str) {
174        self.slots.push(name.to_string());
175    }
176
177    fn pop(&mut self) -> String {
178        self.slots.pop().expect("stack underflow")
179    }
180
181    fn find_depth(&self, name: &str) -> Option<usize> {
182        for (i, slot) in self.slots.iter().enumerate().rev() {
183            if slot == name {
184                return Some(self.slots.len() - 1 - i);
185            }
186        }
187        None
188    }
189
190    fn has(&self, name: &str) -> bool {
191        self.slots.iter().any(|s| s == name)
192    }
193
194    fn remove_at_depth(&mut self, depth_from_top: usize) -> String {
195        let index = self.slots.len() - 1 - depth_from_top;
196        self.slots.remove(index)
197    }
198
199    fn peek_at_depth(&self, depth_from_top: usize) -> &str {
200        let index = self.slots.len() - 1 - depth_from_top;
201        &self.slots[index]
202    }
203
204    fn rename_at_depth(&mut self, depth_from_top: usize, new_name: &str) {
205        let idx = self.slots.len() - 1 - depth_from_top;
206        self.slots[idx] = new_name.to_string();
207    }
208
209    fn swap(&mut self) {
210        let n = self.slots.len();
211        assert!(n >= 2, "stack underflow on swap");
212        self.slots.swap(n - 1, n - 2);
213    }
214
215    fn dup(&mut self) {
216        assert!(!self.slots.is_empty(), "stack underflow on dup");
217        let top = self.slots.last().unwrap().clone();
218        self.slots.push(top);
219    }
220
221    /// Get the set of all non-empty slot names.
222    fn named_slots(&self) -> HashSet<String> {
223        self.slots.iter().filter(|s| !s.is_empty()).cloned().collect()
224    }
225}
226
227// ---------------------------------------------------------------------------
228// Use analysis
229// ---------------------------------------------------------------------------
230
231fn compute_last_uses(bindings: &[ANFBinding]) -> HashMap<String, usize> {
232    let mut last_use = HashMap::new();
233    for (i, binding) in bindings.iter().enumerate() {
234        for r in collect_refs(&binding.value) {
235            last_use.insert(r, i);
236        }
237    }
238    last_use
239}
240
241fn collect_refs(value: &ANFValue) -> Vec<String> {
242    let mut refs = Vec::new();
243    match value {
244        ANFValue::LoadParam { name } => {
245            // Track param name so last-use analysis keeps the param on the stack
246            // (via PICK) until its final load_param, then consumes it (via ROLL).
247            refs.push(name.clone());
248        }
249        ANFValue::LoadProp { .. }
250        | ANFValue::GetStateScript { .. } => {}
251
252        ANFValue::LoadConst { value: v } => {
253            // load_const with @ref: values reference another binding
254            if let Some(s) = v.as_str() {
255                if s.len() > 5 && &s[..5] == "@ref:" {
256                    refs.push(s[5..].to_string());
257                }
258            }
259        }
260
261        ANFValue::BinOp { left, right, .. } => {
262            refs.push(left.clone());
263            refs.push(right.clone());
264        }
265        ANFValue::UnaryOp { operand, .. } => {
266            refs.push(operand.clone());
267        }
268        ANFValue::Call { args, .. } => {
269            refs.extend(args.iter().cloned());
270        }
271        ANFValue::MethodCall { object, args, .. } => {
272            refs.push(object.clone());
273            refs.extend(args.iter().cloned());
274        }
275        ANFValue::If {
276            cond,
277            then,
278            else_branch,
279        } => {
280            refs.push(cond.clone());
281            for b in then {
282                refs.extend(collect_refs(&b.value));
283            }
284            for b in else_branch {
285                refs.extend(collect_refs(&b.value));
286            }
287        }
288        ANFValue::Loop { body, .. } => {
289            for b in body {
290                refs.extend(collect_refs(&b.value));
291            }
292        }
293        ANFValue::Assert { value } => {
294            refs.push(value.clone());
295        }
296        ANFValue::UpdateProp { value, .. } => {
297            refs.push(value.clone());
298        }
299        ANFValue::CheckPreimage { preimage } => {
300            refs.push(preimage.clone());
301        }
302        ANFValue::DeserializeState { preimage } => {
303            refs.push(preimage.clone());
304        }
305        ANFValue::AddOutput { satoshis, state_values, preimage } => {
306            refs.push(satoshis.clone());
307            refs.extend(state_values.iter().cloned());
308            if !preimage.is_empty() {
309                refs.push(preimage.clone());
310            }
311        }
312        ANFValue::AddRawOutput { satoshis, script_bytes } => {
313            refs.push(satoshis.clone());
314            refs.push(script_bytes.clone());
315        }
316        ANFValue::ArrayLiteral { elements } => {
317            refs.extend(elements.iter().cloned());
318        }
319    }
320    refs
321}
322
323// ---------------------------------------------------------------------------
324// Lowering context
325// ---------------------------------------------------------------------------
326
327struct LoweringContext {
328    sm: StackMap,
329    ops: Vec<StackOp>,
330    /// Parallel to `ops`: source location for each emitted op.
331    source_locs: Vec<Option<crate::ir::SourceLocation>>,
332    max_depth: usize,
333    properties: Vec<ANFProperty>,
334    private_methods: HashMap<String, ANFMethod>,
335    /// Binding names defined in the current lowerBindings scope.
336    /// Used by @ref: handler to decide whether to consume (local) or copy (outer-scope).
337    local_bindings: HashSet<String>,
338    /// Parent-scope refs that must not be consumed (used after current if-branch).
339    outer_protected_refs: Option<HashSet<String>>,
340    /// True when executing inside an if-branch. update_prop skips old-value
341    /// removal so that the same-property detection in lower_if can handle it.
342    inside_branch: bool,
343    /// Current source location from the ANF binding being lowered.
344    current_source_loc: Option<crate::ir::SourceLocation>,
345}
346
347impl LoweringContext {
348    fn new(params: &[String], properties: &[ANFProperty]) -> Self {
349        let mut ctx = LoweringContext {
350            sm: StackMap::new(params),
351            ops: Vec::new(),
352            source_locs: Vec::new(),
353            max_depth: 0,
354            properties: properties.to_vec(),
355            private_methods: HashMap::new(),
356            local_bindings: HashSet::new(),
357            outer_protected_refs: None,
358            inside_branch: false,
359            current_source_loc: None,
360        };
361        ctx.track_depth();
362        ctx
363    }
364
365    fn track_depth(&mut self) {
366        if self.sm.depth() > self.max_depth {
367            self.max_depth = self.sm.depth();
368        }
369    }
370
371    fn emit_op(&mut self, op: StackOp) {
372        self.ops.push(op);
373        self.source_locs.push(self.current_source_loc.clone());
374        self.track_depth();
375    }
376
377    /// Emit a Bitcoin varint encoding of the length on top of the stack.
378    ///
379    /// Expects stack: `[..., script, len]`
380    /// Leaves stack:  `[..., script, varint_bytes]`
381    ///
382    /// OP_NUM2BIN uses sign-magnitude encoding where values 128-255 need 2 bytes
383    /// (sign bit). To produce a correct 1-byte unsigned varint, we use
384    /// OP_NUM2BIN 2 then SPLIT to extract only the low byte.
385    /// Similarly for 2-byte unsigned varint, we use OP_NUM2BIN 4 then SPLIT.
386    fn emit_varint_encoding(&mut self) {
387        // Stack: [..., script, len]
388        self.emit_op(StackOp::Dup); // [script, len, len]
389        self.sm.dup();
390        self.emit_op(StackOp::Push(PushValue::Int(253))); // [script, len, len, 253]
391        self.sm.push("");
392        self.emit_op(StackOp::Opcode("OP_LESSTHAN".into())); // [script, len, isSmall]
393        self.sm.pop();
394        self.sm.pop();
395        self.sm.push("");
396
397        self.emit_op(StackOp::Opcode("OP_IF".into()));
398        self.sm.pop(); // pop condition
399
400        // Then: 1-byte varint (len < 253)
401        // Use NUM2BIN 2 to avoid sign-magnitude issue for values 128-252,
402        // then take only the first (low) byte via SPLIT.
403        self.emit_op(StackOp::Push(PushValue::Int(2))); // [script, len, 2]
404        self.sm.push("");
405        self.emit_op(StackOp::Opcode("OP_NUM2BIN".into())); // [script, len_2bytes]
406        self.sm.pop();
407        self.sm.pop();
408        self.sm.push("");
409        self.emit_op(StackOp::Push(PushValue::Int(1))); // [script, len_2bytes, 1]
410        self.sm.push("");
411        self.emit_op(StackOp::Opcode("OP_SPLIT".into())); // [script, lowByte, highByte]
412        self.sm.pop();
413        self.sm.pop();
414        self.sm.push(""); // lowByte
415        self.sm.push(""); // highByte
416        self.emit_op(StackOp::Drop); // [script, lowByte]
417        self.sm.pop();
418
419        self.emit_op(StackOp::Opcode("OP_ELSE".into()));
420
421        // Else: 0xfd + 2-byte LE varint (len >= 253)
422        // Use NUM2BIN 4 to avoid sign-magnitude issue for values >= 32768,
423        // then take only the first 2 (low) bytes via SPLIT.
424        self.emit_op(StackOp::Push(PushValue::Int(4))); // [script, len, 4]
425        self.sm.push("");
426        self.emit_op(StackOp::Opcode("OP_NUM2BIN".into())); // [script, len_4bytes]
427        self.sm.pop();
428        self.sm.pop();
429        self.sm.push("");
430        self.emit_op(StackOp::Push(PushValue::Int(2))); // [script, len_4bytes, 2]
431        self.sm.push("");
432        self.emit_op(StackOp::Opcode("OP_SPLIT".into())); // [script, low2bytes, high2bytes]
433        self.sm.pop();
434        self.sm.pop();
435        self.sm.push(""); // low2bytes
436        self.sm.push(""); // high2bytes
437        self.emit_op(StackOp::Drop); // [script, low2bytes]
438        self.sm.pop();
439        self.emit_op(StackOp::Push(PushValue::Bytes(vec![0xfd])));
440        self.sm.push("");
441        self.emit_op(StackOp::Swap);
442        self.sm.swap();
443        self.sm.pop();
444        self.sm.pop();
445        self.emit_op(StackOp::Opcode("OP_CAT".into()));
446        self.sm.push("");
447
448        self.emit_op(StackOp::Opcode("OP_ENDIF".into()));
449        // --- Stack: [..., script, varint] ---
450    }
451
452    /// Emit push-data encoding for a ByteString value on top of the stack.
453    ///
454    /// Expects stack: [..., bs_value]
455    /// Leaves stack:  [..., pushdata_encoded_value]
456    fn emit_push_data_encode(&mut self) {
457        self.emit_op(StackOp::Opcode("OP_SIZE".into()));
458        self.sm.push("");
459        self.emit_op(StackOp::Dup);
460        self.sm.push("");
461        self.emit_op(StackOp::Push(PushValue::Int(76)));
462        self.sm.push("");
463        self.emit_op(StackOp::Opcode("OP_LESSTHAN".into()));
464        self.sm.pop(); self.sm.pop();
465        self.sm.push("");
466
467        self.emit_op(StackOp::Opcode("OP_IF".into()));
468        self.sm.pop();
469        let sm_after_outer_if = self.sm.clone();
470
471        // THEN: len <= 75
472        self.emit_op(StackOp::Push(PushValue::Int(2)));
473        self.sm.push("");
474        self.emit_op(StackOp::Opcode("OP_NUM2BIN".into()));
475        self.sm.pop(); self.sm.pop();
476        self.sm.push("");
477        self.emit_op(StackOp::Push(PushValue::Int(1)));
478        self.sm.push("");
479        self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
480        self.sm.pop(); self.sm.pop();
481        self.sm.push(""); self.sm.push("");
482        self.emit_op(StackOp::Drop); self.sm.pop();
483        self.emit_op(StackOp::Swap); self.sm.swap();
484        self.sm.pop(); self.sm.pop();
485        self.emit_op(StackOp::Opcode("OP_CAT".into()));
486        self.sm.push("");
487        let sm_end_target = self.sm.clone();
488
489        self.emit_op(StackOp::Opcode("OP_ELSE".into()));
490        self.sm = sm_after_outer_if.clone();
491
492        self.emit_op(StackOp::Dup);
493        self.sm.push("");
494        self.emit_op(StackOp::Push(PushValue::Int(256)));
495        self.sm.push("");
496        self.emit_op(StackOp::Opcode("OP_LESSTHAN".into()));
497        self.sm.pop(); self.sm.pop();
498        self.sm.push("");
499
500        self.emit_op(StackOp::Opcode("OP_IF".into()));
501        self.sm.pop();
502        let sm_after_inner_if = self.sm.clone();
503
504        // THEN: 76-255 → 0x4c + 1-byte
505        self.emit_op(StackOp::Push(PushValue::Int(2)));
506        self.sm.push("");
507        self.emit_op(StackOp::Opcode("OP_NUM2BIN".into()));
508        self.sm.pop(); self.sm.pop();
509        self.sm.push("");
510        self.emit_op(StackOp::Push(PushValue::Int(1)));
511        self.sm.push("");
512        self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
513        self.sm.pop(); self.sm.pop();
514        self.sm.push(""); self.sm.push("");
515        self.emit_op(StackOp::Drop); self.sm.pop();
516        self.emit_op(StackOp::Push(PushValue::Bytes(vec![0x4c])));
517        self.sm.push("");
518        self.emit_op(StackOp::Swap); self.sm.swap();
519        self.sm.pop(); self.sm.pop();
520        self.emit_op(StackOp::Opcode("OP_CAT".into()));
521        self.sm.push("");
522        self.emit_op(StackOp::Swap); self.sm.swap();
523        self.sm.pop(); self.sm.pop();
524        self.emit_op(StackOp::Opcode("OP_CAT".into()));
525        self.sm.push("");
526
527        self.emit_op(StackOp::Opcode("OP_ELSE".into()));
528        self.sm = sm_after_inner_if;
529
530        // ELSE: >= 256 → 0x4d + 2-byte LE
531        self.emit_op(StackOp::Push(PushValue::Int(4)));
532        self.sm.push("");
533        self.emit_op(StackOp::Opcode("OP_NUM2BIN".into()));
534        self.sm.pop(); self.sm.pop();
535        self.sm.push("");
536        self.emit_op(StackOp::Push(PushValue::Int(2)));
537        self.sm.push("");
538        self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
539        self.sm.pop(); self.sm.pop();
540        self.sm.push(""); self.sm.push("");
541        self.emit_op(StackOp::Drop); self.sm.pop();
542        self.emit_op(StackOp::Push(PushValue::Bytes(vec![0x4d])));
543        self.sm.push("");
544        self.emit_op(StackOp::Swap); self.sm.swap();
545        self.sm.pop(); self.sm.pop();
546        self.emit_op(StackOp::Opcode("OP_CAT".into()));
547        self.sm.push("");
548        self.emit_op(StackOp::Swap); self.sm.swap();
549        self.sm.pop(); self.sm.pop();
550        self.emit_op(StackOp::Opcode("OP_CAT".into()));
551        self.sm.push("");
552
553        self.emit_op(StackOp::Opcode("OP_ENDIF".into()));
554        self.emit_op(StackOp::Opcode("OP_ENDIF".into()));
555        self.sm = sm_end_target;
556    }
557
558    /// Emit push-data decoding for a ByteString state field.
559    ///
560    /// Expects stack: [..., state_bytes]
561    /// Leaves stack:  [..., data, remaining_state]
562    fn emit_push_data_decode(&mut self) {
563        self.emit_op(StackOp::Push(PushValue::Int(1)));
564        self.sm.push("");
565        self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
566        self.sm.pop(); self.sm.pop();
567        self.sm.push(""); self.sm.push("");
568        self.emit_op(StackOp::Swap); self.sm.swap();
569        self.emit_op(StackOp::Opcode("OP_BIN2NUM".into()));
570        self.emit_op(StackOp::Dup);
571        self.sm.push("");
572        self.emit_op(StackOp::Push(PushValue::Int(76)));
573        self.sm.push("");
574        self.emit_op(StackOp::Opcode("OP_LESSTHAN".into()));
575        self.sm.pop(); self.sm.pop();
576        self.sm.push("");
577
578        self.emit_op(StackOp::Opcode("OP_IF".into()));
579        self.sm.pop();
580        let sm_after_outer_if = self.sm.clone();
581
582        // THEN: fb < 76 → direct length
583        self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
584        self.sm.pop(); self.sm.pop();
585        self.sm.push(""); self.sm.push("");
586        let sm_end_target = self.sm.clone();
587
588        self.emit_op(StackOp::Opcode("OP_ELSE".into()));
589        self.sm = sm_after_outer_if.clone();
590
591        self.emit_op(StackOp::Dup);
592        self.sm.push("");
593        self.emit_op(StackOp::Push(PushValue::Int(77)));
594        self.sm.push("");
595        self.emit_op(StackOp::Opcode("OP_NUMEQUAL".into()));
596        self.sm.pop(); self.sm.pop();
597        self.sm.push("");
598
599        self.emit_op(StackOp::Opcode("OP_IF".into()));
600        self.sm.pop();
601        let sm_after_inner_if = self.sm.clone();
602
603        // THEN: fb == 77 → 2-byte LE
604        self.emit_op(StackOp::Drop); self.sm.pop();
605        self.emit_op(StackOp::Push(PushValue::Int(2)));
606        self.sm.push("");
607        self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
608        self.sm.pop(); self.sm.pop();
609        self.sm.push(""); self.sm.push("");
610        self.emit_op(StackOp::Swap); self.sm.swap();
611        self.emit_op(StackOp::Opcode("OP_BIN2NUM".into()));
612        self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
613        self.sm.pop(); self.sm.pop();
614        self.sm.push(""); self.sm.push("");
615
616        self.emit_op(StackOp::Opcode("OP_ELSE".into()));
617        self.sm = sm_after_inner_if;
618
619        // ELSE: fb == 76 → 1-byte
620        self.emit_op(StackOp::Drop); self.sm.pop();
621        self.emit_op(StackOp::Push(PushValue::Int(1)));
622        self.sm.push("");
623        self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
624        self.sm.pop(); self.sm.pop();
625        self.sm.push(""); self.sm.push("");
626        self.emit_op(StackOp::Swap); self.sm.swap();
627        self.emit_op(StackOp::Opcode("OP_BIN2NUM".into()));
628        self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
629        self.sm.pop(); self.sm.pop();
630        self.sm.push(""); self.sm.push("");
631
632        self.emit_op(StackOp::Opcode("OP_ENDIF".into()));
633        self.emit_op(StackOp::Opcode("OP_ENDIF".into()));
634        self.sm = sm_end_target;
635    }
636
637    fn is_last_use(&self, name: &str, current_index: usize, last_uses: &HashMap<String, usize>) -> bool {
638        match last_uses.get(name) {
639            None => true,
640            Some(&last) => last <= current_index,
641        }
642    }
643
644    fn bring_to_top(&mut self, name: &str, consume: bool) {
645        let depth = self
646            .sm
647            .find_depth(name)
648            .unwrap_or_else(|| panic!("value '{}' not found on stack", name));
649
650        if depth == 0 {
651            if !consume {
652                self.emit_op(StackOp::Dup);
653                self.sm.dup();
654            }
655            return;
656        }
657
658        if depth == 1 && consume {
659            self.emit_op(StackOp::Swap);
660            self.sm.swap();
661            return;
662        }
663
664        if consume {
665            if depth == 2 {
666                self.emit_op(StackOp::Rot);
667                let removed = self.sm.remove_at_depth(2);
668                self.sm.push(&removed);
669            } else {
670                self.emit_op(StackOp::Push(PushValue::Int(depth as i128)));
671                self.sm.push(""); // temporary depth literal
672                self.emit_op(StackOp::Roll { depth });
673                self.sm.pop(); // remove depth literal
674                let rolled = self.sm.remove_at_depth(depth);
675                self.sm.push(&rolled);
676            }
677        } else {
678            if depth == 1 {
679                self.emit_op(StackOp::Over);
680                let picked = self.sm.peek_at_depth(1).to_string();
681                self.sm.push(&picked);
682            } else {
683                self.emit_op(StackOp::Push(PushValue::Int(depth as i128)));
684                self.sm.push(""); // temporary
685                self.emit_op(StackOp::Pick { depth });
686                self.sm.pop(); // remove depth literal
687                let picked = self.sm.peek_at_depth(depth).to_string();
688                self.sm.push(&picked);
689            }
690        }
691
692        self.track_depth();
693    }
694
695    /// Drain branch-private residue from below TOS at the end of a branch
696    /// body, so both branches converge to a layout the parent stack model can
697    /// faithfully describe before OP_ENDIF (issue #36).
698    ///
699    /// A slot is residue when its name is NOT in `pre_if_names` (the snapshot
700    /// of the parent's named slots taken before the branch ran). This catches
701    /// both anonymous slots (empty-named, pushed by intrinsics like substr's
702    /// OP_SPLIT residue) and named branch-local bindings that lingered past
703    /// their last-use (e.g. dead-code load_const intermediates the optimizer
704    /// didn't fold).
705    ///
706    /// Slots whose name was already in `pre_if_names` are kept — including
707    /// duplicates created by reassigning an outer-scope local from inside the
708    /// branch. The TOS slot is also kept regardless.
709    fn drain_branch_private_residue(&mut self, pre_if_names: &HashSet<String>) {
710        let mut drain_depths: Vec<usize> = Vec::new();
711        for d in 1..self.sm.depth() {
712            let name = self.sm.peek_at_depth(d);
713            if name.is_empty() {
714                drain_depths.push(d);
715            } else if !pre_if_names.contains(name) {
716                drain_depths.push(d);
717            }
718        }
719        if drain_depths.is_empty() {
720            return;
721        }
722        drain_depths.sort_by(|a, b| b.cmp(a));
723        for depth in drain_depths {
724            if depth == 1 {
725                self.emit_op(StackOp::Nip);
726                self.sm.remove_at_depth(1);
727            } else {
728                self.emit_op(StackOp::Push(PushValue::Int(depth as i128)));
729                self.sm.push("");
730                self.emit_op(StackOp::Roll { depth });
731                self.sm.pop();
732                let rolled = self.sm.remove_at_depth(depth);
733                self.sm.push(&rolled);
734                self.emit_op(StackOp::Drop);
735                self.sm.pop();
736            }
737        }
738    }
739
740    // -----------------------------------------------------------------------
741    // Lower bindings
742    // -----------------------------------------------------------------------
743
744    fn lower_bindings(&mut self, bindings: &[ANFBinding], terminal_assert: bool) {
745        self.local_bindings = bindings.iter().map(|b| b.name.clone()).collect();
746        let mut last_uses = compute_last_uses(bindings);
747
748        // Protect parent-scope refs that are still needed after this scope
749        if let Some(ref protected) = self.outer_protected_refs {
750            for r in protected {
751                last_uses.insert(r.clone(), bindings.len());
752            }
753        }
754
755        // Find the terminal binding index (if terminal_assert is set).
756        // If the last binding is an 'if' whose branches end in asserts,
757        // that 'if' is the terminal point (not an earlier standalone assert).
758        let mut last_assert_idx: isize = -1;
759        let mut terminal_if_idx: isize = -1;
760        if terminal_assert {
761            let last_binding = bindings.last();
762            if let Some(b) = last_binding {
763                if matches!(&b.value, ANFValue::If { .. }) {
764                    terminal_if_idx = (bindings.len() - 1) as isize;
765                } else {
766                    for i in (0..bindings.len()).rev() {
767                        if matches!(&bindings[i].value, ANFValue::Assert { .. }) {
768                            last_assert_idx = i as isize;
769                            break;
770                        }
771                    }
772                }
773            }
774        }
775
776        for (i, binding) in bindings.iter().enumerate() {
777            // Propagate source location from ANF binding to StackOps
778            self.current_source_loc = binding.source_loc.clone();
779
780            if matches!(&binding.value, ANFValue::Assert { .. }) && i as isize == last_assert_idx {
781                // Terminal assert: leave value on stack instead of OP_VERIFY
782                if let ANFValue::Assert { value } = &binding.value {
783                    self.lower_assert(value, i, &last_uses, true);
784                }
785            } else if matches!(&binding.value, ANFValue::If { .. }) && i as isize == terminal_if_idx {
786                // Terminal if: propagate terminalAssert into both branches
787                if let ANFValue::If { cond, then, else_branch } = &binding.value {
788                    self.lower_if(&binding.name, cond, then, else_branch, i, &last_uses, true);
789                }
790            } else {
791                self.lower_binding(binding, i, &last_uses);
792            }
793        }
794    }
795
796    fn lower_binding(
797        &mut self,
798        binding: &ANFBinding,
799        binding_index: usize,
800        last_uses: &HashMap<String, usize>,
801    ) {
802        let name = &binding.name;
803        match &binding.value {
804            ANFValue::LoadParam {
805                name: param_name, ..
806            } => {
807                self.lower_load_param(name, param_name, binding_index, last_uses);
808            }
809            ANFValue::LoadProp {
810                name: prop_name, ..
811            } => {
812                self.lower_load_prop(name, prop_name);
813            }
814            ANFValue::LoadConst { .. } => {
815                self.lower_load_const(name, &binding.value, binding_index, last_uses);
816            }
817            ANFValue::BinOp {
818                op, left, right, result_type, ..
819            } => {
820                self.lower_bin_op(name, op, left, right, binding_index, last_uses, result_type.as_deref());
821            }
822            ANFValue::UnaryOp { op, operand, .. } => {
823                self.lower_unary_op(name, op, operand, binding_index, last_uses);
824            }
825            ANFValue::Call {
826                func: func_name,
827                args,
828            } => {
829                self.lower_call(name, func_name, args, binding_index, last_uses);
830            }
831            ANFValue::MethodCall {
832                object,
833                method,
834                args,
835            } => {
836                self.lower_method_call(name, object, method, args, binding_index, last_uses);
837            }
838            ANFValue::If {
839                cond,
840                then,
841                else_branch,
842            } => {
843                self.lower_if(name, cond, then, else_branch, binding_index, last_uses, false);
844            }
845            ANFValue::Loop {
846                count,
847                body,
848                iter_var,
849            } => {
850                self.lower_loop(name, *count, body, iter_var);
851            }
852            ANFValue::Assert { value } => {
853                self.lower_assert(value, binding_index, last_uses, false);
854            }
855            ANFValue::UpdateProp {
856                name: prop_name,
857                value,
858            } => {
859                self.lower_update_prop(prop_name, value, binding_index, last_uses);
860            }
861            ANFValue::GetStateScript {} => {
862                self.lower_get_state_script(name);
863            }
864            ANFValue::CheckPreimage { preimage } => {
865                self.lower_check_preimage(name, preimage, binding_index, last_uses);
866            }
867            ANFValue::DeserializeState { preimage } => {
868                self.lower_deserialize_state(preimage, binding_index, last_uses);
869            }
870            ANFValue::AddOutput { satoshis, state_values, preimage } => {
871                self.lower_add_output(name, satoshis, state_values, preimage, binding_index, last_uses);
872            }
873            ANFValue::AddRawOutput { satoshis, script_bytes } => {
874                self.lower_add_raw_output(name, satoshis, script_bytes, binding_index, last_uses);
875            }
876            ANFValue::ArrayLiteral { elements } => {
877                self.lower_array_literal(name, elements, binding_index, last_uses);
878            }
879        }
880    }
881
882    // -----------------------------------------------------------------------
883    // Individual lowering methods
884    // -----------------------------------------------------------------------
885
886    fn lower_load_param(
887        &mut self,
888        binding_name: &str,
889        param_name: &str,
890        binding_index: usize,
891        last_uses: &HashMap<String, usize>,
892    ) {
893        if self.sm.has(param_name) {
894            let is_last = self.is_last_use(param_name, binding_index, last_uses);
895            self.bring_to_top(param_name, is_last);
896            self.sm.pop();
897            self.sm.push(binding_name);
898        } else {
899            self.emit_op(StackOp::Push(PushValue::Int(0)));
900            self.sm.push(binding_name);
901        }
902    }
903
904    fn lower_load_prop(&mut self, binding_name: &str, prop_name: &str) {
905        let prop = self.properties.iter().find(|p| p.name == prop_name).cloned();
906
907        if self.sm.has(prop_name) {
908            // Property has been updated (via update_prop) — use the stack value.
909            // Must check this BEFORE initial_value — after update_prop, we need the
910            // updated value, not the original constant.
911            self.bring_to_top(prop_name, false);
912            self.sm.pop();
913        } else if let Some(ref p) = prop {
914            if let Some(ref val) = p.initial_value {
915                self.push_json_value(val);
916            } else {
917                // Property value will be provided at deployment time; emit a placeholder.
918                // The emitter records byte offsets so the SDK can splice in real values.
919                let param_index = self
920                    .properties
921                    .iter()
922                    .position(|p2| p2.name == prop_name)
923                    .unwrap_or(0);
924                self.emit_op(StackOp::Placeholder {
925                    param_index,
926                    param_name: prop_name.to_string(),
927                });
928            }
929        } else {
930            // Property not found and not on stack — emit placeholder with index 0.
931            let param_index = self
932                .properties
933                .iter()
934                .position(|p2| p2.name == prop_name)
935                .unwrap_or(0);
936            self.emit_op(StackOp::Placeholder {
937                param_index,
938                param_name: prop_name.to_string(),
939            });
940        }
941        self.sm.push(binding_name);
942    }
943
944    fn push_json_value(&mut self, val: &serde_json::Value) {
945        match val {
946            serde_json::Value::Bool(b) => {
947                self.emit_op(StackOp::Push(PushValue::Bool(*b)));
948            }
949            serde_json::Value::Number(n) => {
950                let i = n.as_i64().map(|v| v as i128).unwrap_or(0);
951                self.emit_op(StackOp::Push(PushValue::Int(i)));
952            }
953            serde_json::Value::String(s) => {
954                let bytes = hex_to_bytes(s);
955                self.emit_op(StackOp::Push(PushValue::Bytes(bytes)));
956            }
957            _ => {
958                self.emit_op(StackOp::Push(PushValue::Int(0)));
959            }
960        }
961    }
962
963    fn lower_load_const(&mut self, binding_name: &str, value: &ANFValue, binding_index: usize, last_uses: &HashMap<String, usize>) {
964        // Handle @ref: aliases (ANF variable aliasing)
965        // When a load_const has a string value starting with "@ref:", it's an alias
966        // to another binding. We bring that value to the top via PICK (non-consuming)
967        // unless this is the last use, in which case we consume it via ROLL.
968        if let Some(ConstValue::Str(ref s)) = value.const_value() {
969            if s.len() > 5 && &s[..5] == "@ref:" {
970                let ref_name = &s[5..];
971                if self.sm.has(ref_name) {
972                    // Only consume (ROLL) if the ref target is a local binding in the
973                    // current scope. Outer-scope refs must be copied (PICK) so that the
974                    // parent stackMap stays in sync (critical for IfElse branches and
975                    // BoundedLoop iterations).
976                    let consume = self.local_bindings.contains(ref_name)
977                        && self.is_last_use(ref_name, binding_index, last_uses);
978                    self.bring_to_top(ref_name, consume);
979                    self.sm.pop();
980                    self.sm.push(binding_name);
981                } else {
982                    // Referenced value not on stack -- push a placeholder
983                    self.emit_op(StackOp::Push(PushValue::Int(0)));
984                    self.sm.push(binding_name);
985                }
986                return;
987            }
988            // Handle @this marker -- compile-time concept, not a runtime value
989            if s == "@this" {
990                self.emit_op(StackOp::Push(PushValue::Int(0)));
991                self.sm.push(binding_name);
992                return;
993            }
994        }
995
996        match value.const_value() {
997            Some(ConstValue::Bool(b)) => {
998                self.emit_op(StackOp::Push(PushValue::Bool(b)));
999            }
1000            Some(ConstValue::Int(n)) => {
1001                self.emit_op(StackOp::Push(PushValue::Int(n)));
1002            }
1003            Some(ConstValue::Str(s)) => {
1004                let bytes = hex_to_bytes(&s);
1005                self.emit_op(StackOp::Push(PushValue::Bytes(bytes)));
1006            }
1007            None => {
1008                self.emit_op(StackOp::Push(PushValue::Int(0)));
1009            }
1010        }
1011        self.sm.push(binding_name);
1012    }
1013
1014    fn lower_bin_op(
1015        &mut self,
1016        binding_name: &str,
1017        op: &str,
1018        left: &str,
1019        right: &str,
1020        binding_index: usize,
1021        last_uses: &HashMap<String, usize>,
1022        result_type: Option<&str>,
1023    ) {
1024        let left_is_last = self.is_last_use(left, binding_index, last_uses);
1025        self.bring_to_top(left, left_is_last);
1026
1027        let right_is_last = self.is_last_use(right, binding_index, last_uses);
1028        self.bring_to_top(right, right_is_last);
1029
1030        self.sm.pop();
1031        self.sm.pop();
1032
1033        // For equality operators, choose OP_EQUAL vs OP_NUMEQUAL based on operand type.
1034        // For addition, choose OP_CAT vs OP_ADD based on operand type.
1035        if result_type == Some("bytes") && op == "+" {
1036            self.emit_op(StackOp::Opcode("OP_CAT".to_string()));
1037        } else if result_type == Some("bytes") && (op == "===" || op == "!==") {
1038            self.emit_op(StackOp::Opcode("OP_EQUAL".to_string()));
1039            if op == "!==" {
1040                self.emit_op(StackOp::Opcode("OP_NOT".to_string()));
1041            }
1042        } else {
1043            let codes = binop_opcodes(op)
1044                .unwrap_or_else(|| panic!("unknown binary operator: {}", op));
1045            for code in codes {
1046                self.emit_op(StackOp::Opcode(code.to_string()));
1047            }
1048        }
1049
1050        self.sm.push(binding_name);
1051        self.track_depth();
1052    }
1053
1054    fn lower_unary_op(
1055        &mut self,
1056        binding_name: &str,
1057        op: &str,
1058        operand: &str,
1059        binding_index: usize,
1060        last_uses: &HashMap<String, usize>,
1061    ) {
1062        let is_last = self.is_last_use(operand, binding_index, last_uses);
1063        self.bring_to_top(operand, is_last);
1064
1065        self.sm.pop();
1066
1067        let codes = unaryop_opcodes(op)
1068            .unwrap_or_else(|| panic!("unknown unary operator: {}", op));
1069        for code in codes {
1070            self.emit_op(StackOp::Opcode(code.to_string()));
1071        }
1072
1073        self.sm.push(binding_name);
1074        self.track_depth();
1075    }
1076
1077    fn lower_call(
1078        &mut self,
1079        binding_name: &str,
1080        func_name: &str,
1081        args: &[String],
1082        binding_index: usize,
1083        last_uses: &HashMap<String, usize>,
1084    ) {
1085        // Special handling for assert
1086        if func_name == "assert" {
1087            if !args.is_empty() {
1088                let is_last = self.is_last_use(&args[0], binding_index, last_uses);
1089                self.bring_to_top(&args[0], is_last);
1090                self.sm.pop();
1091                self.emit_op(StackOp::Opcode("OP_VERIFY".to_string()));
1092                self.sm.push(binding_name);
1093            }
1094            return;
1095        }
1096
1097        // super() in constructor -- no opcode emission needed.
1098        // Constructor args are already on the stack.
1099        if func_name == "super" {
1100            self.sm.push(binding_name);
1101            return;
1102        }
1103
1104        // checkMultiSig(sigs, pks) — special handling for OP_CHECKMULTISIG.
1105        if func_name == "checkMultiSig" && args.len() == 2 {
1106            self.lower_check_multi_sig(binding_name, args, binding_index, last_uses);
1107            return;
1108        }
1109
1110        if func_name == "__array_access" {
1111            self.lower_array_access(binding_name, args, binding_index, last_uses);
1112            return;
1113        }
1114
1115        if func_name == "reverseBytes" {
1116            self.lower_reverse_bytes(binding_name, args, binding_index, last_uses);
1117            return;
1118        }
1119
1120        if func_name == "substr" {
1121            self.lower_substr(binding_name, args, binding_index, last_uses);
1122            return;
1123        }
1124
1125        if func_name == "verifyRabinSig" {
1126            self.lower_verify_rabin_sig(binding_name, args, binding_index, last_uses);
1127            return;
1128        }
1129
1130        if func_name == "verifyWOTS" {
1131            self.lower_verify_wots(binding_name, args, binding_index, last_uses);
1132            return;
1133        }
1134
1135        if func_name.starts_with("verifySLHDSA_") {
1136            let param_key = func_name.trim_start_matches("verifySLHDSA_");
1137            self.lower_verify_slh_dsa(binding_name, param_key, args, binding_index, last_uses);
1138            return;
1139        }
1140
1141        if func_name == "sha256Compress" {
1142            self.lower_sha256_compress(binding_name, args, binding_index, last_uses);
1143            return;
1144        }
1145
1146        if func_name == "sha256Finalize" {
1147            self.lower_sha256_finalize(binding_name, args, binding_index, last_uses);
1148            return;
1149        }
1150
1151        if func_name == "blake3Compress" {
1152            self.lower_blake3_compress(binding_name, args, binding_index, last_uses);
1153            return;
1154        }
1155
1156        if func_name == "blake3Hash" {
1157            self.lower_blake3_hash(binding_name, args, binding_index, last_uses);
1158            return;
1159        }
1160
1161        if is_ec_builtin(func_name) {
1162            self.lower_ec_builtin(binding_name, func_name, args, binding_index, last_uses);
1163            return;
1164        }
1165
1166        if func_name == "safediv" {
1167            self.lower_safediv(binding_name, args, binding_index, last_uses);
1168            return;
1169        }
1170
1171        if func_name == "safemod" {
1172            self.lower_safemod(binding_name, args, binding_index, last_uses);
1173            return;
1174        }
1175
1176        if func_name == "clamp" {
1177            self.lower_clamp(binding_name, args, binding_index, last_uses);
1178            return;
1179        }
1180
1181        if func_name == "pow" {
1182            self.lower_pow(binding_name, args, binding_index, last_uses);
1183            return;
1184        }
1185
1186        if func_name == "mulDiv" {
1187            self.lower_mul_div(binding_name, args, binding_index, last_uses);
1188            return;
1189        }
1190
1191        if func_name == "percentOf" {
1192            self.lower_percent_of(binding_name, args, binding_index, last_uses);
1193            return;
1194        }
1195
1196        if func_name == "sqrt" {
1197            self.lower_sqrt(binding_name, args, binding_index, last_uses);
1198            return;
1199        }
1200
1201        if func_name == "gcd" {
1202            self.lower_gcd(binding_name, args, binding_index, last_uses);
1203            return;
1204        }
1205
1206        if func_name == "divmod" {
1207            self.lower_divmod(binding_name, args, binding_index, last_uses);
1208            return;
1209        }
1210
1211        if func_name == "log2" {
1212            self.lower_log2(binding_name, args, binding_index, last_uses);
1213            return;
1214        }
1215
1216        if func_name == "sign" {
1217            self.lower_sign(binding_name, args, binding_index, last_uses);
1218            return;
1219        }
1220
1221        if func_name == "right" {
1222            self.lower_right(binding_name, args, binding_index, last_uses);
1223            return;
1224        }
1225
1226        // pack and toByteString are no-ops: the value is already on the stack in
1227        // the correct representation. We just consume the arg and rename.
1228        if func_name == "pack" || func_name == "toByteString" {
1229            if !args.is_empty() {
1230                let is_last = self.is_last_use(&args[0], binding_index, last_uses);
1231                self.bring_to_top(&args[0], is_last);
1232                self.sm.pop();
1233            }
1234            self.sm.push(binding_name);
1235            return;
1236        }
1237
1238        // computeStateOutputHash(preimage, stateBytes) — builds full BIP-143 output
1239        // serialization for single-output stateful continuation, then hashes it.
1240        if func_name == "computeStateOutputHash" {
1241            self.lower_compute_state_output_hash(binding_name, args, binding_index, last_uses);
1242            return;
1243        }
1244
1245        // computeStateOutput(preimage, stateBytes) — same as computeStateOutputHash
1246        // but returns raw output bytes WITHOUT hashing. Used when the output bytes
1247        // need to be concatenated with a change output before hashing.
1248        if func_name == "computeStateOutput" {
1249            self.lower_compute_state_output(binding_name, args, binding_index, last_uses);
1250            return;
1251        }
1252
1253        // buildChangeOutput(pkh, amount) — builds a P2PKH output serialization:
1254        //   amount(8LE) + varint(25) + OP_DUP OP_HASH160 OP_PUSHBYTES_20 <pkh> OP_EQUALVERIFY OP_CHECKSIG
1255        //   = amount(8LE) + 0x19 + 76a914 <pkh:20> 88ac
1256        if func_name == "buildChangeOutput" {
1257            self.lower_build_change_output(binding_name, args, binding_index, last_uses);
1258            return;
1259        }
1260
1261        // Preimage field extractors — each needs a custom OP_SPLIT sequence
1262        // because OP_SPLIT produces two stack values and the intermediate stack
1263        // management cannot be expressed in the simple builtin_opcodes table.
1264        if func_name.starts_with("extract") {
1265            self.lower_extractor(binding_name, func_name, args, binding_index, last_uses);
1266            return;
1267        }
1268
1269        // General builtin: push args in order, then emit opcodes
1270        for arg in args {
1271            let is_last = self.is_last_use(arg, binding_index, last_uses);
1272            self.bring_to_top(arg, is_last);
1273        }
1274
1275        for _ in args {
1276            self.sm.pop();
1277        }
1278
1279        if let Some(codes) = builtin_opcodes(func_name) {
1280            for code in codes {
1281                self.emit_op(StackOp::Opcode(code.to_string()));
1282            }
1283        } else {
1284            // Unknown function -- push a placeholder
1285            self.emit_op(StackOp::Push(PushValue::Int(0)));
1286            self.sm.push(binding_name);
1287            return;
1288        }
1289
1290        if func_name == "split" {
1291            self.sm.push("");
1292            self.sm.push(binding_name);
1293        } else if func_name == "len" {
1294            self.sm.push("");
1295            self.sm.push(binding_name);
1296        } else {
1297            self.sm.push(binding_name);
1298        }
1299
1300        self.track_depth();
1301    }
1302
1303    fn lower_method_call(
1304        &mut self,
1305        binding_name: &str,
1306        object: &str,
1307        method: &str,
1308        args: &[String],
1309        binding_index: usize,
1310        last_uses: &HashMap<String, usize>,
1311    ) {
1312        // Consume the @this object reference — it's a compile-time concept,
1313        // not a runtime value. Without this, 0n stays on the stack.
1314        if self.sm.has(object) {
1315            self.bring_to_top(object, true);
1316            self.emit_op(StackOp::Drop);
1317            self.sm.pop();
1318        }
1319
1320        if method == "getStateScript" {
1321            self.lower_get_state_script(binding_name);
1322            return;
1323        }
1324
1325        // Check if this is a private method call that should be inlined
1326        if let Some(private_method) = self.private_methods.get(method).cloned() {
1327            self.inline_method_call(binding_name, &private_method, args, binding_index, last_uses);
1328            return;
1329        }
1330
1331        // For other method calls, treat like a function call
1332        self.lower_call(binding_name, method, args, binding_index, last_uses);
1333    }
1334
1335    /// Inline a private method by lowering its body in the current context.
1336    /// The method's parameters are bound to the call arguments.
1337    fn inline_method_call(
1338        &mut self,
1339        binding_name: &str,
1340        method: &ANFMethod,
1341        args: &[String],
1342        binding_index: usize,
1343        last_uses: &HashMap<String, usize>,
1344    ) {
1345        // Track shadowed names so we can restore them after the body runs.
1346        // When a param name already exists on the stack, temporarily rename
1347        // the existing entry to avoid duplicate names which break Set-based
1348        // branch reconciliation in lower_if.
1349        let mut shadowed: Vec<(String, String)> = Vec::new();
1350
1351        // Bind call arguments to private method params.
1352        for (i, arg) in args.iter().enumerate() {
1353            if i < method.params.len() {
1354                let is_last = self.is_last_use(arg, binding_index, last_uses);
1355                self.bring_to_top(arg, is_last);
1356                self.sm.pop();
1357
1358                let param_name = &method.params[i].name;
1359
1360                // If param_name already exists on the stack, shadow it by renaming
1361                // the existing entry to prevent duplicate-name issues.
1362                if self.sm.has(param_name) {
1363                    let existing_depth = self.sm.find_depth(param_name).unwrap();
1364                    let shadowed_name = format!("__shadowed_{}_{}", binding_index, param_name);
1365                    self.sm.rename_at_depth(existing_depth, &shadowed_name);
1366                    shadowed.push((param_name.clone(), shadowed_name));
1367                }
1368
1369                // Rename to param name
1370                self.sm.push(param_name);
1371            }
1372        }
1373
1374        // Lower the method body
1375        self.lower_bindings(&method.body, false);
1376
1377        // Restore shadowed names so the caller's scope sees its original entries.
1378        for (param_name, shadowed_name) in &shadowed {
1379            if self.sm.has(shadowed_name) {
1380                let depth = self.sm.find_depth(shadowed_name).unwrap();
1381                self.sm.rename_at_depth(depth, param_name);
1382            }
1383        }
1384
1385        // The last binding's result should be on top of the stack.
1386        // Rename it to the calling binding name.
1387        if !method.body.is_empty() {
1388            let last_binding_name = &method.body[method.body.len() - 1].name;
1389            if self.sm.depth() > 0 {
1390                let top_name = self.sm.peek_at_depth(0).to_string();
1391                if top_name == *last_binding_name {
1392                    self.sm.pop();
1393                    self.sm.push(binding_name);
1394                }
1395            }
1396        }
1397    }
1398
1399    fn lower_if(
1400        &mut self,
1401        binding_name: &str,
1402        cond: &str,
1403        then_bindings: &[ANFBinding],
1404        else_bindings: &[ANFBinding],
1405        binding_index: usize,
1406        last_uses: &HashMap<String, usize>,
1407        terminal_assert: bool,
1408    ) {
1409        let is_last = self.is_last_use(cond, binding_index, last_uses);
1410        self.bring_to_top(cond, is_last);
1411        self.sm.pop(); // OP_IF consumes condition
1412
1413        // Identify parent-scope items still needed after this if-expression.
1414        let mut protected_refs = HashSet::new();
1415        for (ref_name, &last_idx) in last_uses.iter() {
1416            if last_idx > binding_index && self.sm.has(ref_name) {
1417                protected_refs.insert(ref_name.clone());
1418            }
1419        }
1420
1421        // Snapshot parent stackMap names before branches run
1422        let pre_if_names = self.sm.named_slots();
1423
1424        // Lower then-branch
1425        let mut then_ctx = LoweringContext::new(&[], &self.properties);
1426        then_ctx.sm = self.sm.clone();
1427        then_ctx.outer_protected_refs = Some(protected_refs.clone());
1428        then_ctx.inside_branch = true;
1429        then_ctx.lower_bindings(then_bindings, terminal_assert);
1430
1431        then_ctx.drain_branch_private_residue(&pre_if_names);
1432
1433        if terminal_assert && then_ctx.sm.depth() > 1 {
1434            let excess = then_ctx.sm.depth() - 1;
1435            for _ in 0..excess {
1436                then_ctx.emit_op(StackOp::Nip);
1437                then_ctx.sm.remove_at_depth(1);
1438            }
1439        }
1440
1441        // Lower else-branch
1442        let mut else_ctx = LoweringContext::new(&[], &self.properties);
1443        else_ctx.sm = self.sm.clone();
1444        else_ctx.outer_protected_refs = Some(protected_refs);
1445        else_ctx.inside_branch = true;
1446        else_ctx.lower_bindings(else_bindings, terminal_assert);
1447
1448        else_ctx.drain_branch_private_residue(&pre_if_names);
1449
1450        if terminal_assert && else_ctx.sm.depth() > 1 {
1451            let excess = else_ctx.sm.depth() - 1;
1452            for _ in 0..excess {
1453                else_ctx.emit_op(StackOp::Nip);
1454                else_ctx.sm.remove_at_depth(1);
1455            }
1456        }
1457
1458        // Balance stack between branches so both end at the same depth.
1459        // When addOutput is inside an if-then with no else, the then-branch
1460        // consumes stack items and pushes a serialized output, while the
1461        // else-branch leaves the stack unchanged. Both must end at the same
1462        // depth for correct execution after OP_ENDIF.
1463        //
1464        // Fix: identify items consumed by the then-branch (present in parent
1465        // but gone after then). Emit targeted ROLL+DROP in the else-branch
1466        // to remove those same items, then push empty bytes as placeholder.
1467        // OP_CAT with empty bytes is identity (no-op for output hashing).
1468        // Identify items consumed asymmetrically between branches.
1469        // Phase 1: collect consumed names from both directions.
1470        let post_then_names = then_ctx.sm.named_slots();
1471        let mut consumed_names: Vec<String> = Vec::new();
1472        for name in &pre_if_names {
1473            if !post_then_names.contains(name) && else_ctx.sm.has(name) {
1474                consumed_names.push(name.clone());
1475            }
1476        }
1477        let post_else_names = else_ctx.sm.named_slots();
1478        let mut else_consumed_names: Vec<String> = Vec::new();
1479        for name in &pre_if_names {
1480            if !post_else_names.contains(name) && then_ctx.sm.has(name) {
1481                else_consumed_names.push(name.clone());
1482            }
1483        }
1484
1485        // Phase 2: perform ALL drops before any placeholder pushes.
1486        // This prevents double-placeholder when bilateral drops balance each other.
1487        if !consumed_names.is_empty() {
1488            let mut depths: Vec<usize> = consumed_names
1489                .iter()
1490                .map(|n| else_ctx.sm.find_depth(n).unwrap())
1491                .collect();
1492            depths.sort_by(|a, b| b.cmp(a));
1493            for depth in depths {
1494                if depth == 0 {
1495                    else_ctx.emit_op(StackOp::Drop);
1496                    else_ctx.sm.pop();
1497                } else if depth == 1 {
1498                    else_ctx.emit_op(StackOp::Nip);
1499                    else_ctx.sm.remove_at_depth(1);
1500                } else {
1501                    else_ctx.emit_op(StackOp::Push(PushValue::Int(depth as i128)));
1502                    else_ctx.sm.push("");
1503                    else_ctx.emit_op(StackOp::Roll { depth });
1504                    else_ctx.sm.pop();
1505                    let rolled = else_ctx.sm.remove_at_depth(depth);
1506                    else_ctx.sm.push(&rolled);
1507                    else_ctx.emit_op(StackOp::Drop);
1508                    else_ctx.sm.pop();
1509                }
1510            }
1511        }
1512        if !else_consumed_names.is_empty() {
1513            let mut depths: Vec<usize> = else_consumed_names
1514                .iter()
1515                .map(|n| then_ctx.sm.find_depth(n).unwrap())
1516                .collect();
1517            depths.sort_by(|a, b| b.cmp(a));
1518            for depth in depths {
1519                if depth == 0 {
1520                    then_ctx.emit_op(StackOp::Drop);
1521                    then_ctx.sm.pop();
1522                } else if depth == 1 {
1523                    then_ctx.emit_op(StackOp::Nip);
1524                    then_ctx.sm.remove_at_depth(1);
1525                } else {
1526                    then_ctx.emit_op(StackOp::Push(PushValue::Int(depth as i128)));
1527                    then_ctx.sm.push("");
1528                    then_ctx.emit_op(StackOp::Roll { depth });
1529                    then_ctx.sm.pop();
1530                    let rolled = then_ctx.sm.remove_at_depth(depth);
1531                    then_ctx.sm.push(&rolled);
1532                    then_ctx.emit_op(StackOp::Drop);
1533                    then_ctx.sm.pop();
1534                }
1535            }
1536        }
1537
1538        // Phase 3: single depth-balance check after ALL drops.
1539        // Push placeholder only if one branch is still deeper than the other.
1540        if then_ctx.sm.depth() > else_ctx.sm.depth() {
1541            // When the then-branch reassigned a local variable (if-without-else),
1542            // push a COPY of that variable in the else-branch instead of a generic
1543            // placeholder.
1544            let then_top_p3 = then_ctx.sm.peek_at_depth(0).to_string();
1545            if else_bindings.is_empty() && !then_top_p3.is_empty() && else_ctx.sm.has(&then_top_p3) {
1546                let var_depth = else_ctx.sm.find_depth(&then_top_p3).unwrap();
1547                if var_depth == 0 {
1548                    else_ctx.emit_op(StackOp::Dup);
1549                } else {
1550                    else_ctx.emit_op(StackOp::Push(PushValue::Int(var_depth as i128)));
1551                    else_ctx.sm.push("");
1552                    else_ctx.emit_op(StackOp::Pick { depth: var_depth });
1553                    else_ctx.sm.pop();
1554                }
1555                else_ctx.sm.push(&then_top_p3);
1556            } else {
1557                else_ctx.emit_op(StackOp::Push(PushValue::Bytes(Vec::new())));
1558                else_ctx.sm.push("");
1559            }
1560        } else if else_ctx.sm.depth() > then_ctx.sm.depth() {
1561            then_ctx.emit_op(StackOp::Push(PushValue::Bytes(Vec::new())));
1562            then_ctx.sm.push("");
1563        }
1564
1565        let then_ops = then_ctx.ops;
1566        let else_ops = else_ctx.ops;
1567
1568        self.emit_op(StackOp::If {
1569            then_ops,
1570            else_ops: if else_ops.is_empty() {
1571                Vec::new()
1572            } else {
1573                else_ops
1574            },
1575        });
1576
1577        // Reconcile parent stackMap: remove items consumed by the branches.
1578        let post_branch_names = then_ctx.sm.named_slots();
1579        for name in &pre_if_names {
1580            if !post_branch_names.contains(name) && self.sm.has(name) {
1581                if let Some(depth) = self.sm.find_depth(name) {
1582                    self.sm.remove_at_depth(depth);
1583                }
1584            }
1585        }
1586
1587        // The if expression may produce a result value on top.
1588        if then_ctx.sm.depth() > self.sm.depth() {
1589            let then_top = then_ctx.sm.peek_at_depth(0).to_string();
1590            let else_top = if else_ctx.sm.depth() > 0 {
1591                else_ctx.sm.peek_at_depth(0).to_string()
1592            } else {
1593                String::new()
1594            };
1595            let is_property = self.properties.iter().any(|p| p.name == then_top);
1596            if is_property && !then_top.is_empty() && then_top == else_top
1597                && then_top != binding_name && self.sm.has(&then_top)
1598            {
1599                // Both branches did update_prop for the same property
1600                self.sm.push(&then_top);
1601                for d in 1..self.sm.depth() {
1602                    if self.sm.peek_at_depth(d) == then_top {
1603                        if d == 1 {
1604                            self.emit_op(StackOp::Nip);
1605                            self.sm.remove_at_depth(1);
1606                        } else {
1607                            self.emit_op(StackOp::Push(PushValue::Int(d as i128)));
1608                            self.sm.push("");
1609                            self.emit_op(StackOp::Roll { depth: d + 1 });
1610                            self.sm.pop();
1611                            let rolled = self.sm.remove_at_depth(d);
1612                            self.sm.push(&rolled);
1613                            self.emit_op(StackOp::Drop);
1614                            self.sm.pop();
1615                        }
1616                        break;
1617                    }
1618                }
1619            } else if !then_top.is_empty() && !is_property && else_bindings.is_empty()
1620                && then_top != binding_name && self.sm.has(&then_top)
1621            {
1622                // If-without-else: then-branch reassigned a local variable that
1623                // was PICKed (outer-protected), leaving a stale copy on the stack.
1624                // Push the local name and remove the stale entry.
1625                self.sm.push(&then_top);
1626                for d in 1..self.sm.depth() {
1627                    if self.sm.peek_at_depth(d) == then_top {
1628                        if d == 1 {
1629                            self.emit_op(StackOp::Nip);
1630                            self.sm.remove_at_depth(1);
1631                        } else {
1632                            self.emit_op(StackOp::Push(PushValue::Int(d as i128)));
1633                            self.sm.push("");
1634                            self.emit_op(StackOp::Roll { depth: d + 1 });
1635                            self.sm.pop();
1636                            let rolled = self.sm.remove_at_depth(d);
1637                            self.sm.push(&rolled);
1638                            self.emit_op(StackOp::Drop);
1639                            self.sm.pop();
1640                        }
1641                        break;
1642                    }
1643                }
1644            } else {
1645                self.sm.push(binding_name);
1646            }
1647        } else if else_ctx.sm.depth() > self.sm.depth() {
1648            self.sm.push(binding_name);
1649        } else {
1650            // Void if — don't push phantom
1651        }
1652        self.track_depth();
1653
1654        if then_ctx.max_depth > self.max_depth {
1655            self.max_depth = then_ctx.max_depth;
1656        }
1657        if else_ctx.max_depth > self.max_depth {
1658            self.max_depth = else_ctx.max_depth;
1659        }
1660    }
1661
1662    fn lower_loop(
1663        &mut self,
1664        _binding_name: &str,
1665        count: usize,
1666        body: &[ANFBinding],
1667        iter_var: &str,
1668    ) {
1669        // Collect outer-scope names referenced in the loop body.
1670        // These must not be consumed in non-final iterations.
1671        let body_binding_names: HashSet<String> = body.iter().map(|b| b.name.clone()).collect();
1672        let mut outer_refs = HashSet::new();
1673        for b in body {
1674            if let ANFValue::LoadParam { name } = &b.value {
1675                if name != iter_var {
1676                    outer_refs.insert(name.clone());
1677                }
1678            }
1679            // Also protect @ref: targets from outer scope (not redefined in body)
1680            if let ANFValue::LoadConst { value: v } = &b.value {
1681                if let Some(s) = v.as_str() {
1682                    if s.len() > 5 && &s[..5] == "@ref:" {
1683                        let ref_name = &s[5..];
1684                        if !body_binding_names.contains(ref_name) {
1685                            outer_refs.insert(ref_name.to_string());
1686                        }
1687                    }
1688                }
1689            }
1690        }
1691
1692        // Temporarily extend localBindings with body binding names so
1693        // @ref: to body-internal values can consume on last use.
1694        let prev_local_bindings = self.local_bindings.clone();
1695        self.local_bindings = self.local_bindings.union(&body_binding_names).cloned().collect();
1696
1697        for i in 0..count {
1698            self.emit_op(StackOp::Push(PushValue::Int(i as i128)));
1699            self.sm.push(iter_var);
1700
1701            let mut last_uses = compute_last_uses(body);
1702
1703            // In non-final iterations, prevent outer-scope refs from being
1704            // consumed by setting their last-use beyond any body binding index.
1705            if i < count - 1 {
1706                for ref_name in &outer_refs {
1707                    last_uses.insert(ref_name.clone(), body.len());
1708                }
1709            }
1710
1711            for (j, binding) in body.iter().enumerate() {
1712                self.lower_binding(binding, j, &last_uses);
1713            }
1714
1715            // Clean up the iteration variable if it was not consumed by the body.
1716            // The body may not reference iter_var at all, leaving it on the stack.
1717            if self.sm.has(iter_var) {
1718                let depth = self.sm.find_depth(iter_var);
1719                if let Some(0) = depth {
1720                    self.emit_op(StackOp::Drop);
1721                    self.sm.pop();
1722                }
1723            }
1724        }
1725        // Restore localBindings
1726        self.local_bindings = prev_local_bindings;
1727        // Note: loops are statements, not expressions — they don't produce a
1728        // physical stack value. Do NOT push a dummy stackMap entry, as it would
1729        // desync the stackMap depth from the physical stack.
1730    }
1731
1732    fn lower_assert(
1733        &mut self,
1734        value_ref: &str,
1735        binding_index: usize,
1736        last_uses: &HashMap<String, usize>,
1737        terminal: bool,
1738    ) {
1739        let is_last = self.is_last_use(value_ref, binding_index, last_uses);
1740        self.bring_to_top(value_ref, is_last);
1741        if terminal {
1742            // Terminal assert: leave value on stack for Bitcoin Script's
1743            // final truthiness check (no OP_VERIFY).
1744        } else {
1745            self.sm.pop();
1746            self.emit_op(StackOp::Opcode("OP_VERIFY".to_string()));
1747        }
1748        self.track_depth();
1749    }
1750
1751    fn lower_update_prop(
1752        &mut self,
1753        prop_name: &str,
1754        value_ref: &str,
1755        binding_index: usize,
1756        last_uses: &HashMap<String, usize>,
1757    ) {
1758        let is_last = self.is_last_use(value_ref, binding_index, last_uses);
1759        self.bring_to_top(value_ref, is_last);
1760        self.sm.pop();
1761        self.sm.push(prop_name);
1762
1763        // When NOT inside an if-branch, remove the old property entry from
1764        // the stack. After liftBranchUpdateProps transforms conditional
1765        // property updates into flat if-expressions + top-level update_prop,
1766        // the old value is dead and must be removed to keep stack depth correct.
1767        // Inside branches, the old value is kept for lower_if's same-property
1768        // detection to handle correctly.
1769        if !self.inside_branch {
1770            for d in 1..self.sm.depth() {
1771                if self.sm.peek_at_depth(d) == prop_name {
1772                    if d == 1 {
1773                        self.emit_op(StackOp::Nip);
1774                        self.sm.remove_at_depth(1);
1775                    } else {
1776                        self.emit_op(StackOp::Push(PushValue::Int(d as i128)));
1777                        self.sm.push("");
1778                        self.emit_op(StackOp::Roll { depth: d + 1 });
1779                        self.sm.pop();
1780                        let rolled = self.sm.remove_at_depth(d);
1781                        self.sm.push(&rolled);
1782                        self.emit_op(StackOp::Drop);
1783                        self.sm.pop();
1784                    }
1785                    break;
1786                }
1787            }
1788        }
1789
1790        self.track_depth();
1791    }
1792
1793    fn lower_get_state_script(&mut self, binding_name: &str) {
1794        let state_props: Vec<ANFProperty> = self
1795            .properties
1796            .iter()
1797            .filter(|p| !p.readonly)
1798            .cloned()
1799            .collect();
1800
1801        if state_props.is_empty() {
1802            self.emit_op(StackOp::Push(PushValue::Bytes(Vec::new())));
1803            self.sm.push(binding_name);
1804            return;
1805        }
1806
1807        let mut first = true;
1808        for prop in &state_props {
1809            if self.sm.has(&prop.name) {
1810                self.bring_to_top(&prop.name, true); // consume: raw value dead after serialization
1811            } else if let Some(ref val) = prop.initial_value {
1812                self.push_json_value(val);
1813                self.sm.push("");
1814            } else {
1815                self.emit_op(StackOp::Push(PushValue::Int(0)));
1816                self.sm.push("");
1817            }
1818
1819            // Convert numeric/boolean values to fixed-width bytes via OP_NUM2BIN
1820            if prop.prop_type == "bigint" {
1821                self.emit_op(StackOp::Push(PushValue::Int(8)));
1822                self.sm.push("");
1823                self.emit_op(StackOp::Opcode("OP_NUM2BIN".to_string()));
1824                self.sm.pop(); // pop the width
1825            } else if prop.prop_type == "boolean" {
1826                self.emit_op(StackOp::Push(PushValue::Int(1)));
1827                self.sm.push("");
1828                self.emit_op(StackOp::Opcode("OP_NUM2BIN".to_string()));
1829                self.sm.pop(); // pop the width
1830            } else if prop.prop_type == "ByteString" {
1831                // Prepend push-data length prefix (matching SDK format)
1832                self.emit_push_data_encode();
1833            }
1834            // Other byte types (PubKey, Sig, Sha256, etc.) need no conversion
1835
1836            if !first {
1837                self.sm.pop();
1838                self.sm.pop();
1839                self.emit_op(StackOp::Opcode("OP_CAT".to_string()));
1840                self.sm.push("");
1841            }
1842            first = false;
1843        }
1844
1845        self.sm.pop();
1846        self.sm.push(binding_name);
1847        self.track_depth();
1848    }
1849
1850    /// Builds the full BIP-143 output serialization for a single-output stateful
1851    /// continuation and hashes it with SHA256d. Uses _codePart implicit parameter
1852    /// for the code portion and extracts the amount from the preimage.
1853    fn lower_compute_state_output_hash(
1854        &mut self,
1855        binding_name: &str,
1856        args: &[String],
1857        binding_index: usize,
1858        last_uses: &std::collections::HashMap<String, usize>,
1859    ) {
1860        let preimage_ref = &args[0];
1861        let state_bytes_ref = &args[1];
1862
1863        // Bring stateBytes to stack first.
1864        let sb_last = self.is_last_use(state_bytes_ref, binding_index, last_uses);
1865        self.bring_to_top(state_bytes_ref, sb_last);
1866
1867        // Extract amount from preimage for the continuation output.
1868        let pre_last = self.is_last_use(preimage_ref, binding_index, last_uses);
1869        self.bring_to_top(preimage_ref, pre_last);
1870
1871        // Extract amount: last 52 bytes, take 8 bytes at offset 0.
1872        self.emit_op(StackOp::Opcode("OP_SIZE".into()));
1873        self.sm.push("");
1874        self.emit_op(StackOp::Push(PushValue::Int(52))); // 8 (amount) + 44 (tail)
1875        self.sm.push("");
1876        self.emit_op(StackOp::Opcode("OP_SUB".into()));
1877        self.sm.pop();
1878        self.sm.pop();
1879        self.sm.push("");
1880        self.emit_op(StackOp::Opcode("OP_SPLIT".into())); // [prefix, amountAndTail]
1881        self.sm.pop();
1882        self.sm.pop();
1883        self.sm.push(""); // prefix
1884        self.sm.push(""); // amountAndTail
1885        self.emit_op(StackOp::Nip); // drop prefix
1886        self.sm.pop();
1887        self.sm.pop();
1888        self.sm.push("");
1889        self.emit_op(StackOp::Push(PushValue::Int(8)));
1890        self.sm.push("");
1891        self.emit_op(StackOp::Opcode("OP_SPLIT".into())); // [amount(8), tail(44)]
1892        self.sm.pop();
1893        self.sm.pop();
1894        self.sm.push(""); // amount
1895        self.sm.push(""); // tail
1896        self.emit_op(StackOp::Drop); // drop tail
1897        self.sm.pop();
1898        // --- Stack: [..., stateBytes, amount(8LE)] ---
1899
1900        // Save amount to altstack
1901        self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into()));
1902        self.sm.pop();
1903
1904        // Bring _codePart to top (PICK — never consume, reused across outputs)
1905        self.bring_to_top("_codePart", false);
1906        // --- Stack: [..., stateBytes, codePart] ---
1907
1908        // Append OP_RETURN + stateBytes
1909        self.emit_op(StackOp::Push(PushValue::Bytes(vec![0x6a])));
1910        self.sm.push("");
1911        self.emit_op(StackOp::Opcode("OP_CAT".into()));
1912        self.sm.pop();
1913        self.sm.pop();
1914        self.sm.push("");
1915        // --- Stack: [..., stateBytes, codePart+OP_RETURN] ---
1916
1917        self.emit_op(StackOp::Swap);
1918        self.sm.swap();
1919        self.emit_op(StackOp::Opcode("OP_CAT".into()));
1920        self.sm.pop();
1921        self.sm.pop();
1922        self.sm.push("");
1923        // --- Stack: [..., codePart+OP_RETURN+stateBytes] ---
1924
1925        // Compute varint prefix for script length
1926        self.emit_op(StackOp::Opcode("OP_SIZE".into()));
1927        self.sm.push("");
1928        self.emit_varint_encoding();
1929
1930        // Prepend varint to script
1931        self.emit_op(StackOp::Swap);
1932        self.sm.swap();
1933        self.sm.pop();
1934        self.sm.pop();
1935        self.emit_op(StackOp::Opcode("OP_CAT".into()));
1936        self.sm.push("");
1937
1938        // Prepend amount from altstack
1939        self.emit_op(StackOp::Opcode("OP_FROMALTSTACK".into()));
1940        self.sm.push("");
1941        self.emit_op(StackOp::Swap);
1942        self.sm.swap();
1943        self.emit_op(StackOp::Opcode("OP_CAT".into()));
1944        self.sm.pop();
1945        self.sm.pop();
1946        self.sm.push("");
1947
1948        // Hash with SHA256d
1949        self.emit_op(StackOp::Opcode("OP_HASH256".into()));
1950
1951        self.sm.pop();
1952        self.sm.push(binding_name);
1953        self.track_depth();
1954    }
1955
1956    /// `computeStateOutput(preimage, stateBytes, newAmount)` — builds the continuation
1957    /// output using _newAmount and _codePart instead of extracting from preimage.
1958    /// Returns raw output bytes WITHOUT the final OP_HASH256.
1959    fn lower_compute_state_output(
1960        &mut self,
1961        binding_name: &str,
1962        args: &[String],
1963        binding_index: usize,
1964        last_uses: &std::collections::HashMap<String, usize>,
1965    ) {
1966        let preimage_ref = &args[0];
1967        let state_bytes_ref = &args[1];
1968        let new_amount_ref = &args[2];
1969
1970        // Consume preimage ref (no longer needed — we use _codePart and _newAmount).
1971        let pre_last = self.is_last_use(preimage_ref, binding_index, last_uses);
1972        self.bring_to_top(preimage_ref, pre_last);
1973        self.emit_op(StackOp::Drop);
1974        self.sm.pop();
1975
1976        // Step 1: Convert _newAmount to 8-byte LE and save to altstack.
1977        let amount_last = self.is_last_use(new_amount_ref, binding_index, last_uses);
1978        self.bring_to_top(new_amount_ref, amount_last);
1979        self.emit_op(StackOp::Push(PushValue::Int(8)));
1980        self.sm.push("");
1981        self.emit_op(StackOp::Opcode("OP_NUM2BIN".into()));
1982        self.sm.pop();
1983        self.sm.pop();
1984        self.sm.push("");
1985        self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into()));
1986        self.sm.pop();
1987
1988        // Step 2: Bring stateBytes to stack.
1989        let sb_last = self.is_last_use(state_bytes_ref, binding_index, last_uses);
1990        self.bring_to_top(state_bytes_ref, sb_last);
1991
1992        // Step 3: Bring _codePart to top (PICK — never consume, reused across outputs)
1993        self.bring_to_top("_codePart", false);
1994        // --- Stack: [..., stateBytes, codePart] ---
1995
1996        // Step 4: Append OP_RETURN + stateBytes
1997        self.emit_op(StackOp::Push(PushValue::Bytes(vec![0x6a])));
1998        self.sm.push("");
1999        self.emit_op(StackOp::Opcode("OP_CAT".into()));
2000        self.sm.pop();
2001        self.sm.pop();
2002        self.sm.push("");
2003        // --- Stack: [..., stateBytes, codePart+OP_RETURN] ---
2004
2005        self.emit_op(StackOp::Swap);
2006        self.sm.swap();
2007        self.emit_op(StackOp::Opcode("OP_CAT".into()));
2008        self.sm.pop();
2009        self.sm.pop();
2010        self.sm.push("");
2011        // --- Stack: [..., codePart+OP_RETURN+stateBytes] ---
2012
2013        // Step 5: Compute varint prefix for script length
2014        self.emit_op(StackOp::Opcode("OP_SIZE".into()));
2015        self.sm.push("");
2016        self.emit_varint_encoding();
2017
2018        // Step 6: Prepend varint to script
2019        self.emit_op(StackOp::Swap);
2020        self.sm.swap();
2021        self.sm.pop();
2022        self.sm.pop();
2023        self.emit_op(StackOp::Opcode("OP_CAT".into()));
2024        self.sm.push("");
2025
2026        // Step 7: Prepend _newAmount (8-byte LE) from altstack.
2027        self.emit_op(StackOp::Opcode("OP_FROMALTSTACK".into()));
2028        self.sm.push("");
2029        self.emit_op(StackOp::Swap);
2030        self.sm.swap();
2031        self.emit_op(StackOp::Opcode("OP_CAT".into()));
2032        self.sm.pop();
2033        self.sm.pop();
2034        self.sm.push("");
2035        // --- Stack: [..., fullOutputSerialization] --- (NO hash)
2036
2037        self.sm.pop();
2038        self.sm.push(binding_name);
2039        self.track_depth();
2040    }
2041
2042    /// `buildChangeOutput(pkh, amount)` — builds a P2PKH output serialization:
2043    ///   amount(8LE) + 0x19 + 76a914 <pkh:20bytes> 88ac
2044    /// Total: 34 bytes (8 + 1 + 25).
2045    fn lower_build_change_output(
2046        &mut self,
2047        binding_name: &str,
2048        args: &[String],
2049        binding_index: usize,
2050        last_uses: &std::collections::HashMap<String, usize>,
2051    ) {
2052        let pkh_ref = &args[0];
2053        let amount_ref = &args[1];
2054
2055        // Step 1: Build the P2PKH locking script with length prefix.
2056        // Push prefix: varint(25) + OP_DUP + OP_HASH160 + OP_PUSHBYTES_20 = 0x1976a914
2057        self.emit_op(StackOp::Push(PushValue::Bytes(vec![0x19, 0x76, 0xa9, 0x14])));
2058        self.sm.push("");
2059
2060        // Push the 20-byte PKH
2061        let pkh_last = self.is_last_use(pkh_ref, binding_index, last_uses);
2062        self.bring_to_top(pkh_ref, pkh_last);
2063        // CAT: prefix || pkh
2064        self.emit_op(StackOp::Opcode("OP_CAT".into()));
2065        self.sm.pop();
2066        self.sm.pop();
2067        self.sm.push("");
2068
2069        // Push suffix: OP_EQUALVERIFY + OP_CHECKSIG = 0x88ac
2070        self.emit_op(StackOp::Push(PushValue::Bytes(vec![0x88, 0xac])));
2071        self.sm.push("");
2072        // CAT: (prefix || pkh) || suffix
2073        self.emit_op(StackOp::Opcode("OP_CAT".into()));
2074        self.sm.pop();
2075        self.sm.pop();
2076        self.sm.push("");
2077        // --- Stack: [..., 0x1976a914{pkh}88ac] ---
2078
2079        // Step 2: Prepend amount as 8-byte LE.
2080        let amount_last = self.is_last_use(amount_ref, binding_index, last_uses);
2081        self.bring_to_top(amount_ref, amount_last);
2082        self.emit_op(StackOp::Push(PushValue::Int(8)));
2083        self.sm.push("");
2084        self.emit_op(StackOp::Opcode("OP_NUM2BIN".into()));
2085        self.sm.pop(); // pop width
2086        // Stack: [..., script, amount(8LE)]
2087        self.emit_op(StackOp::Swap);
2088        self.sm.swap();
2089        // Stack: [..., amount(8LE), script]
2090        self.emit_op(StackOp::Opcode("OP_CAT".into()));
2091        self.sm.pop();
2092        self.sm.pop();
2093        self.sm.push("");
2094        // --- Stack: [..., amount(8LE)+0x1976a914{pkh}88ac] ---
2095
2096        self.sm.pop();
2097        self.sm.push(binding_name);
2098        self.track_depth();
2099    }
2100
2101    fn lower_add_output(
2102        &mut self,
2103        binding_name: &str,
2104        satoshis: &str,
2105        state_values: &[String],
2106        _preimage: &str,
2107        binding_index: usize,
2108        last_uses: &HashMap<String, usize>,
2109    ) {
2110        // Build a full BIP-143 output serialization:
2111        //   amount(8LE) + varint(scriptLen) + codePart + OP_RETURN + stateBytes
2112        // Uses _codePart implicit parameter (passed by SDK) instead of extracting
2113        // codePart from the preimage. This is simpler and works with OP_CODESEPARATOR.
2114
2115        let state_props: Vec<ANFProperty> = self
2116            .properties
2117            .iter()
2118            .filter(|p| !p.readonly)
2119            .cloned()
2120            .collect();
2121
2122        // Step 1: Bring _codePart to top (PICK — never consume, reused across outputs)
2123        self.bring_to_top("_codePart", false);
2124        // --- Stack: [..., codePart] ---
2125
2126        // Step 2: Append OP_RETURN byte (0x6a).
2127        self.emit_op(StackOp::Push(PushValue::Bytes(vec![0x6a])));
2128        self.sm.push("");
2129        self.emit_op(StackOp::Opcode("OP_CAT".into()));
2130        self.sm.pop();
2131        self.sm.pop();
2132        self.sm.push("");
2133        // --- Stack: [..., codePart+OP_RETURN] ---
2134
2135        // Step 3: Serialize each state value and concatenate.
2136        for (i, value_ref) in state_values.iter().enumerate() {
2137            if i >= state_props.len() {
2138                break;
2139            }
2140            let prop = &state_props[i];
2141
2142            let is_last = self.is_last_use(value_ref, binding_index, last_uses);
2143            self.bring_to_top(value_ref, is_last);
2144
2145            if prop.prop_type == "bigint" {
2146                self.emit_op(StackOp::Push(PushValue::Int(8)));
2147                self.sm.push("");
2148                self.emit_op(StackOp::Opcode("OP_NUM2BIN".to_string()));
2149                self.sm.pop();
2150            } else if prop.prop_type == "boolean" {
2151                self.emit_op(StackOp::Push(PushValue::Int(1)));
2152                self.sm.push("");
2153                self.emit_op(StackOp::Opcode("OP_NUM2BIN".to_string()));
2154                self.sm.pop();
2155            } else if prop.prop_type == "ByteString" {
2156                // Prepend push-data length prefix (matching SDK format)
2157                self.emit_push_data_encode();
2158            }
2159
2160            self.sm.pop();
2161            self.sm.pop();
2162            self.emit_op(StackOp::Opcode("OP_CAT".to_string()));
2163            self.sm.push("");
2164        }
2165        // --- Stack: [..., codePart+OP_RETURN+stateBytes] ---
2166
2167        // Step 4: Compute varint prefix for the full script length.
2168        self.emit_op(StackOp::Opcode("OP_SIZE".into())); // [script, len]
2169        self.sm.push("");
2170        self.emit_varint_encoding();
2171        // --- Stack: [..., script, varint] ---
2172
2173        // Step 5: Prepend varint to script: SWAP CAT
2174        self.emit_op(StackOp::Swap);
2175        self.sm.swap();
2176        self.sm.pop();
2177        self.sm.pop();
2178        self.emit_op(StackOp::Opcode("OP_CAT".into()));
2179        self.sm.push("");
2180        // --- Stack: [..., varint+script] ---
2181
2182        // Step 6: Prepend satoshis as 8-byte LE.
2183        let is_last_satoshis = self.is_last_use(satoshis, binding_index, last_uses);
2184        self.bring_to_top(satoshis, is_last_satoshis);
2185        self.emit_op(StackOp::Push(PushValue::Int(8)));
2186        self.sm.push("");
2187        self.emit_op(StackOp::Opcode("OP_NUM2BIN".to_string()));
2188        self.sm.pop(); // pop the width
2189        // Stack: [..., varint+script, satoshis(8LE)]
2190        self.emit_op(StackOp::Swap);
2191        self.sm.swap();
2192        self.sm.pop();
2193        self.sm.pop();
2194        self.emit_op(StackOp::Opcode("OP_CAT".to_string())); // satoshis || varint+script
2195        self.sm.push("");
2196        // --- Stack: [..., amount(8LE)+varint+scriptPubKey] ---
2197
2198        // Rename top to binding name
2199        self.sm.pop();
2200        self.sm.push(binding_name);
2201        self.track_depth();
2202    }
2203
2204    /// `add_raw_output(satoshis, scriptBytes)` — builds a raw output serialization:
2205    ///   amount(8LE) + varint(scriptLen) + scriptBytes
2206    /// The scriptBytes are used as-is (no codePart/state insertion).
2207    fn lower_add_raw_output(
2208        &mut self,
2209        binding_name: &str,
2210        satoshis: &str,
2211        script_bytes: &str,
2212        binding_index: usize,
2213        last_uses: &HashMap<String, usize>,
2214    ) {
2215        // Step 1: Bring scriptBytes to top
2216        let script_is_last = self.is_last_use(script_bytes, binding_index, last_uses);
2217        self.bring_to_top(script_bytes, script_is_last);
2218
2219        // Step 2: Compute varint prefix for script length
2220        self.emit_op(StackOp::Opcode("OP_SIZE".to_string())); // [script, len]
2221        self.sm.push("");
2222        self.emit_varint_encoding();
2223        // --- Stack: [..., script, varint] ---
2224
2225        // Step 3: Prepend varint to script: SWAP CAT
2226        self.emit_op(StackOp::Swap); // [varint, script]
2227        self.sm.swap();
2228        self.sm.pop();
2229        self.sm.pop();
2230        self.emit_op(StackOp::Opcode("OP_CAT".to_string())); // [varint+script]
2231        self.sm.push("");
2232
2233        // Step 4: Prepend satoshis as 8-byte LE
2234        let sat_is_last = self.is_last_use(satoshis, binding_index, last_uses);
2235        self.bring_to_top(satoshis, sat_is_last);
2236        self.emit_op(StackOp::Push(PushValue::Int(8)));
2237        self.sm.push("");
2238        self.emit_op(StackOp::Opcode("OP_NUM2BIN".to_string()));
2239        self.sm.pop(); // pop width
2240        // Stack: [..., varint+script, satoshis(8LE)]
2241        self.emit_op(StackOp::Swap);
2242        self.sm.swap();
2243        self.sm.pop();
2244        self.sm.pop();
2245        self.emit_op(StackOp::Opcode("OP_CAT".to_string())); // satoshis || varint+script
2246        self.sm.push("");
2247
2248        // Rename top to binding name
2249        self.sm.pop();
2250        self.sm.push(binding_name);
2251        self.track_depth();
2252    }
2253
2254    fn lower_array_literal(
2255        &mut self,
2256        binding_name: &str,
2257        elements: &[String],
2258        binding_index: usize,
2259        last_uses: &HashMap<String, usize>,
2260    ) {
2261        // An array_literal brings each element to the top of the stack.
2262        // The elements remain as individual stack entries; the binding name tracks
2263        // the last element so that callers (e.g. checkMultiSig) can find them.
2264        for elem in elements {
2265            let is_last = self.is_last_use(elem, binding_index, last_uses);
2266            self.bring_to_top(elem, is_last);
2267            self.sm.pop();
2268            self.sm.push(""); // anonymous stack entry for intermediate elements
2269        }
2270        // Rename the topmost entry to the binding name
2271        if !elements.is_empty() {
2272            self.sm.pop();
2273        }
2274        self.sm.push(binding_name);
2275        self.track_depth();
2276    }
2277
2278    fn lower_check_multi_sig(
2279        &mut self,
2280        binding_name: &str,
2281        args: &[String],
2282        binding_index: usize,
2283        last_uses: &HashMap<String, usize>,
2284    ) {
2285        // checkMultiSig(sigs, pks) — emits the OP_CHECKMULTISIG sequence.
2286        // Bitcoin Script stack layout:
2287        //   OP_0 <sig1> ... <sigN> <nSigs> <pk1> ... <pkM> <nPKs> OP_CHECKMULTISIG
2288        //
2289        // The two args reference array_literal bindings whose individual elements
2290        // are already on the stack.
2291
2292        // Push OP_0 dummy (Bitcoin CHECKMULTISIG off-by-one bug workaround)
2293        self.emit_op(StackOp::Push(PushValue::Int(0)));
2294        self.sm.push("");
2295
2296        // Bring sigs array ref to top
2297        let sigs_is_last = self.is_last_use(&args[0], binding_index, last_uses);
2298        self.bring_to_top(&args[0], sigs_is_last);
2299
2300        // Bring pks array ref to top
2301        let pks_is_last = self.is_last_use(&args[1], binding_index, last_uses);
2302        self.bring_to_top(&args[1], pks_is_last);
2303
2304        // Pop all args + dummy
2305        self.sm.pop(); // pks
2306        self.sm.pop(); // sigs
2307        self.sm.pop(); // OP_0 dummy
2308
2309        self.emit_op(StackOp::Opcode("OP_CHECKMULTISIG".to_string()));
2310        self.sm.push(binding_name);
2311        self.track_depth();
2312    }
2313
2314    fn lower_check_preimage(
2315        &mut self,
2316        binding_name: &str,
2317        preimage: &str,
2318        binding_index: usize,
2319        last_uses: &HashMap<String, usize>,
2320    ) {
2321        // OP_PUSH_TX: verify the sighash preimage matches the current spending
2322        // transaction.  See https://wiki.bitcoinsv.io/index.php/OP_PUSH_TX
2323        //
2324        // The technique uses a well-known ECDSA keypair where private key = 1
2325        // (so the public key is the secp256k1 generator point G, compressed:
2326        //   0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798).
2327        //
2328        // At spending time the SDK must:
2329        //   1. Serialise the BIP-143 sighash preimage for the current input.
2330        //   2. Compute sighash = SHA256(SHA256(preimage)).
2331        //   3. Derive an ECDSA signature (r, s) with privkey = 1:
2332        //        r = Gx  (x-coordinate of the generator point, constant)
2333        //        s = (sighash + r) mod n
2334        //   4. DER-encode (r, s) and append the SIGHASH_ALL|FORKID byte (0x41).
2335        //   5. Push <sig> <preimage> (plus any other method args) as the
2336        //      unlocking script.
2337        //
2338        // The locking script sequence:
2339        //   [bring preimage to top]     -- via PICK or ROLL
2340        //   [bring _opPushTxSig to top] -- via ROLL (consuming)
2341        //   <G>                         -- push compressed generator point
2342        //   OP_CHECKSIG                 -- verify sig over SHA256(SHA256(preimage))
2343        //   OP_VERIFY                   -- abort if invalid
2344        //   -- preimage remains on stack for field extractors
2345        //
2346        // Stack map trace:
2347        //   After bring_to_top(preimage):  [..., preimage]
2348        //   After bring_to_top(sig, true): [..., preimage, _opPushTxSig]
2349        //   After push G:                  [..., preimage, _opPushTxSig, null(G)]
2350        //   After OP_CHECKSIG:             [..., preimage, null(result)]
2351        //   After OP_VERIFY:               [..., preimage]
2352
2353        // Step 0: Emit OP_CODESEPARATOR so that the scriptCode in the BIP-143
2354        // preimage is only the code after this point. This reduces preimage size
2355        // for large scripts and is required for scripts > ~32KB.
2356        self.emit_op(StackOp::Opcode("OP_CODESEPARATOR".to_string()));
2357
2358        // Step 1: Bring preimage to top.
2359        let is_last = self.is_last_use(preimage, binding_index, last_uses);
2360        self.bring_to_top(preimage, is_last);
2361
2362        // Step 2: Bring the implicit _opPushTxSig to top (consuming).
2363        self.bring_to_top("_opPushTxSig", true);
2364
2365        // Step 3: Push compressed secp256k1 generator point G (33 bytes).
2366        let g: Vec<u8> = vec![
2367            0x02, 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB,
2368            0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87, 0x0B,
2369            0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28,
2370            0xD9, 0x59, 0xF2, 0x81, 0x5B, 0x16, 0xF8, 0x17,
2371            0x98,
2372        ];
2373        self.emit_op(StackOp::Push(PushValue::Bytes(g)));
2374        self.sm.push(""); // G on stack
2375
2376        // Step 4: OP_CHECKSIG -- pops pubkey (G) and sig, pushes boolean result.
2377        self.emit_op(StackOp::Opcode("OP_CHECKSIG".to_string()));
2378        self.sm.pop(); // G consumed
2379        self.sm.pop(); // _opPushTxSig consumed
2380        self.sm.push(""); // boolean result
2381
2382        // Step 5: OP_VERIFY -- abort if false, removes result from stack.
2383        self.emit_op(StackOp::Opcode("OP_VERIFY".to_string()));
2384        self.sm.pop(); // result consumed
2385
2386        // The preimage is now on top (from Step 1). Rename to binding name
2387        // so field extractors can reference it.
2388        self.sm.pop();
2389        self.sm.push(binding_name);
2390
2391        self.track_depth();
2392    }
2393
2394    /// Lower `deserialize_state(preimage)` — extracts mutable property values
2395    /// from the BIP-143 preimage's scriptCode field. The state is stored as the
2396    /// last `stateLen` bytes of the scriptCode (after OP_RETURN).
2397    ///
2398    /// For each mutable property, the value is extracted, converted to the
2399    /// correct type (BIN2NUM for bigint/boolean), and pushed onto the stack
2400    /// with the property name in the stackMap. This allows `load_prop` to
2401    /// find the deserialized values instead of using hardcoded initial values.
2402    fn lower_deserialize_state(
2403        &mut self,
2404        preimage_ref: &str,
2405        binding_index: usize,
2406        last_uses: &HashMap<String, usize>,
2407    ) {
2408        let mut prop_names: Vec<String> = Vec::new();
2409        let mut prop_types: Vec<String> = Vec::new();
2410        let mut prop_sizes: Vec<i128> = Vec::new();
2411        let mut has_variable_length = false;
2412
2413        for p in &self.properties {
2414            if p.readonly {
2415                continue;
2416            }
2417            prop_names.push(p.name.clone());
2418            prop_types.push(p.prop_type.clone());
2419            let sz: i128 = match p.prop_type.as_str() {
2420                "bigint" => 8,
2421                "boolean" => 1,
2422                "PubKey" => 33,
2423                "Addr" => 20,
2424                "Sha256" => 32,
2425                "Point" => 64,
2426                "ByteString" => { has_variable_length = true; -1 },
2427                _ => panic!("deserialize_state: unsupported type: {}", p.prop_type),
2428            };
2429            prop_sizes.push(sz);
2430        }
2431
2432        if prop_names.is_empty() {
2433            return;
2434        }
2435
2436        let is_last = self.is_last_use(preimage_ref, binding_index, last_uses);
2437        self.bring_to_top(preimage_ref, is_last);
2438
2439        // 1. Skip first 104 bytes (header), drop prefix.
2440        self.emit_op(StackOp::Push(PushValue::Int(104)));
2441        self.sm.push("");
2442        self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2443        self.sm.pop(); self.sm.pop();
2444        self.sm.push(""); self.sm.push("");
2445        self.emit_op(StackOp::Nip);
2446        self.sm.pop(); self.sm.pop();
2447        self.sm.push("");
2448
2449        // 2. Drop tail 44 bytes.
2450        self.emit_op(StackOp::Opcode("OP_SIZE".to_string()));
2451        self.sm.push("");
2452        self.emit_op(StackOp::Push(PushValue::Int(44)));
2453        self.sm.push("");
2454        self.emit_op(StackOp::Opcode("OP_SUB".to_string()));
2455        self.sm.pop(); self.sm.pop();
2456        self.sm.push("");
2457        self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2458        self.sm.pop(); self.sm.pop();
2459        self.sm.push(""); self.sm.push("");
2460        self.emit_op(StackOp::Drop);
2461        self.sm.pop();
2462
2463        // 3. Drop amount (last 8 bytes).
2464        self.emit_op(StackOp::Opcode("OP_SIZE".to_string()));
2465        self.sm.push("");
2466        self.emit_op(StackOp::Push(PushValue::Int(8)));
2467        self.sm.push("");
2468        self.emit_op(StackOp::Opcode("OP_SUB".to_string()));
2469        self.sm.pop(); self.sm.pop();
2470        self.sm.push("");
2471        self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2472        self.sm.pop(); self.sm.pop();
2473        self.sm.push(""); self.sm.push("");
2474        self.emit_op(StackOp::Drop);
2475        self.sm.pop();
2476
2477        if !has_variable_length {
2478            let state_len: i128 = prop_sizes.iter().sum();
2479
2480            // 4. Extract last stateLen bytes.
2481            self.emit_op(StackOp::Opcode("OP_SIZE".to_string()));
2482            self.sm.push("");
2483            self.emit_op(StackOp::Push(PushValue::Int(state_len)));
2484            self.sm.push("");
2485            self.emit_op(StackOp::Opcode("OP_SUB".to_string()));
2486            self.sm.pop(); self.sm.pop();
2487            self.sm.push("");
2488            self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2489            self.sm.pop(); self.sm.pop();
2490            self.sm.push(""); self.sm.push("");
2491            self.emit_op(StackOp::Nip);
2492            self.sm.pop(); self.sm.pop();
2493            self.sm.push("");
2494
2495            // 5. Split fixed-size state fields.
2496            self.split_fixed_state_fields(&prop_names, &prop_types, &prop_sizes);
2497        } else if !self.sm.has("_codePart") {
2498            // Variable-length state but _codePart not available (terminal method).
2499            self.emit_op(StackOp::Drop);
2500            self.sm.pop();
2501        } else {
2502            // Variable-length path: strip varint, use _codePart to find state
2503            self.emit_op(StackOp::Push(PushValue::Int(1)));
2504            self.sm.push("");
2505            self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
2506            self.sm.pop(); self.sm.pop();
2507            self.sm.push(""); self.sm.push("");
2508            self.emit_op(StackOp::Swap);
2509            self.sm.swap();
2510            self.emit_op(StackOp::Dup);
2511            self.sm.push("");
2512            // Zero-pad before BIN2NUM to prevent sign-bit misinterpretation (0xfd → -125 without pad)
2513            self.emit_op(StackOp::Push(PushValue::Bytes(vec![0])));
2514            self.sm.push("");
2515            self.emit_op(StackOp::Opcode("OP_CAT".into()));
2516            self.sm.pop(); self.sm.pop();
2517            self.sm.push("");
2518            self.emit_op(StackOp::Opcode("OP_BIN2NUM".into()));
2519            self.emit_op(StackOp::Push(PushValue::Int(253)));
2520            self.sm.push("");
2521            self.emit_op(StackOp::Opcode("OP_LESSTHAN".into()));
2522            self.sm.pop(); self.sm.pop();
2523            self.sm.push("");
2524
2525            self.emit_op(StackOp::Opcode("OP_IF".into()));
2526            self.sm.pop();
2527            let sm_at_varint_if = self.sm.clone();
2528            self.emit_op(StackOp::Drop);
2529            self.sm.pop();
2530
2531            self.emit_op(StackOp::Opcode("OP_ELSE".into()));
2532            self.sm = sm_at_varint_if.clone();
2533            self.emit_op(StackOp::Drop);
2534            self.sm.pop();
2535            self.emit_op(StackOp::Push(PushValue::Int(2)));
2536            self.sm.push("");
2537            self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
2538            self.sm.pop(); self.sm.pop();
2539            self.sm.push(""); self.sm.push("");
2540            self.emit_op(StackOp::Nip);
2541            self.sm.pop(); self.sm.pop();
2542            self.sm.push("");
2543
2544            self.emit_op(StackOp::Opcode("OP_ENDIF".into()));
2545
2546            // Compute skip = SIZE(_codePart) - codeSepIdx
2547            self.bring_to_top("_codePart", false);
2548            self.emit_op(StackOp::Opcode("OP_SIZE".into()));
2549            self.sm.push("");
2550            self.emit_op(StackOp::Nip);
2551            self.sm.pop(); self.sm.pop();
2552            self.sm.push("");
2553            self.emit_op(StackOp::PushCodeSepIndex);
2554            self.sm.push("");
2555            self.emit_op(StackOp::Opcode("OP_SUB".into()));
2556            self.sm.pop(); self.sm.pop();
2557            self.sm.push("");
2558
2559            // Split scriptCode at skip to get state
2560            self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
2561            self.sm.pop(); self.sm.pop();
2562            self.sm.push(""); self.sm.push("");
2563            self.emit_op(StackOp::Nip);
2564            self.sm.pop(); self.sm.pop();
2565            self.sm.push("");
2566
2567            // Parse variable-length state fields
2568            self.parse_variable_length_state_fields(&prop_names, &prop_types, &prop_sizes);
2569        }
2570
2571        self.track_depth();
2572    }
2573
2574    fn split_fixed_state_fields(
2575        &mut self,
2576        prop_names: &[String],
2577        prop_types: &[String],
2578        prop_sizes: &[i128],
2579    ) {
2580        let num_props = prop_names.len();
2581        if num_props == 1 {
2582            if prop_types[0] == "bigint" || prop_types[0] == "boolean" {
2583                self.emit_op(StackOp::Opcode("OP_BIN2NUM".to_string()));
2584            }
2585            self.sm.pop();
2586            self.sm.push(&prop_names[0]);
2587        } else {
2588            for i in 0..num_props {
2589                let sz = prop_sizes[i];
2590                if i < num_props - 1 {
2591                    self.emit_op(StackOp::Push(PushValue::Int(sz)));
2592                    self.sm.push("");
2593                    self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2594                    self.sm.pop(); self.sm.pop();
2595                    self.sm.push(""); self.sm.push("");
2596                    self.emit_op(StackOp::Swap);
2597                    self.sm.swap();
2598                    if prop_types[i] == "bigint" || prop_types[i] == "boolean" {
2599                        self.emit_op(StackOp::Opcode("OP_BIN2NUM".to_string()));
2600                    }
2601                    self.emit_op(StackOp::Swap);
2602                    self.sm.swap();
2603                    self.sm.pop(); self.sm.pop();
2604                    self.sm.push(&prop_names[i]);
2605                    self.sm.push("");
2606                } else {
2607                    if prop_types[i] == "bigint" || prop_types[i] == "boolean" {
2608                        self.emit_op(StackOp::Opcode("OP_BIN2NUM".to_string()));
2609                    }
2610                    self.sm.pop();
2611                    self.sm.push(&prop_names[i]);
2612                }
2613            }
2614        }
2615    }
2616
2617    fn parse_variable_length_state_fields(
2618        &mut self,
2619        prop_names: &[String],
2620        prop_types: &[String],
2621        prop_sizes: &[i128],
2622    ) {
2623        let num_props = prop_names.len();
2624        if num_props == 1 {
2625            if prop_types[0] == "ByteString" {
2626                // Single ByteString field: decode push-data prefix, drop trailing empty
2627                self.emit_push_data_decode(); // [..., data, remaining]
2628                self.emit_op(StackOp::Drop); self.sm.pop();
2629            } else if prop_types[0] == "bigint" || prop_types[0] == "boolean" {
2630                self.emit_op(StackOp::Opcode("OP_BIN2NUM".into()));
2631            }
2632            self.sm.pop();
2633            self.sm.push(&prop_names[0]);
2634        } else {
2635            for i in 0..num_props {
2636                if i < num_props - 1 {
2637                    if prop_types[i] == "ByteString" {
2638                        // ByteString: decode push-data prefix, extract data
2639                        self.emit_push_data_decode(); // [..., data, rest]
2640                        self.sm.pop(); self.sm.pop();
2641                        self.sm.push(&prop_names[i]);
2642                        self.sm.push(""); // rest on top
2643                    } else {
2644                        self.emit_op(StackOp::Push(PushValue::Int(prop_sizes[i])));
2645                        self.sm.push("");
2646                        self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
2647                        self.sm.pop(); self.sm.pop();
2648                        self.sm.push(""); self.sm.push("");
2649                        self.emit_op(StackOp::Swap); self.sm.swap();
2650                        if prop_types[i] == "bigint" || prop_types[i] == "boolean" {
2651                            self.emit_op(StackOp::Opcode("OP_BIN2NUM".into()));
2652                        }
2653                        self.emit_op(StackOp::Swap); self.sm.swap();
2654                        self.sm.pop(); self.sm.pop();
2655                        self.sm.push(&prop_names[i]);
2656                        self.sm.push("");
2657                    }
2658                } else {
2659                    if prop_types[i] == "ByteString" {
2660                        // Last ByteString: decode push-data prefix, drop trailing empty
2661                        self.emit_push_data_decode(); // [..., data, remaining]
2662                        self.emit_op(StackOp::Drop); self.sm.pop();
2663                    } else if prop_types[i] == "bigint" || prop_types[i] == "boolean" {
2664                        self.emit_op(StackOp::Opcode("OP_BIN2NUM".into()));
2665                    }
2666                    self.sm.pop();
2667                    self.sm.push(&prop_names[i]);
2668                }
2669            }
2670        }
2671    }
2672
2673    /// Lower a preimage field extractor call.
2674    ///
2675    /// The SigHashPreimage follows BIP-143 format:
2676    ///   Offset  Bytes  Field
2677    ///   0       4      nVersion (LE uint32)
2678    ///   4       32     hashPrevouts
2679    ///   36      32     hashSequence
2680    ///   68      36     outpoint (txid 32 + vout 4)
2681    ///   104     var    scriptCode (varint-prefixed)
2682    ///   var     8      amount (satoshis, LE int64)
2683    ///   var     4      nSequence
2684    ///   var     32     hashOutputs
2685    ///   var     4      nLocktime
2686    ///   var     4      sighashType
2687    ///
2688    /// Fixed-offset fields use absolute OP_SPLIT positions.
2689    /// Variable-offset fields use end-relative positions via OP_SIZE.
2690    fn lower_extractor(
2691        &mut self,
2692        binding_name: &str,
2693        func_name: &str,
2694        args: &[String],
2695        binding_index: usize,
2696        last_uses: &HashMap<String, usize>,
2697    ) {
2698        assert!(!args.is_empty(), "{} requires 1 argument", func_name);
2699        let is_last = self.is_last_use(&args[0], binding_index, last_uses);
2700        self.bring_to_top(&args[0], is_last);
2701
2702        // The preimage is now on top of the stack.
2703        self.sm.pop(); // consume the preimage from stack map
2704
2705        match func_name {
2706            "extractVersion" => {
2707                // <preimage> 4 OP_SPLIT OP_DROP OP_BIN2NUM
2708                self.emit_op(StackOp::Push(PushValue::Int(4)));
2709                self.sm.push("");
2710                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2711                self.sm.pop();
2712                self.sm.push("");
2713                self.sm.push("");
2714                self.emit_op(StackOp::Drop);
2715                self.sm.pop();
2716                self.emit_op(StackOp::Opcode("OP_BIN2NUM".to_string()));
2717            }
2718            "extractHashPrevouts" => {
2719                // <preimage> 4 OP_SPLIT OP_NIP 32 OP_SPLIT OP_DROP
2720                self.emit_op(StackOp::Push(PushValue::Int(4)));
2721                self.sm.push("");
2722                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2723                self.sm.pop();
2724                self.sm.push("");
2725                self.sm.push("");
2726                self.emit_op(StackOp::Nip);
2727                self.sm.pop();
2728                self.sm.pop();
2729                self.sm.push("");
2730                self.emit_op(StackOp::Push(PushValue::Int(32)));
2731                self.sm.push("");
2732                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2733                self.sm.pop(); // pop position (32)
2734                self.sm.pop(); // pop data being split
2735                self.sm.push("");
2736                self.sm.push("");
2737                self.emit_op(StackOp::Drop);
2738                self.sm.pop();
2739            }
2740            "extractHashSequence" => {
2741                // <preimage> 36 OP_SPLIT OP_NIP 32 OP_SPLIT OP_DROP
2742                self.emit_op(StackOp::Push(PushValue::Int(36)));
2743                self.sm.push("");
2744                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2745                self.sm.pop();
2746                self.sm.push("");
2747                self.sm.push("");
2748                self.emit_op(StackOp::Nip);
2749                self.sm.pop();
2750                self.sm.pop();
2751                self.sm.push("");
2752                self.emit_op(StackOp::Push(PushValue::Int(32)));
2753                self.sm.push("");
2754                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2755                self.sm.pop(); // pop position (32)
2756                self.sm.pop(); // pop data being split
2757                self.sm.push("");
2758                self.sm.push("");
2759                self.emit_op(StackOp::Drop);
2760                self.sm.pop();
2761            }
2762            "extractOutpoint" => {
2763                // <preimage> 68 OP_SPLIT OP_NIP 36 OP_SPLIT OP_DROP
2764                self.emit_op(StackOp::Push(PushValue::Int(68)));
2765                self.sm.push("");
2766                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2767                self.sm.pop();
2768                self.sm.push("");
2769                self.sm.push("");
2770                self.emit_op(StackOp::Nip);
2771                self.sm.pop();
2772                self.sm.pop();
2773                self.sm.push("");
2774                self.emit_op(StackOp::Push(PushValue::Int(36)));
2775                self.sm.push("");
2776                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2777                self.sm.pop(); // pop position (36)
2778                self.sm.pop(); // pop data being split
2779                self.sm.push("");
2780                self.sm.push("");
2781                self.emit_op(StackOp::Drop);
2782                self.sm.pop();
2783            }
2784            "extractSigHashType" => {
2785                // End-relative: last 4 bytes, converted to number.
2786                // <preimage> OP_SIZE 4 OP_SUB OP_SPLIT OP_NIP OP_BIN2NUM
2787                self.emit_op(StackOp::Opcode("OP_SIZE".to_string()));
2788                self.sm.push("");
2789                self.sm.push("");
2790                self.emit_op(StackOp::Push(PushValue::Int(4)));
2791                self.sm.push("");
2792                self.emit_op(StackOp::Opcode("OP_SUB".to_string()));
2793                self.sm.pop();
2794                self.sm.pop();
2795                self.sm.push("");
2796                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2797                self.sm.pop();
2798                self.sm.pop();
2799                self.sm.push("");
2800                self.sm.push("");
2801                self.emit_op(StackOp::Nip);
2802                self.sm.pop();
2803                self.sm.pop();
2804                self.sm.push("");
2805                self.emit_op(StackOp::Opcode("OP_BIN2NUM".to_string()));
2806            }
2807            "extractLocktime" => {
2808                // End-relative: 4 bytes before the last 4 (sighashType).
2809                // <preimage> OP_SIZE 8 OP_SUB OP_SPLIT OP_NIP 4 OP_SPLIT OP_DROP OP_BIN2NUM
2810                self.emit_op(StackOp::Opcode("OP_SIZE".to_string()));
2811                self.sm.push("");
2812                self.sm.push("");
2813                self.emit_op(StackOp::Push(PushValue::Int(8)));
2814                self.sm.push("");
2815                self.emit_op(StackOp::Opcode("OP_SUB".to_string()));
2816                self.sm.pop();
2817                self.sm.pop();
2818                self.sm.push("");
2819                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2820                self.sm.pop();
2821                self.sm.pop();
2822                self.sm.push("");
2823                self.sm.push("");
2824                self.emit_op(StackOp::Nip);
2825                self.sm.pop();
2826                self.sm.pop();
2827                self.sm.push("");
2828                self.emit_op(StackOp::Push(PushValue::Int(4)));
2829                self.sm.push("");
2830                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2831                self.sm.pop(); // pop position (4)
2832                self.sm.pop(); // pop value being split
2833                self.sm.push("");
2834                self.sm.push("");
2835                self.emit_op(StackOp::Drop);
2836                self.sm.pop();
2837                self.emit_op(StackOp::Opcode("OP_BIN2NUM".to_string()));
2838            }
2839            "extractOutputHash" | "extractOutputs" => {
2840                // End-relative: 32 bytes before the last 8 (nLocktime 4 + sighashType 4).
2841                // <preimage> OP_SIZE 40 OP_SUB OP_SPLIT OP_NIP 32 OP_SPLIT OP_DROP
2842                self.emit_op(StackOp::Opcode("OP_SIZE".to_string()));
2843                self.sm.push("");
2844                self.sm.push("");
2845                self.emit_op(StackOp::Push(PushValue::Int(40)));
2846                self.sm.push("");
2847                self.emit_op(StackOp::Opcode("OP_SUB".to_string()));
2848                self.sm.pop();
2849                self.sm.pop();
2850                self.sm.push("");
2851                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2852                self.sm.pop();
2853                self.sm.pop();
2854                self.sm.push("");
2855                self.sm.push("");
2856                self.emit_op(StackOp::Nip);
2857                self.sm.pop();
2858                self.sm.pop();
2859                self.sm.push("");
2860                self.emit_op(StackOp::Push(PushValue::Int(32)));
2861                self.sm.push("");
2862                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2863                self.sm.pop(); // pop position (32)
2864                self.sm.pop(); // pop value being split (last40)
2865                self.sm.push("");
2866                self.sm.push("");
2867                self.emit_op(StackOp::Drop);
2868                self.sm.pop();
2869            }
2870            "extractAmount" => {
2871                // End-relative: 8 bytes at offset -(52) from end.
2872                // <preimage> OP_SIZE 52 OP_SUB OP_SPLIT OP_NIP 8 OP_SPLIT OP_DROP OP_BIN2NUM
2873                self.emit_op(StackOp::Opcode("OP_SIZE".to_string()));
2874                self.sm.push("");
2875                self.sm.push("");
2876                self.emit_op(StackOp::Push(PushValue::Int(52)));
2877                self.sm.push("");
2878                self.emit_op(StackOp::Opcode("OP_SUB".to_string()));
2879                self.sm.pop();
2880                self.sm.pop();
2881                self.sm.push("");
2882                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2883                self.sm.pop();
2884                self.sm.pop();
2885                self.sm.push("");
2886                self.sm.push("");
2887                self.emit_op(StackOp::Nip);
2888                self.sm.pop();
2889                self.sm.pop();
2890                self.sm.push("");
2891                self.emit_op(StackOp::Push(PushValue::Int(8)));
2892                self.sm.push("");
2893                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2894                self.sm.pop(); // pop position (8)
2895                self.sm.pop(); // pop value being split
2896                self.sm.push("");
2897                self.sm.push("");
2898                self.emit_op(StackOp::Drop);
2899                self.sm.pop();
2900                self.emit_op(StackOp::Opcode("OP_BIN2NUM".to_string()));
2901            }
2902            "extractSequence" => {
2903                // End-relative: 4 bytes (nSequence) at offset -(44) from end.
2904                // <preimage> OP_SIZE 44 OP_SUB OP_SPLIT OP_NIP 4 OP_SPLIT OP_DROP OP_BIN2NUM
2905                self.emit_op(StackOp::Opcode("OP_SIZE".to_string()));
2906                self.sm.push("");
2907                self.sm.push("");
2908                self.emit_op(StackOp::Push(PushValue::Int(44)));
2909                self.sm.push("");
2910                self.emit_op(StackOp::Opcode("OP_SUB".to_string()));
2911                self.sm.pop();
2912                self.sm.pop();
2913                self.sm.push("");
2914                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2915                self.sm.pop();
2916                self.sm.pop();
2917                self.sm.push("");
2918                self.sm.push("");
2919                self.emit_op(StackOp::Nip);
2920                self.sm.pop();
2921                self.sm.pop();
2922                self.sm.push("");
2923                self.emit_op(StackOp::Push(PushValue::Int(4)));
2924                self.sm.push("");
2925                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2926                self.sm.pop(); // pop position (4)
2927                self.sm.pop(); // pop value being split
2928                self.sm.push("");
2929                self.sm.push("");
2930                self.emit_op(StackOp::Drop);
2931                self.sm.pop();
2932                self.emit_op(StackOp::Opcode("OP_BIN2NUM".to_string()));
2933            }
2934            "extractScriptCode" => {
2935                // Variable-length field at offset 104. End-relative tail = 52 bytes.
2936                // <preimage> 104 OP_SPLIT OP_NIP OP_SIZE 52 OP_SUB OP_SPLIT OP_DROP
2937                self.emit_op(StackOp::Push(PushValue::Int(104)));
2938                self.sm.push("");
2939                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2940                self.sm.pop();
2941                self.sm.push("");
2942                self.sm.push("");
2943                self.emit_op(StackOp::Nip);
2944                self.sm.pop();
2945                self.sm.pop();
2946                self.sm.push("");
2947                self.emit_op(StackOp::Opcode("OP_SIZE".to_string()));
2948                self.sm.push("");
2949                self.emit_op(StackOp::Push(PushValue::Int(52)));
2950                self.sm.push("");
2951                self.emit_op(StackOp::Opcode("OP_SUB".to_string()));
2952                self.sm.pop();
2953                self.sm.pop();
2954                self.sm.push("");
2955                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2956                self.sm.pop();
2957                self.sm.pop();
2958                self.sm.push("");
2959                self.sm.push("");
2960                self.emit_op(StackOp::Drop);
2961                self.sm.pop();
2962            }
2963            "extractInputIndex" => {
2964                // Input index = vout field of outpoint, at offset 100, 4 bytes.
2965                // <preimage> 100 OP_SPLIT OP_NIP 4 OP_SPLIT OP_DROP OP_BIN2NUM
2966                self.emit_op(StackOp::Push(PushValue::Int(100)));
2967                self.sm.push("");
2968                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2969                self.sm.pop();
2970                self.sm.push("");
2971                self.sm.push("");
2972                self.emit_op(StackOp::Nip);
2973                self.sm.pop();
2974                self.sm.pop();
2975                self.sm.push("");
2976                self.emit_op(StackOp::Push(PushValue::Int(4)));
2977                self.sm.push("");
2978                self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
2979                self.sm.pop(); // pop position (4)
2980                self.sm.pop(); // pop value being split
2981                self.sm.push("");
2982                self.sm.push("");
2983                self.emit_op(StackOp::Drop);
2984                self.sm.pop();
2985                self.emit_op(StackOp::Opcode("OP_BIN2NUM".to_string()));
2986            }
2987            _ => panic!("unknown extractor: {}", func_name),
2988        }
2989
2990        // Rename top of stack to the binding name
2991        self.sm.pop();
2992        self.sm.push(binding_name);
2993        self.track_depth();
2994    }
2995
2996    /// Lower `__array_access(data, index)` — ByteString byte-level indexing.
2997    ///
2998    /// Compiled to:
2999    ///   `<data> <index> OP_SPLIT OP_NIP 1 OP_SPLIT OP_DROP OP_BIN2NUM`
3000    ///
3001    /// Stack trace:
3002    ///   `[..., data, index]`
3003    ///   `OP_SPLIT  → [..., left, right]`       (split at index)
3004    ///   `OP_NIP    → [..., right]`             (discard left)
3005    ///   `push 1    → [..., right, 1]`
3006    ///   `OP_SPLIT  → [..., firstByte, rest]`   (split off first byte)
3007    ///   `OP_DROP   → [..., firstByte]`         (discard rest)
3008    ///   `OP_BIN2NUM → [..., numericValue]`     (convert byte to bigint)
3009    fn lower_array_access(
3010        &mut self,
3011        binding_name: &str,
3012        args: &[String],
3013        binding_index: usize,
3014        last_uses: &HashMap<String, usize>,
3015    ) {
3016        assert!(args.len() >= 2, "__array_access requires 2 arguments (object, index)");
3017
3018        let obj = &args[0];
3019        let index = &args[1];
3020
3021        // Push the data (ByteString) onto the stack
3022        let obj_is_last = self.is_last_use(obj, binding_index, last_uses);
3023        self.bring_to_top(obj, obj_is_last);
3024
3025        // Push the index onto the stack
3026        let index_is_last = self.is_last_use(index, binding_index, last_uses);
3027        self.bring_to_top(index, index_is_last);
3028
3029        // OP_SPLIT at index: stack = [..., left, right]
3030        self.sm.pop();  // index consumed
3031        self.sm.pop();  // data consumed
3032        self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
3033        self.sm.push("");  // left part (discard)
3034        self.sm.push("");  // right part (keep)
3035
3036        // OP_NIP: discard left, keep right: stack = [..., right]
3037        self.emit_op(StackOp::Nip);
3038        self.sm.pop();
3039        let right_part = self.sm.pop();
3040        self.sm.push(&right_part);
3041
3042        // Push 1 for the next split (extract 1 byte)
3043        self.emit_op(StackOp::Push(PushValue::Int(1)));
3044        self.sm.push("");
3045
3046        // OP_SPLIT: split off first byte: stack = [..., firstByte, rest]
3047        self.sm.pop();  // 1 consumed
3048        self.sm.pop();  // right consumed
3049        self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
3050        self.sm.push("");  // first byte (keep)
3051        self.sm.push("");  // rest (discard)
3052
3053        // OP_DROP: discard rest: stack = [..., firstByte]
3054        self.emit_op(StackOp::Drop);
3055        self.sm.pop();
3056        self.sm.pop();
3057        self.sm.push("");
3058
3059        // OP_BIN2NUM: convert single byte to numeric value
3060        self.sm.pop();
3061        self.emit_op(StackOp::Opcode("OP_BIN2NUM".to_string()));
3062
3063        self.sm.push(binding_name);
3064        self.track_depth();
3065    }
3066
3067    fn lower_reverse_bytes(
3068        &mut self,
3069        binding_name: &str,
3070        args: &[String],
3071        binding_index: usize,
3072        last_uses: &HashMap<String, usize>,
3073    ) {
3074        assert!(!args.is_empty(), "reverseBytes requires 1 argument");
3075        let is_last = self.is_last_use(&args[0], binding_index, last_uses);
3076        self.bring_to_top(&args[0], is_last);
3077
3078        // Variable-length byte reversal using bounded unrolled loop.
3079        // Each iteration peels off the first byte and prepends it to the result.
3080        // 520 iterations covers the maximum BSV element size.
3081        self.sm.pop();
3082
3083        // Push empty result (OP_0), swap so data is on top
3084        self.emit_op(StackOp::Push(PushValue::Int(0)));
3085        self.emit_op(StackOp::Swap);
3086
3087        // 520 iterations (max BSV element size)
3088        for _ in 0..520 {
3089            // Stack: [result, data]
3090            self.emit_op(StackOp::Opcode("OP_DUP".to_string()));
3091            self.emit_op(StackOp::Opcode("OP_SIZE".to_string()));
3092            self.emit_op(StackOp::Nip);
3093            self.emit_op(StackOp::If {
3094                then_ops: vec![
3095                    StackOp::Push(PushValue::Int(1)),
3096                    StackOp::Opcode("OP_SPLIT".to_string()),
3097                    StackOp::Swap,
3098                    StackOp::Rot,
3099                    StackOp::Opcode("OP_CAT".to_string()),
3100                    StackOp::Swap,
3101                ],
3102                else_ops: vec![],
3103            });
3104        }
3105
3106        // Drop empty remainder
3107        self.emit_op(StackOp::Drop);
3108
3109        self.sm.push(binding_name);
3110        self.track_depth();
3111    }
3112
3113    fn lower_substr(
3114        &mut self,
3115        binding_name: &str,
3116        args: &[String],
3117        binding_index: usize,
3118        last_uses: &HashMap<String, usize>,
3119    ) {
3120        assert!(args.len() >= 3, "substr requires 3 arguments");
3121
3122        let data = &args[0];
3123        let start = &args[1];
3124        let length = &args[2];
3125
3126        let data_is_last = self.is_last_use(data, binding_index, last_uses);
3127        self.bring_to_top(data, data_is_last);
3128
3129        let start_is_last = self.is_last_use(start, binding_index, last_uses);
3130        self.bring_to_top(start, start_is_last);
3131
3132        self.sm.pop();
3133        self.sm.pop();
3134        self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
3135        self.sm.push("");
3136        self.sm.push("");
3137
3138        self.emit_op(StackOp::Nip);
3139        self.sm.pop();
3140        let right_part = self.sm.pop();
3141        self.sm.push(&right_part);
3142
3143        let len_is_last = self.is_last_use(length, binding_index, last_uses);
3144        self.bring_to_top(length, len_is_last);
3145
3146        self.sm.pop();
3147        self.sm.pop();
3148        self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));
3149        self.sm.push("");
3150        self.sm.push("");
3151
3152        self.emit_op(StackOp::Drop);
3153        self.sm.pop();
3154        self.sm.pop();
3155
3156        self.sm.push(binding_name);
3157        self.track_depth();
3158    }
3159    fn lower_verify_rabin_sig(
3160        &mut self,
3161        binding_name: &str,
3162        args: &[String],
3163        binding_index: usize,
3164        last_uses: &HashMap<String, usize>,
3165    ) {
3166        assert!(args.len() >= 4, "verifyRabinSig requires 4 arguments");
3167
3168        // Stack input: <msg> <sig> <padding> <pubKey>
3169        // Computation: (sig^2 + padding) mod pubKey == SHA256(msg)
3170        // Opcode sequence: OP_SWAP OP_ROT OP_DUP OP_MUL OP_ADD OP_SWAP OP_MOD OP_SWAP OP_SHA256 OP_EQUAL
3171        let msg = &args[0];
3172        let sig = &args[1];
3173        let padding = &args[2];
3174        let pub_key = &args[3];
3175
3176        let msg_is_last = self.is_last_use(msg, binding_index, last_uses);
3177        self.bring_to_top(msg, msg_is_last);
3178
3179        let sig_is_last = self.is_last_use(sig, binding_index, last_uses);
3180        self.bring_to_top(sig, sig_is_last);
3181
3182        let padding_is_last = self.is_last_use(padding, binding_index, last_uses);
3183        self.bring_to_top(padding, padding_is_last);
3184
3185        let pub_key_is_last = self.is_last_use(pub_key, binding_index, last_uses);
3186        self.bring_to_top(pub_key, pub_key_is_last);
3187
3188        // Pop all 4 args from stack map
3189        self.sm.pop();
3190        self.sm.pop();
3191        self.sm.pop();
3192        self.sm.pop();
3193
3194        // Emit the Rabin signature verification opcode sequence
3195        // Stack: msg(3) sig(2) padding(1) pubKey(0)
3196        self.emit_op(StackOp::Opcode("OP_SWAP".to_string()));  // msg sig pubKey padding
3197        self.emit_op(StackOp::Opcode("OP_ROT".to_string()));   // msg pubKey padding sig
3198        self.emit_op(StackOp::Opcode("OP_DUP".to_string()));
3199        self.emit_op(StackOp::Opcode("OP_MUL".to_string()));   // msg pubKey padding sig^2
3200        self.emit_op(StackOp::Opcode("OP_ADD".to_string()));   // msg pubKey (sig^2+padding)
3201        self.emit_op(StackOp::Opcode("OP_SWAP".to_string()));  // msg (sig^2+padding) pubKey
3202        self.emit_op(StackOp::Opcode("OP_MOD".to_string()));   // msg ((sig^2+padding) mod pubKey)
3203        self.emit_op(StackOp::Opcode("OP_SWAP".to_string()));  // ((sig^2+padding) mod pubKey) msg
3204        self.emit_op(StackOp::Opcode("OP_SHA256".to_string()));
3205        self.emit_op(StackOp::Opcode("OP_EQUAL".to_string()));
3206
3207        self.sm.push(binding_name);
3208        self.track_depth();
3209    }
3210
3211    /// Lower sign(x) to Script that avoids division by zero for x == 0.
3212    /// OP_DUP OP_IF OP_DUP OP_ABS OP_SWAP OP_DIV OP_ENDIF
3213    fn lower_sign(
3214        &mut self,
3215        binding_name: &str,
3216        args: &[String],
3217        binding_index: usize,
3218        last_uses: &HashMap<String, usize>,
3219    ) {
3220        assert!(!args.is_empty(), "sign requires 1 argument");
3221        let x = &args[0];
3222
3223        let x_is_last = self.is_last_use(x, binding_index, last_uses);
3224        self.bring_to_top(x, x_is_last);
3225        self.sm.pop();
3226
3227        self.emit_op(StackOp::Opcode("OP_DUP".to_string()));
3228        self.emit_op(StackOp::If {
3229            then_ops: vec![
3230                StackOp::Opcode("OP_DUP".to_string()),
3231                StackOp::Opcode("OP_ABS".to_string()),
3232                StackOp::Swap,
3233                StackOp::Opcode("OP_DIV".to_string()),
3234            ],
3235            else_ops: vec![],
3236        });
3237
3238        self.sm.push(binding_name);
3239        self.track_depth();
3240    }
3241
3242    /// Lower right(data, len) to Script.
3243    /// OP_SWAP OP_SIZE OP_ROT OP_SUB OP_SPLIT OP_NIP
3244    fn lower_right(
3245        &mut self,
3246        binding_name: &str,
3247        args: &[String],
3248        binding_index: usize,
3249        last_uses: &HashMap<String, usize>,
3250    ) {
3251        assert!(args.len() >= 2, "right requires 2 arguments");
3252        let data = &args[0];
3253        let length = &args[1];
3254
3255        let data_is_last = self.is_last_use(data, binding_index, last_uses);
3256        self.bring_to_top(data, data_is_last);
3257
3258        let length_is_last = self.is_last_use(length, binding_index, last_uses);
3259        self.bring_to_top(length, length_is_last);
3260
3261        self.sm.pop(); // len
3262        self.sm.pop(); // data
3263
3264        self.emit_op(StackOp::Swap);                                     // <len> <data>
3265        self.emit_op(StackOp::Opcode("OP_SIZE".to_string()));            // <len> <data> <size>
3266        self.emit_op(StackOp::Rot);                                      // <data> <size> <len>
3267        self.emit_op(StackOp::Opcode("OP_SUB".to_string()));             // <data> <size-len>
3268        self.emit_op(StackOp::Opcode("OP_SPLIT".to_string()));           // <left> <right>
3269        self.emit_op(StackOp::Nip);                                      // <right>
3270
3271        self.sm.push(binding_name);
3272        self.track_depth();
3273    }
3274
3275    /// Emit one WOTS+ chain with RFC 8391 tweakable hash.
3276    /// Stack entry: pubSeed(bottom) sig csum endpt digit(top)
3277    /// Stack exit:  pubSeed(bottom) sigRest newCsum newEndpt
3278    fn emit_wots_one_chain(&mut self, chain_index: usize) {
3279        // Save steps_copy = 15 - digit to alt (for checksum accumulation later)
3280        self.emit_op(StackOp::Opcode("OP_DUP".into()));
3281        self.emit_op(StackOp::Push(PushValue::Int(15)));
3282        self.emit_op(StackOp::Swap);
3283        self.emit_op(StackOp::Opcode("OP_SUB".into()));
3284        self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into())); // push#1: steps_copy
3285
3286        // Save endpt, csum to alt. Leave pubSeed+sig+digit on main.
3287        self.emit_op(StackOp::Swap);
3288        self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into())); // push#2: endpt
3289        self.emit_op(StackOp::Swap);
3290        self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into())); // push#3: csum
3291        // main: pubSeed sig digit
3292
3293        // Split 32B sig element
3294        self.emit_op(StackOp::Swap);
3295        self.emit_op(StackOp::Push(PushValue::Int(32)));
3296        self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
3297        self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into())); // push#4: sigRest
3298        self.emit_op(StackOp::Swap);
3299        // main: pubSeed sigElem digit
3300
3301        // Hash loop: skip first `digit` iterations, then apply F for the rest.
3302        // When digit > 0: decrement (skip). When digit == 0: hash at step j.
3303        // Stack: pubSeed(depth2) sigElem(depth1) digit(depth0=top)
3304        for j in 0..15usize {
3305            let adrs_bytes = vec![chain_index as u8, j as u8];
3306            self.emit_op(StackOp::Opcode("OP_DUP".into()));
3307            self.emit_op(StackOp::Opcode("OP_0NOTEQUAL".into()));
3308            self.emit_op(StackOp::If {
3309                then_ops: vec![
3310                    StackOp::Opcode("OP_1SUB".into()),            // skip: digit--
3311                ],
3312                else_ops: vec![
3313                    StackOp::Swap,                                  // pubSeed digit X
3314                    StackOp::Push(PushValue::Int(2)),
3315                    StackOp::Opcode("OP_PICK".into()),            // copy pubSeed
3316                    StackOp::Push(PushValue::Bytes(adrs_bytes)),   // ADRS [chainIndex, j]
3317                    StackOp::Opcode("OP_CAT".into()),              // pubSeed || adrs
3318                    StackOp::Swap,                                  // bring X to top
3319                    StackOp::Opcode("OP_CAT".into()),              // pubSeed || adrs || X
3320                    StackOp::Opcode("OP_SHA256".into()),           // F result
3321                    StackOp::Swap,                                  // pubSeed new_X digit(=0)
3322                ],
3323            });
3324        }
3325        self.emit_op(StackOp::Drop); // drop digit (now 0)
3326        // main: pubSeed endpoint
3327
3328        // Restore from alt (LIFO): sigRest, csum, endpt_acc, steps_copy
3329        self.emit_op(StackOp::Opcode("OP_FROMALTSTACK".into()));
3330        self.emit_op(StackOp::Opcode("OP_FROMALTSTACK".into()));
3331        self.emit_op(StackOp::Opcode("OP_FROMALTSTACK".into()));
3332        self.emit_op(StackOp::Opcode("OP_FROMALTSTACK".into()));
3333
3334        // csum += steps_copy
3335        self.emit_op(StackOp::Rot);
3336        self.emit_op(StackOp::Opcode("OP_ADD".into()));
3337
3338        // Concat endpoint to endpt_acc
3339        self.emit_op(StackOp::Swap);
3340        self.emit_op(StackOp::Push(PushValue::Int(3)));
3341        self.emit_op(StackOp::Opcode("OP_ROLL".into()));
3342        self.emit_op(StackOp::Opcode("OP_CAT".into()));
3343    }
3344
3345    /// WOTS+ signature verification with RFC 8391 tweakable hash (post-quantum).
3346    /// Parameters: w=16, n=32 (SHA-256), len=67 chains.
3347    /// pubkey is 64 bytes: pubSeed(32) || pkRoot(32).
3348    fn lower_verify_wots(
3349        &mut self,
3350        binding_name: &str,
3351        args: &[String],
3352        binding_index: usize,
3353        last_uses: &HashMap<String, usize>,
3354    ) {
3355        assert!(args.len() >= 3, "verifyWOTS requires 3 arguments: msg, sig, pubkey");
3356
3357        for arg in args.iter() {
3358            let is_last = self.is_last_use(arg, binding_index, last_uses);
3359            self.bring_to_top(arg, is_last);
3360        }
3361        for _ in 0..3 { self.sm.pop(); }
3362        // main: msg sig pubkey(64B: pubSeed||pkRoot)
3363
3364        // Split 64-byte pubkey into pubSeed(32) and pkRoot(32)
3365        self.emit_op(StackOp::Push(PushValue::Int(32)));
3366        self.emit_op(StackOp::Opcode("OP_SPLIT".into()));          // msg sig pubSeed pkRoot
3367        self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into()));    // pkRoot → alt
3368
3369        // Rearrange: put pubSeed at bottom, hash msg
3370        self.emit_op(StackOp::Rot);                                 // sig pubSeed msg
3371        self.emit_op(StackOp::Rot);                                 // pubSeed msg sig
3372        self.emit_op(StackOp::Swap);                                // pubSeed sig msg
3373        self.emit_op(StackOp::Opcode("OP_SHA256".into()));         // pubSeed sig msgHash
3374
3375        // Canonical layout: pubSeed(bottom) sig csum=0 endptAcc=empty hashRem(top)
3376        self.emit_op(StackOp::Swap);
3377        self.emit_op(StackOp::Push(PushValue::Int(0)));
3378        self.emit_op(StackOp::Opcode("OP_0".into()));
3379        self.emit_op(StackOp::Push(PushValue::Int(3)));
3380        self.emit_op(StackOp::Opcode("OP_ROLL".into()));
3381
3382        // Process 32 bytes → 64 message chains
3383        for byte_idx in 0..32 {
3384            if byte_idx < 31 {
3385                self.emit_op(StackOp::Push(PushValue::Int(1)));
3386                self.emit_op(StackOp::Opcode("OP_SPLIT".into()));
3387                self.emit_op(StackOp::Swap);
3388            }
3389            // Unsigned byte conversion
3390            self.emit_op(StackOp::Push(PushValue::Int(0)));
3391            self.emit_op(StackOp::Push(PushValue::Int(1)));
3392            self.emit_op(StackOp::Opcode("OP_NUM2BIN".into()));
3393            self.emit_op(StackOp::Opcode("OP_CAT".into()));
3394            self.emit_op(StackOp::Opcode("OP_BIN2NUM".into()));
3395            // Extract nibbles
3396            self.emit_op(StackOp::Opcode("OP_DUP".into()));
3397            self.emit_op(StackOp::Push(PushValue::Int(16)));
3398            self.emit_op(StackOp::Opcode("OP_DIV".into()));
3399            self.emit_op(StackOp::Swap);
3400            self.emit_op(StackOp::Push(PushValue::Int(16)));
3401            self.emit_op(StackOp::Opcode("OP_MOD".into()));
3402
3403            if byte_idx < 31 {
3404                self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into()));
3405                self.emit_op(StackOp::Swap);
3406                self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into()));
3407            } else {
3408                self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into()));
3409            }
3410
3411            self.emit_wots_one_chain(byte_idx * 2); // high nibble chain
3412
3413            if byte_idx < 31 {
3414                self.emit_op(StackOp::Opcode("OP_FROMALTSTACK".into()));
3415                self.emit_op(StackOp::Opcode("OP_FROMALTSTACK".into()));
3416                self.emit_op(StackOp::Swap);
3417                self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into()));
3418            } else {
3419                self.emit_op(StackOp::Opcode("OP_FROMALTSTACK".into()));
3420            }
3421
3422            self.emit_wots_one_chain(byte_idx * 2 + 1); // low nibble chain
3423
3424            if byte_idx < 31 {
3425                self.emit_op(StackOp::Opcode("OP_FROMALTSTACK".into()));
3426            }
3427        }
3428
3429        // Checksum digits
3430        self.emit_op(StackOp::Swap);
3431        // d66
3432        self.emit_op(StackOp::Opcode("OP_DUP".into()));
3433        self.emit_op(StackOp::Push(PushValue::Int(16)));
3434        self.emit_op(StackOp::Opcode("OP_MOD".into()));
3435        self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into()));
3436        // d65
3437        self.emit_op(StackOp::Opcode("OP_DUP".into()));
3438        self.emit_op(StackOp::Push(PushValue::Int(16)));
3439        self.emit_op(StackOp::Opcode("OP_DIV".into()));
3440        self.emit_op(StackOp::Push(PushValue::Int(16)));
3441        self.emit_op(StackOp::Opcode("OP_MOD".into()));
3442        self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into()));
3443        // d64
3444        self.emit_op(StackOp::Push(PushValue::Int(256)));
3445        self.emit_op(StackOp::Opcode("OP_DIV".into()));
3446        self.emit_op(StackOp::Push(PushValue::Int(16)));
3447        self.emit_op(StackOp::Opcode("OP_MOD".into()));
3448        self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into()));
3449
3450        // 3 checksum chains (indices 64, 65, 66)
3451        for ci in 0..3 {
3452            self.emit_op(StackOp::Opcode("OP_TOALTSTACK".into()));
3453            self.emit_op(StackOp::Push(PushValue::Int(0)));
3454            self.emit_op(StackOp::Opcode("OP_FROMALTSTACK".into()));
3455            self.emit_op(StackOp::Opcode("OP_FROMALTSTACK".into()));
3456            self.emit_wots_one_chain(64 + ci);
3457            self.emit_op(StackOp::Swap);
3458            self.emit_op(StackOp::Drop);
3459        }
3460
3461        // Final comparison
3462        self.emit_op(StackOp::Swap);
3463        self.emit_op(StackOp::Drop);
3464        // main: pubSeed endptAcc
3465        self.emit_op(StackOp::Opcode("OP_SHA256".into()));
3466        self.emit_op(StackOp::Opcode("OP_FROMALTSTACK".into())); // pkRoot
3467        self.emit_op(StackOp::Opcode("OP_EQUAL".into()));
3468        // Clean up pubSeed
3469        self.emit_op(StackOp::Swap);
3470        self.emit_op(StackOp::Drop);
3471
3472        self.sm.push(binding_name);
3473        self.track_depth();
3474    }
3475
3476    /// SLH-DSA (FIPS 205) signature verification.
3477    /// Brings all 3 args to the top, pops them, delegates to slh_dsa::emit_verify_slh_dsa,
3478    /// and pushes the boolean result.
3479    fn lower_verify_slh_dsa(
3480        &mut self,
3481        binding_name: &str,
3482        param_key: &str,
3483        args: &[String],
3484        binding_index: usize,
3485        last_uses: &HashMap<String, usize>,
3486    ) {
3487        assert!(
3488            args.len() >= 3,
3489            "verifySLHDSA requires 3 arguments: msg, sig, pubkey"
3490        );
3491
3492        // Bring args to top in order: msg, sig, pubkey
3493        for arg in args.iter() {
3494            let is_last = self.is_last_use(arg, binding_index, last_uses);
3495            self.bring_to_top(arg, is_last);
3496        }
3497        for _ in 0..3 {
3498            self.sm.pop();
3499        }
3500
3501        // Delegate to slh_dsa module
3502        super::slh_dsa::emit_verify_slh_dsa(&mut |op| self.ops.push(op), param_key);
3503
3504        self.sm.push(binding_name);
3505        self.track_depth();
3506    }
3507
3508    // =========================================================================
3509    // SHA-256 compression -- delegates to sha256.rs
3510    // =========================================================================
3511
3512    fn lower_sha256_compress(
3513        &mut self,
3514        binding_name: &str,
3515        args: &[String],
3516        binding_index: usize,
3517        last_uses: &HashMap<String, usize>,
3518    ) {
3519        assert!(
3520            args.len() >= 2,
3521            "sha256Compress requires 2 arguments: state, block"
3522        );
3523        for arg in args.iter() {
3524            let is_last = self.is_last_use(arg, binding_index, last_uses);
3525            self.bring_to_top(arg, is_last);
3526        }
3527        for _ in 0..2 {
3528            self.sm.pop();
3529        }
3530
3531        super::sha256::emit_sha256_compress(&mut |op| self.ops.push(op));
3532
3533        self.sm.push(binding_name);
3534        self.track_depth();
3535    }
3536
3537    fn lower_sha256_finalize(
3538        &mut self,
3539        binding_name: &str,
3540        args: &[String],
3541        binding_index: usize,
3542        last_uses: &HashMap<String, usize>,
3543    ) {
3544        assert!(
3545            args.len() >= 3,
3546            "sha256Finalize requires 3 arguments: state, remaining, msgBitLen"
3547        );
3548        for arg in args.iter() {
3549            let is_last = self.is_last_use(arg, binding_index, last_uses);
3550            self.bring_to_top(arg, is_last);
3551        }
3552        for _ in 0..3 {
3553            self.sm.pop();
3554        }
3555
3556        super::sha256::emit_sha256_finalize(&mut |op| self.ops.push(op));
3557
3558        self.sm.push(binding_name);
3559        self.track_depth();
3560    }
3561
3562    fn lower_blake3_compress(
3563        &mut self,
3564        binding_name: &str,
3565        args: &[String],
3566        binding_index: usize,
3567        last_uses: &HashMap<String, usize>,
3568    ) {
3569        assert!(
3570            args.len() >= 2,
3571            "blake3Compress requires 2 arguments: chainingValue, block"
3572        );
3573        for arg in args.iter() {
3574            let is_last = self.is_last_use(arg, binding_index, last_uses);
3575            self.bring_to_top(arg, is_last);
3576        }
3577        for _ in 0..2 {
3578            self.sm.pop();
3579        }
3580
3581        super::blake3::emit_blake3_compress(&mut |op| self.ops.push(op));
3582
3583        self.sm.push(binding_name);
3584        self.track_depth();
3585    }
3586
3587    fn lower_blake3_hash(
3588        &mut self,
3589        binding_name: &str,
3590        args: &[String],
3591        binding_index: usize,
3592        last_uses: &HashMap<String, usize>,
3593    ) {
3594        assert!(
3595            args.len() >= 1,
3596            "blake3Hash requires 1 argument: message"
3597        );
3598        for arg in args.iter() {
3599            let is_last = self.is_last_use(arg, binding_index, last_uses);
3600            self.bring_to_top(arg, is_last);
3601        }
3602        for _ in 0..1 {
3603            self.sm.pop();
3604        }
3605
3606        super::blake3::emit_blake3_hash(&mut |op| self.ops.push(op));
3607
3608        self.sm.push(binding_name);
3609        self.track_depth();
3610    }
3611
3612    fn lower_ec_builtin(
3613        &mut self,
3614        binding_name: &str,
3615        func_name: &str,
3616        args: &[String],
3617        binding_index: usize,
3618        last_uses: &HashMap<String, usize>,
3619    ) {
3620        // Bring args to top in order
3621        for arg in args.iter() {
3622            let is_last = self.is_last_use(arg, binding_index, last_uses);
3623            self.bring_to_top(arg, is_last);
3624        }
3625        for _ in args {
3626            self.sm.pop();
3627        }
3628
3629        let emit = &mut |op: StackOp| self.ops.push(op);
3630
3631        match func_name {
3632            "ecAdd" => super::ec::emit_ec_add(emit),
3633            "ecMul" => super::ec::emit_ec_mul(emit),
3634            "ecMulGen" => super::ec::emit_ec_mul_gen(emit),
3635            "ecNegate" => super::ec::emit_ec_negate(emit),
3636            "ecOnCurve" => super::ec::emit_ec_on_curve(emit),
3637            "ecModReduce" => super::ec::emit_ec_mod_reduce(emit),
3638            "ecEncodeCompressed" => super::ec::emit_ec_encode_compressed(emit),
3639            "ecMakePoint" => super::ec::emit_ec_make_point(emit),
3640            "ecPointX" => super::ec::emit_ec_point_x(emit),
3641            "ecPointY" => super::ec::emit_ec_point_y(emit),
3642            _ => panic!("unknown EC builtin: {}", func_name),
3643        }
3644
3645        self.sm.push(binding_name);
3646        self.track_depth();
3647    }
3648
3649    /// safediv(a, b): a / b with division-by-zero check.
3650    /// Stack: a b -> OP_DUP OP_0NOTEQUAL OP_VERIFY OP_DIV -> result
3651    fn lower_safediv(
3652        &mut self,
3653        binding_name: &str,
3654        args: &[String],
3655        binding_index: usize,
3656        last_uses: &HashMap<String, usize>,
3657    ) {
3658        assert!(args.len() >= 2, "safediv requires 2 arguments");
3659
3660        let a_is_last = self.is_last_use(&args[0], binding_index, last_uses);
3661        self.bring_to_top(&args[0], a_is_last);
3662
3663        let b_is_last = self.is_last_use(&args[1], binding_index, last_uses);
3664        self.bring_to_top(&args[1], b_is_last);
3665
3666        self.sm.pop();
3667        self.sm.pop();
3668
3669        self.emit_op(StackOp::Opcode("OP_DUP".to_string()));
3670        self.emit_op(StackOp::Opcode("OP_0NOTEQUAL".to_string()));
3671        self.emit_op(StackOp::Opcode("OP_VERIFY".to_string()));
3672        self.emit_op(StackOp::Opcode("OP_DIV".to_string()));
3673
3674        self.sm.push(binding_name);
3675        self.track_depth();
3676    }
3677
3678    /// safemod(a, b): a % b with division-by-zero check.
3679    /// Stack: a b -> OP_DUP OP_0NOTEQUAL OP_VERIFY OP_MOD -> result
3680    fn lower_safemod(
3681        &mut self,
3682        binding_name: &str,
3683        args: &[String],
3684        binding_index: usize,
3685        last_uses: &HashMap<String, usize>,
3686    ) {
3687        assert!(args.len() >= 2, "safemod requires 2 arguments");
3688
3689        let a_is_last = self.is_last_use(&args[0], binding_index, last_uses);
3690        self.bring_to_top(&args[0], a_is_last);
3691
3692        let b_is_last = self.is_last_use(&args[1], binding_index, last_uses);
3693        self.bring_to_top(&args[1], b_is_last);
3694
3695        self.sm.pop();
3696        self.sm.pop();
3697
3698        self.emit_op(StackOp::Opcode("OP_DUP".to_string()));
3699        self.emit_op(StackOp::Opcode("OP_0NOTEQUAL".to_string()));
3700        self.emit_op(StackOp::Opcode("OP_VERIFY".to_string()));
3701        self.emit_op(StackOp::Opcode("OP_MOD".to_string()));
3702
3703        self.sm.push(binding_name);
3704        self.track_depth();
3705    }
3706
3707    /// clamp(val, lo, hi): clamp val to [lo, hi].
3708    /// Stack: val lo hi -> val lo OP_MAX hi OP_MIN -> result
3709    fn lower_clamp(
3710        &mut self,
3711        binding_name: &str,
3712        args: &[String],
3713        binding_index: usize,
3714        last_uses: &HashMap<String, usize>,
3715    ) {
3716        assert!(args.len() >= 3, "clamp requires 3 arguments");
3717
3718        let val_is_last = self.is_last_use(&args[0], binding_index, last_uses);
3719        self.bring_to_top(&args[0], val_is_last);
3720
3721        let lo_is_last = self.is_last_use(&args[1], binding_index, last_uses);
3722        self.bring_to_top(&args[1], lo_is_last);
3723
3724        self.sm.pop();
3725        self.sm.pop();
3726        self.emit_op(StackOp::Opcode("OP_MAX".to_string()));
3727        self.sm.push(""); // intermediate result
3728
3729        let hi_is_last = self.is_last_use(&args[2], binding_index, last_uses);
3730        self.bring_to_top(&args[2], hi_is_last);
3731
3732        self.sm.pop();
3733        self.sm.pop();
3734        self.emit_op(StackOp::Opcode("OP_MIN".to_string()));
3735
3736        self.sm.push(binding_name);
3737        self.track_depth();
3738    }
3739
3740    /// pow(base, exp): base^exp via 32-iteration bounded conditional multiply.
3741    /// Strategy: swap to get exp base, push 1 (acc), then 32 rounds of:
3742    ///   2 OP_PICK (get exp), push(i+1), OP_GREATERTHAN, OP_IF, OP_OVER, OP_MUL, OP_ENDIF
3743    /// After iterations: OP_NIP OP_NIP to get result.
3744    fn lower_pow(
3745        &mut self,
3746        binding_name: &str,
3747        args: &[String],
3748        binding_index: usize,
3749        last_uses: &HashMap<String, usize>,
3750    ) {
3751        assert!(args.len() >= 2, "pow requires 2 arguments");
3752
3753        let base_is_last = self.is_last_use(&args[0], binding_index, last_uses);
3754        self.bring_to_top(&args[0], base_is_last);
3755
3756        let exp_is_last = self.is_last_use(&args[1], binding_index, last_uses);
3757        self.bring_to_top(&args[1], exp_is_last);
3758
3759        self.sm.pop();
3760        self.sm.pop();
3761
3762        // Stack: base exp
3763        self.emit_op(StackOp::Swap);                                  // exp base
3764        self.emit_op(StackOp::Push(PushValue::Int(1)));               // exp base 1(acc)
3765
3766        // Runtime guard: <exp> <MAX> OP_LESSTHANOREQUAL OP_VERIFY.
3767        // Bitcoin Script can't loop, so the iteration count is fixed at
3768        // compile time. Without this guard, `exp > 32` would silently
3769        // saturate at base^32 (issue #34). Script aborts cleanly now.
3770        const MAX_POW_ITERATIONS: i128 = 32;
3771        self.emit_op(StackOp::Push(PushValue::Int(2)));
3772        self.emit_op(StackOp::Opcode("OP_PICK".to_string()));         // exp base acc exp
3773        self.emit_op(StackOp::Push(PushValue::Int(MAX_POW_ITERATIONS)));
3774        self.emit_op(StackOp::Opcode("OP_LESSTHANOREQUAL".to_string())); // exp base acc (exp <= MAX)
3775        self.emit_op(StackOp::Opcode("OP_VERIFY".to_string()));
3776
3777        for i in 0..MAX_POW_ITERATIONS {
3778            // Stack: exp base acc
3779            self.emit_op(StackOp::Push(PushValue::Int(2)));
3780            self.emit_op(StackOp::Opcode("OP_PICK".to_string()));     // exp base acc exp
3781            self.emit_op(StackOp::Push(PushValue::Int(i)));
3782            self.emit_op(StackOp::Opcode("OP_GREATERTHAN".to_string())); // exp base acc (exp > i)
3783            self.emit_op(StackOp::If {
3784                then_ops: vec![
3785                    StackOp::Over,                                    // exp base acc base
3786                    StackOp::Opcode("OP_MUL".to_string()),           // exp base (acc*base)
3787                ],
3788                else_ops: vec![],
3789            });
3790        }
3791        // Stack: exp base result
3792        self.emit_op(StackOp::Nip);                                   // exp result
3793        self.emit_op(StackOp::Nip);                                   // result
3794
3795        self.sm.push(binding_name);
3796        self.track_depth();
3797    }
3798
3799    /// mulDiv(a, b, c): (a * b) / c
3800    /// Stack: a b c -> a b OP_MUL c OP_DIV -> result
3801    fn lower_mul_div(
3802        &mut self,
3803        binding_name: &str,
3804        args: &[String],
3805        binding_index: usize,
3806        last_uses: &HashMap<String, usize>,
3807    ) {
3808        assert!(args.len() >= 3, "mulDiv requires 3 arguments");
3809
3810        let a_is_last = self.is_last_use(&args[0], binding_index, last_uses);
3811        self.bring_to_top(&args[0], a_is_last);
3812
3813        let b_is_last = self.is_last_use(&args[1], binding_index, last_uses);
3814        self.bring_to_top(&args[1], b_is_last);
3815
3816        self.sm.pop();
3817        self.sm.pop();
3818        self.emit_op(StackOp::Opcode("OP_MUL".to_string()));
3819        self.sm.push(""); // a*b
3820
3821        let c_is_last = self.is_last_use(&args[2], binding_index, last_uses);
3822        self.bring_to_top(&args[2], c_is_last);
3823
3824        self.sm.pop();
3825        self.sm.pop();
3826        self.emit_op(StackOp::Opcode("OP_DIV".to_string()));
3827
3828        self.sm.push(binding_name);
3829        self.track_depth();
3830    }
3831
3832    /// percentOf(amount, bps): (amount * bps) / 10000
3833    /// Stack: amount bps -> OP_MUL 10000 OP_DIV -> result
3834    fn lower_percent_of(
3835        &mut self,
3836        binding_name: &str,
3837        args: &[String],
3838        binding_index: usize,
3839        last_uses: &HashMap<String, usize>,
3840    ) {
3841        assert!(args.len() >= 2, "percentOf requires 2 arguments");
3842
3843        let amount_is_last = self.is_last_use(&args[0], binding_index, last_uses);
3844        self.bring_to_top(&args[0], amount_is_last);
3845
3846        let bps_is_last = self.is_last_use(&args[1], binding_index, last_uses);
3847        self.bring_to_top(&args[1], bps_is_last);
3848
3849        self.sm.pop();
3850        self.sm.pop();
3851
3852        self.emit_op(StackOp::Opcode("OP_MUL".to_string()));
3853        self.emit_op(StackOp::Push(PushValue::Int(10000)));
3854        self.emit_op(StackOp::Opcode("OP_DIV".to_string()));
3855
3856        self.sm.push(binding_name);
3857        self.track_depth();
3858    }
3859
3860    /// sqrt(n): integer square root via Newton's method, 16 iterations.
3861    /// Uses: guess = n, then 16x: guess = (guess + n/guess) / 2
3862    /// Guards against n == 0 to avoid division by zero.
3863    fn lower_sqrt(
3864        &mut self,
3865        binding_name: &str,
3866        args: &[String],
3867        binding_index: usize,
3868        last_uses: &HashMap<String, usize>,
3869    ) {
3870        assert!(!args.is_empty(), "sqrt requires 1 argument");
3871
3872        let n_is_last = self.is_last_use(&args[0], binding_index, last_uses);
3873        self.bring_to_top(&args[0], n_is_last);
3874
3875        self.sm.pop();
3876
3877        // Stack: n
3878        // Guard: if n == 0, skip Newton iteration entirely (result is 0).
3879        self.emit_op(StackOp::Opcode("OP_DUP".to_string()));
3880        // Stack: n n
3881
3882        // Build the Newton iteration ops inside the OP_IF branch
3883        let mut newton_ops = Vec::new();
3884        // Stack inside IF: n  (the DUP'd copy was consumed by OP_IF)
3885        // DUP to get initial guess = n
3886        newton_ops.push(StackOp::Opcode("OP_DUP".to_string()));
3887        // Stack: n guess
3888
3889        // 16 iterations of Newton's method: guess = (guess + n/guess) / 2
3890        for _ in 0..16 {
3891            // Stack: n guess
3892            newton_ops.push(StackOp::Over);                               // n guess n
3893            newton_ops.push(StackOp::Over);                               // n guess n guess
3894            newton_ops.push(StackOp::Opcode("OP_DIV".to_string()));      // n guess (n/guess)
3895            newton_ops.push(StackOp::Opcode("OP_ADD".to_string()));      // n (guess + n/guess)
3896            newton_ops.push(StackOp::Push(PushValue::Int(2)));            // n (guess + n/guess) 2
3897            newton_ops.push(StackOp::Opcode("OP_DIV".to_string()));      // n new_guess
3898        }
3899
3900        // Stack: n guess
3901        // Drop n, keep guess
3902        newton_ops.push(StackOp::Opcode("OP_NIP".to_string()));
3903
3904        self.emit_op(StackOp::If {
3905            then_ops: newton_ops,
3906            else_ops: vec![],  // n == 0, result is already 0 on stack
3907        });
3908
3909        self.sm.push(binding_name);
3910        self.track_depth();
3911    }
3912
3913    /// gcd(a, b): Euclidean algorithm, 256 iterations with conditional OP_IF.
3914    /// Each iteration: if b != 0 then (b, a % b) else (a, 0)
3915    fn lower_gcd(
3916        &mut self,
3917        binding_name: &str,
3918        args: &[String],
3919        binding_index: usize,
3920        last_uses: &HashMap<String, usize>,
3921    ) {
3922        assert!(args.len() >= 2, "gcd requires 2 arguments");
3923
3924        let a_is_last = self.is_last_use(&args[0], binding_index, last_uses);
3925        self.bring_to_top(&args[0], a_is_last);
3926
3927        let b_is_last = self.is_last_use(&args[1], binding_index, last_uses);
3928        self.bring_to_top(&args[1], b_is_last);
3929
3930        self.sm.pop();
3931        self.sm.pop();
3932
3933        // Stack: a b
3934        // Both should be absolute values
3935        self.emit_op(StackOp::Opcode("OP_ABS".to_string()));
3936        self.emit_op(StackOp::Swap);
3937        self.emit_op(StackOp::Opcode("OP_ABS".to_string()));
3938        self.emit_op(StackOp::Swap);
3939        // Stack: |a| |b|
3940
3941        // 256 iterations of Euclidean algorithm
3942        for _ in 0..256 {
3943            // Stack: a b
3944            // Check if b != 0
3945            self.emit_op(StackOp::Opcode("OP_DUP".to_string()));      // a b b
3946            self.emit_op(StackOp::Opcode("OP_0NOTEQUAL".to_string())); // a b (b!=0)
3947
3948            self.emit_op(StackOp::If {
3949                then_ops: vec![
3950                    // Stack: a b (b != 0)
3951                    // Compute a % b, then swap: new a = b, new b = a%b
3952                    StackOp::Opcode("OP_TUCK".to_string()),            // b a b
3953                    StackOp::Opcode("OP_MOD".to_string()),             // b (a%b)
3954                ],
3955                else_ops: vec![
3956                    // Stack: a b (b == 0), just keep as-is
3957                ],
3958            });
3959        }
3960
3961        // Stack: a b (where b should be 0)
3962        // Drop b, keep a (the GCD)
3963        self.emit_op(StackOp::Drop);
3964
3965        self.sm.push(binding_name);
3966        self.track_depth();
3967    }
3968
3969    /// divmod(a, b): computes both a/b and a%b, returns a/b (drops a%b).
3970    /// Stack: a b -> OP_2DUP OP_DIV OP_ROT OP_ROT OP_MOD OP_DROP -> quotient
3971    fn lower_divmod(
3972        &mut self,
3973        binding_name: &str,
3974        args: &[String],
3975        binding_index: usize,
3976        last_uses: &HashMap<String, usize>,
3977    ) {
3978        assert!(args.len() >= 2, "divmod requires 2 arguments");
3979
3980        let a_is_last = self.is_last_use(&args[0], binding_index, last_uses);
3981        self.bring_to_top(&args[0], a_is_last);
3982
3983        let b_is_last = self.is_last_use(&args[1], binding_index, last_uses);
3984        self.bring_to_top(&args[1], b_is_last);
3985
3986        self.sm.pop();
3987        self.sm.pop();
3988
3989        // Stack: a b
3990        self.emit_op(StackOp::Opcode("OP_2DUP".to_string()));         // a b a b
3991        self.emit_op(StackOp::Opcode("OP_DIV".to_string()));          // a b (a/b)
3992        self.emit_op(StackOp::Opcode("OP_ROT".to_string()));          // a (a/b) b
3993        self.emit_op(StackOp::Opcode("OP_ROT".to_string()));          // (a/b) b a
3994        self.emit_op(StackOp::Opcode("OP_MOD".to_string()));          // (a/b) (a%b) -- wait
3995        // ROT ROT on a b (a/b): ROT -> b (a/b) a, ROT -> (a/b) a b
3996        // Then MOD -> (a/b) (a%b)
3997        // DROP -> (a/b)
3998        self.emit_op(StackOp::Drop);
3999
4000        self.sm.push(binding_name);
4001        self.track_depth();
4002    }
4003
4004    /// log2(n): exact floor(log2(n)) via bit-scanning.
4005    ///
4006    /// Uses a bounded unrolled loop (64 iterations for bigint range):
4007    ///   counter = 0
4008    ///   while input > 1: input >>= 1, counter++
4009    ///   result = counter
4010    ///
4011    /// Stack layout during loop: <input> <counter>
4012    /// Each iteration: OP_SWAP OP_DUP 1 OP_GREATERTHAN OP_IF 2 OP_DIV OP_SWAP OP_1ADD OP_SWAP OP_ENDIF OP_SWAP
4013    fn lower_log2(
4014        &mut self,
4015        binding_name: &str,
4016        args: &[String],
4017        binding_index: usize,
4018        last_uses: &HashMap<String, usize>,
4019    ) {
4020        assert!(!args.is_empty(), "log2 requires 1 argument");
4021
4022        let n_is_last = self.is_last_use(&args[0], binding_index, last_uses);
4023        self.bring_to_top(&args[0], n_is_last);
4024
4025        self.sm.pop();
4026
4027        // Stack: <n>
4028        // Push counter = 0
4029        self.emit_op(StackOp::Push(PushValue::Int(0))); // n 0
4030
4031        // 64 iterations (sufficient for Bitcoin Script bigint range)
4032        const LOG2_ITERATIONS: usize = 64;
4033        for _ in 0..LOG2_ITERATIONS {
4034            // Stack: input counter
4035            self.emit_op(StackOp::Swap);                                     // counter input
4036            self.emit_op(StackOp::Opcode("OP_DUP".to_string()));            // counter input input
4037            self.emit_op(StackOp::Push(PushValue::Int(1)));                  // counter input input 1
4038            self.emit_op(StackOp::Opcode("OP_GREATERTHAN".to_string()));     // counter input (input>1)
4039            self.emit_op(StackOp::If {
4040                then_ops: vec![
4041                    StackOp::Push(PushValue::Int(2)),                        // counter input 2
4042                    StackOp::Opcode("OP_DIV".to_string()),                   // counter (input/2)
4043                    StackOp::Swap,                                           // (input/2) counter
4044                    StackOp::Opcode("OP_1ADD".to_string()),                  // (input/2) (counter+1)
4045                    StackOp::Swap,                                           // (counter+1) (input/2)
4046                ],
4047                else_ops: vec![],
4048            });
4049            // Stack: counter input (or input counter if swapped back)
4050            // After the if: stack is counter input (swap at start, then if-branch swaps back)
4051            self.emit_op(StackOp::Swap);                                     // input counter
4052        }
4053        // Stack: input counter
4054        // Drop input, keep counter
4055        self.emit_op(StackOp::Nip); // counter
4056
4057        self.sm.push(binding_name);
4058        self.track_depth();
4059    }
4060}
4061
4062// ---------------------------------------------------------------------------
4063// Public API
4064// ---------------------------------------------------------------------------
4065
4066/// Lower an ANF program to Stack IR.
4067/// Private methods are inlined at call sites rather than compiled separately.
4068/// The constructor is skipped since it's not emitted to Bitcoin Script.
4069pub fn lower_to_stack(program: &ANFProgram) -> Result<Vec<StackMethod>, String> {
4070    // Wrap the inner implementation with catch_unwind to convert any panics
4071    // (from stack underflow, unknown operators, type mismatches, etc.) into
4072    // proper error returns instead of crashing the process.
4073    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4074        lower_to_stack_inner(program)
4075    }))
4076    .unwrap_or_else(|e| {
4077        if let Some(s) = e.downcast_ref::<String>() {
4078            Err(format!("stack lowering: {}", s))
4079        } else if let Some(s) = e.downcast_ref::<&str>() {
4080            Err(format!("stack lowering: {}", s))
4081        } else {
4082            Err("stack lowering: internal error".to_string())
4083        }
4084    })
4085}
4086
4087fn lower_to_stack_inner(program: &ANFProgram) -> Result<Vec<StackMethod>, String> {
4088    // Build map of private methods for inlining
4089    let mut private_methods: HashMap<String, ANFMethod> = HashMap::new();
4090    for method in &program.methods {
4091        if !method.is_public && method.name != "constructor" {
4092            private_methods.insert(method.name.clone(), method.clone());
4093        }
4094    }
4095
4096    let mut methods = Vec::new();
4097
4098    for method in &program.methods {
4099        // Skip constructor and private methods
4100        if method.name == "constructor" || (!method.is_public && method.name != "constructor") {
4101            continue;
4102        }
4103        let sm = lower_method_with_private_methods(method, &program.properties, &private_methods)?;
4104        methods.push(sm);
4105    }
4106
4107    Ok(methods)
4108}
4109
4110/// Check whether a method's body contains a CheckPreimage binding.
4111/// If found, the unlocking script will push an implicit <sig> parameter before
4112/// all declared parameters (OP_PUSH_TX pattern).
4113fn method_uses_check_preimage(bindings: &[ANFBinding]) -> bool {
4114    bindings.iter().any(|b| matches!(&b.value, ANFValue::CheckPreimage { .. }))
4115}
4116
4117/// Check whether a method has add_output, add_raw_output, or computeStateOutput/
4118/// computeStateOutputHash calls (recursively). Only methods that construct
4119/// continuation outputs need the _codePart implicit parameter.
4120fn method_uses_code_part(bindings: &[ANFBinding]) -> bool {
4121    bindings.iter().any(|b| match &b.value {
4122        ANFValue::AddOutput { .. } | ANFValue::AddRawOutput { .. } => true,
4123        ANFValue::Call { func, .. } if func == "computeStateOutput" || func == "computeStateOutputHash" => true,
4124        ANFValue::If { then, else_branch, .. } => method_uses_code_part(then) || method_uses_code_part(else_branch),
4125        ANFValue::Loop { body, .. } => method_uses_code_part(body),
4126        _ => false,
4127    })
4128}
4129
4130fn lower_method_with_private_methods(
4131    method: &ANFMethod,
4132    properties: &[ANFProperty],
4133    private_methods: &HashMap<String, ANFMethod>,
4134) -> Result<StackMethod, String> {
4135    let mut param_names: Vec<String> = method.params.iter().map(|p| p.name.clone()).collect();
4136
4137    // If the method uses checkPreimage, the unlocking script pushes implicit
4138    // params before all declared parameters (OP_PUSH_TX pattern).
4139    // _codePart: full code script (locking script minus state) as ByteString
4140    // _opPushTxSig: ECDSA signature for OP_PUSH_TX verification
4141    // These are inserted at the base of the stack so they can be consumed later.
4142    if method_uses_check_preimage(&method.body) {
4143        param_names.insert(0, "_opPushTxSig".to_string());
4144        // _codePart is needed when the method has add_output or add_raw_output
4145        // (it provides the code script for continuation output construction),
4146        // or when deserializing variable-length (ByteString) state fields.
4147        if method_uses_code_part(&method.body) {
4148            param_names.insert(0, "_codePart".to_string());
4149        }
4150    }
4151
4152    let mut ctx = LoweringContext::new(&param_names, properties);
4153    ctx.private_methods = private_methods.clone();
4154    // Pass terminal_assert=true for public methods so the last assert leaves
4155    // its value on the stack (Bitcoin Script requires a truthy top-of-stack).
4156    ctx.lower_bindings(&method.body, method.is_public);
4157
4158    // Clean up excess stack items left by deserialize_state.
4159    let has_deserialize_state = method.body.iter().any(|b| matches!(&b.value, ANFValue::DeserializeState { .. }));
4160    if method.is_public && has_deserialize_state && ctx.sm.depth() > 1 {
4161        let excess = ctx.sm.depth() - 1;
4162        for _ in 0..excess {
4163            ctx.emit_op(StackOp::Nip);
4164            ctx.sm.remove_at_depth(1);
4165        }
4166    }
4167
4168    if ctx.max_depth > MAX_STACK_DEPTH {
4169        return Err(format!(
4170            "method '{}' exceeds maximum stack depth of {} (actual: {}). Simplify the contract logic.",
4171            method.name, MAX_STACK_DEPTH, ctx.max_depth
4172        ));
4173    }
4174
4175    Ok(StackMethod {
4176        name: method.name.clone(),
4177        source_locs: ctx.source_locs,
4178        ops: ctx.ops,
4179        max_stack_depth: ctx.max_depth,
4180    })
4181}
4182
4183// ---------------------------------------------------------------------------
4184// Helpers
4185// ---------------------------------------------------------------------------
4186
4187fn hex_to_bytes(hex_str: &str) -> Vec<u8> {
4188    if hex_str.is_empty() {
4189        return Vec::new();
4190    }
4191    assert!(
4192        hex_str.len() % 2 == 0,
4193        "invalid hex string length: {}",
4194        hex_str.len()
4195    );
4196    (0..hex_str.len())
4197        .step_by(2)
4198        .map(|i| u8::from_str_radix(&hex_str[i..i + 2], 16).unwrap_or(0))
4199        .collect()
4200}
4201
4202// ---------------------------------------------------------------------------
4203// Tests
4204// ---------------------------------------------------------------------------
4205
4206#[cfg(test)]
4207mod tests {
4208    use super::*;
4209    use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFProperty, ANFValue};
4210
4211    /// Build a minimal P2PKH IR program for testing stack lowering.
4212    fn p2pkh_program() -> ANFProgram {
4213        ANFProgram {
4214            contract_name: "P2PKH".to_string(),
4215            properties: vec![ANFProperty {
4216                name: "pubKeyHash".to_string(),
4217                prop_type: "Addr".to_string(),
4218                readonly: true,
4219                initial_value: None,
4220            }],
4221            methods: vec![ANFMethod {
4222                name: "unlock".to_string(),
4223                params: vec![
4224                    ANFParam {
4225                        name: "sig".to_string(),
4226                        param_type: "Sig".to_string(),
4227                    },
4228                    ANFParam {
4229                        name: "pubKey".to_string(),
4230                        param_type: "PubKey".to_string(),
4231                    },
4232                ],
4233                body: vec![
4234                    ANFBinding {
4235                        name: "t0".to_string(),
4236                        value: ANFValue::LoadParam {
4237                            name: "sig".to_string(),
4238                        },
4239                        source_loc: None,
4240                    },
4241                    ANFBinding {
4242                        name: "t1".to_string(),
4243                        value: ANFValue::LoadParam {
4244                            name: "pubKey".to_string(),
4245                        },
4246                        source_loc: None,
4247                    },
4248                    ANFBinding {
4249                        name: "t2".to_string(),
4250                        value: ANFValue::LoadProp {
4251                            name: "pubKeyHash".to_string(),
4252                        },
4253                        source_loc: None,
4254                    },
4255                    ANFBinding {
4256                        name: "t3".to_string(),
4257                        value: ANFValue::Call {
4258                            func: "hash160".to_string(),
4259                            args: vec!["t1".to_string()],
4260                        },
4261                        source_loc: None,
4262                    },
4263                    ANFBinding {
4264                        name: "t4".to_string(),
4265                        value: ANFValue::BinOp {
4266                            op: "===".to_string(),
4267                            left: "t3".to_string(),
4268                            right: "t2".to_string(),
4269                            result_type: None,
4270                        },
4271                        source_loc: None,
4272                    },
4273                    ANFBinding {
4274                        name: "t5".to_string(),
4275                        value: ANFValue::Assert {
4276                            value: "t4".to_string(),
4277                        },
4278                        source_loc: None,
4279                    },
4280                    ANFBinding {
4281                        name: "t6".to_string(),
4282                        value: ANFValue::Call {
4283                            func: "checkSig".to_string(),
4284                            args: vec!["t0".to_string(), "t1".to_string()],
4285                        },
4286                        source_loc: None,
4287                    },
4288                    ANFBinding {
4289                        name: "t7".to_string(),
4290                        value: ANFValue::Assert {
4291                            value: "t6".to_string(),
4292                        },
4293                        source_loc: None,
4294                    },
4295                ],
4296                is_public: true,
4297            }],
4298        }
4299    }
4300
4301    #[test]
4302    fn test_p2pkh_stack_lowering_produces_placeholder_ops() {
4303        let program = p2pkh_program();
4304        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
4305        assert_eq!(methods.len(), 1);
4306        assert_eq!(methods[0].name, "unlock");
4307
4308        // There should be at least one Placeholder op (for the pubKeyHash property)
4309        let has_placeholder = methods[0].ops.iter().any(|op| {
4310            matches!(op, StackOp::Placeholder { .. })
4311        });
4312        assert!(
4313            has_placeholder,
4314            "P2PKH should have Placeholder ops for constructor params, ops: {:?}",
4315            methods[0].ops
4316        );
4317    }
4318
4319    #[test]
4320    fn test_placeholder_has_correct_param_index() {
4321        let program = p2pkh_program();
4322        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
4323
4324        // Find the Placeholder op and check its param_index
4325        let placeholders: Vec<&StackOp> = methods[0]
4326            .ops
4327            .iter()
4328            .filter(|op| matches!(op, StackOp::Placeholder { .. }))
4329            .collect();
4330
4331        assert!(
4332            !placeholders.is_empty(),
4333            "should have at least one Placeholder"
4334        );
4335
4336        // pubKeyHash is the only property at index 0
4337        if let StackOp::Placeholder {
4338            param_index,
4339            param_name,
4340        } = placeholders[0]
4341        {
4342            assert_eq!(*param_index, 0);
4343            assert_eq!(param_name, "pubKeyHash");
4344        } else {
4345            panic!("expected Placeholder op");
4346        }
4347    }
4348
4349    #[test]
4350    fn test_with_initial_values_no_placeholder_ops() {
4351        let mut program = p2pkh_program();
4352        // Set an initial value for the property -- this bakes it in
4353        program.properties[0].initial_value =
4354            Some(serde_json::Value::String("aabbccdd".to_string()));
4355
4356        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
4357        let has_placeholder = methods[0].ops.iter().any(|op| {
4358            matches!(op, StackOp::Placeholder { .. })
4359        });
4360        assert!(
4361            !has_placeholder,
4362            "with initial values, there should be no Placeholder ops"
4363        );
4364    }
4365
4366    #[test]
4367    fn test_stack_lowering_produces_standard_opcodes() {
4368        let program = p2pkh_program();
4369        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
4370
4371        // Collect all Opcode strings
4372        let opcodes: Vec<&str> = methods[0]
4373            .ops
4374            .iter()
4375            .filter_map(|op| match op {
4376                StackOp::Opcode(code) => Some(code.as_str()),
4377                _ => None,
4378            })
4379            .collect();
4380
4381        // P2PKH should contain OP_HASH160, OP_NUMEQUAL (from ===), OP_VERIFY, OP_CHECKSIG
4382        assert!(
4383            opcodes.contains(&"OP_HASH160"),
4384            "expected OP_HASH160 in opcodes: {:?}",
4385            opcodes
4386        );
4387        assert!(
4388            opcodes.contains(&"OP_CHECKSIG"),
4389            "expected OP_CHECKSIG in opcodes: {:?}",
4390            opcodes
4391        );
4392    }
4393
4394    #[test]
4395    fn test_max_stack_depth_is_tracked() {
4396        let program = p2pkh_program();
4397        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
4398        assert!(
4399            methods[0].max_stack_depth > 0,
4400            "max_stack_depth should be > 0"
4401        );
4402        // P2PKH has 2 params + some intermediates, so depth should be reasonable
4403        assert!(
4404            methods[0].max_stack_depth <= 10,
4405            "max_stack_depth should be reasonable for P2PKH, got: {}",
4406            methods[0].max_stack_depth
4407        );
4408    }
4409
4410    // -----------------------------------------------------------------------
4411    // Helper: collect all opcodes from a StackOp list (including inside If)
4412    // -----------------------------------------------------------------------
4413
4414    fn collect_all_opcodes(ops: &[StackOp]) -> Vec<String> {
4415        let mut result = Vec::new();
4416        for op in ops {
4417            match op {
4418                StackOp::Opcode(code) => result.push(code.clone()),
4419                StackOp::If { then_ops, else_ops } => {
4420                    result.push("OP_IF".to_string());
4421                    result.extend(collect_all_opcodes(then_ops));
4422                    result.push("OP_ELSE".to_string());
4423                    result.extend(collect_all_opcodes(else_ops));
4424                    result.push("OP_ENDIF".to_string());
4425                }
4426                StackOp::Push(PushValue::Int(n)) => {
4427                    result.push(format!("PUSH({})", n));
4428                }
4429                StackOp::Drop => result.push("OP_DROP".to_string()),
4430                StackOp::Swap => result.push("OP_SWAP".to_string()),
4431                StackOp::Dup => result.push("OP_DUP".to_string()),
4432                StackOp::Over => result.push("OP_OVER".to_string()),
4433                StackOp::Rot => result.push("OP_ROT".to_string()),
4434                StackOp::Nip => result.push("OP_NIP".to_string()),
4435                _ => {}
4436            }
4437        }
4438        result
4439    }
4440
4441    fn collect_opcodes_in_if_branches(ops: &[StackOp]) -> (Vec<String>, Vec<String>) {
4442        for op in ops {
4443            if let StackOp::If { then_ops, else_ops } = op {
4444                return (collect_all_opcodes(then_ops), collect_all_opcodes(else_ops));
4445            }
4446        }
4447        (vec![], vec![])
4448    }
4449
4450    // -----------------------------------------------------------------------
4451    // Fix #1: extractOutputHash offset must be 40, not 44
4452    // -----------------------------------------------------------------------
4453
4454    #[test]
4455    fn test_extract_output_hash_uses_offset_40() {
4456        // Build a stateful contract that calls extractOutputHash on a preimage
4457        let program = ANFProgram {
4458            contract_name: "TestExtract".to_string(),
4459            properties: vec![ANFProperty {
4460                name: "val".to_string(),
4461                prop_type: "bigint".to_string(),
4462                readonly: false,
4463                initial_value: Some(serde_json::Value::Number(serde_json::Number::from(0))),
4464            }],
4465            methods: vec![ANFMethod {
4466                name: "check".to_string(),
4467                params: vec![
4468                    ANFParam { name: "preimage".to_string(), param_type: "SigHashPreimage".to_string() },
4469                ],
4470                body: vec![
4471                    ANFBinding {
4472                        name: "t0".to_string(),
4473                        value: ANFValue::LoadParam { name: "preimage".to_string() },
4474                        source_loc: None,
4475                    },
4476                    ANFBinding {
4477                        name: "t1".to_string(),
4478                        value: ANFValue::Call {
4479                            func: "extractOutputHash".to_string(),
4480                            args: vec!["t0".to_string()],
4481                        },
4482                        source_loc: None,
4483                    },
4484                    ANFBinding {
4485                        name: "t2".to_string(),
4486                        value: ANFValue::LoadConst { value: serde_json::Value::Bool(true) },
4487                        source_loc: None,
4488                    },
4489                    ANFBinding {
4490                        name: "t3".to_string(),
4491                        value: ANFValue::Assert { value: "t2".to_string() },
4492                        source_loc: None,
4493                    },
4494                ],
4495                is_public: true,
4496            }],
4497        };
4498
4499        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
4500        let opcodes = collect_all_opcodes(&methods[0].ops);
4501
4502        // The offset 40 should appear as PUSH(40), not PUSH(44)
4503        assert!(
4504            opcodes.contains(&"PUSH(40)".to_string()),
4505            "extractOutputHash should use offset 40 (BIP-143 hashOutputs starts at size-40), ops: {:?}",
4506            opcodes
4507        );
4508        assert!(
4509            !opcodes.contains(&"PUSH(44)".to_string()),
4510            "extractOutputHash should NOT use offset 44, ops: {:?}",
4511            opcodes
4512        );
4513    }
4514
4515    // -----------------------------------------------------------------------
4516    // Fix #3: Terminal-if propagation
4517    // -----------------------------------------------------------------------
4518
4519    #[test]
4520    fn test_terminal_if_propagates_terminal_assert() {
4521        // A public method ending with if/else where both branches have asserts.
4522        // The terminal asserts in both branches should NOT emit OP_VERIFY.
4523        let program = ANFProgram {
4524            contract_name: "TerminalIf".to_string(),
4525            properties: vec![],
4526            methods: vec![ANFMethod {
4527                name: "check".to_string(),
4528                params: vec![
4529                    ANFParam { name: "mode".to_string(), param_type: "boolean".to_string() },
4530                    ANFParam { name: "x".to_string(), param_type: "bigint".to_string() },
4531                ],
4532                body: vec![
4533                    ANFBinding {
4534                        name: "t0".to_string(),
4535                        value: ANFValue::LoadParam { name: "mode".to_string() },
4536                        source_loc: None,
4537                    },
4538                    ANFBinding {
4539                        name: "t1".to_string(),
4540                        value: ANFValue::LoadParam { name: "x".to_string() },
4541                        source_loc: None,
4542                    },
4543                    ANFBinding {
4544                        name: "t2".to_string(),
4545                        value: ANFValue::If {
4546                            cond: "t0".to_string(),
4547                            then: vec![
4548                                ANFBinding {
4549                                    name: "t3".to_string(),
4550                                    value: ANFValue::LoadConst {
4551                                        value: serde_json::Value::Number(serde_json::Number::from(10)),
4552                                    },
4553                                    source_loc: None,
4554                                },
4555                                ANFBinding {
4556                                    name: "t4".to_string(),
4557                                    value: ANFValue::BinOp {
4558                                        op: ">".to_string(),
4559                                        left: "t1".to_string(),
4560                                        right: "t3".to_string(),
4561                                        result_type: None,
4562                                    },
4563                                    source_loc: None,
4564                                },
4565                                ANFBinding {
4566                                    name: "t5".to_string(),
4567                                    value: ANFValue::Assert { value: "t4".to_string() },
4568                                    source_loc: None,
4569                                },
4570                            ],
4571                            else_branch: vec![
4572                                ANFBinding {
4573                                    name: "t6".to_string(),
4574                                    value: ANFValue::LoadConst {
4575                                        value: serde_json::Value::Number(serde_json::Number::from(5)),
4576                                    },
4577                                    source_loc: None,
4578                                },
4579                                ANFBinding {
4580                                    name: "t7".to_string(),
4581                                    value: ANFValue::BinOp {
4582                                        op: ">".to_string(),
4583                                        left: "t1".to_string(),
4584                                        right: "t6".to_string(),
4585                                        result_type: None,
4586                                    },
4587                                    source_loc: None,
4588                                },
4589                                ANFBinding {
4590                                    name: "t8".to_string(),
4591                                    value: ANFValue::Assert { value: "t7".to_string() },
4592                                    source_loc: None,
4593                                },
4594                            ],
4595                        },
4596                        source_loc: None,
4597                    },
4598                ],
4599                is_public: true,
4600            }],
4601        };
4602
4603        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
4604
4605        // Get the opcodes inside the if branches
4606        let (then_opcodes, else_opcodes) = collect_opcodes_in_if_branches(&methods[0].ops);
4607
4608        // Neither branch should contain OP_VERIFY — the asserts are terminal
4609        assert!(
4610            !then_opcodes.contains(&"OP_VERIFY".to_string()),
4611            "then branch should not contain OP_VERIFY (terminal assert), got: {:?}",
4612            then_opcodes
4613        );
4614        assert!(
4615            !else_opcodes.contains(&"OP_VERIFY".to_string()),
4616            "else branch should not contain OP_VERIFY (terminal assert), got: {:?}",
4617            else_opcodes
4618        );
4619    }
4620
4621    // -----------------------------------------------------------------------
4622    // Fix #8: pack/unpack/toByteString builtins
4623    // -----------------------------------------------------------------------
4624
4625    #[test]
4626    fn test_unpack_emits_bin2num() {
4627        let program = ANFProgram {
4628            contract_name: "TestUnpack".to_string(),
4629            properties: vec![],
4630            methods: vec![ANFMethod {
4631                name: "check".to_string(),
4632                params: vec![
4633                    ANFParam { name: "data".to_string(), param_type: "ByteString".to_string() },
4634                ],
4635                body: vec![
4636                    ANFBinding {
4637                        name: "t0".to_string(),
4638                        value: ANFValue::LoadParam { name: "data".to_string() },
4639                        source_loc: None,
4640                    },
4641                    ANFBinding {
4642                        name: "t1".to_string(),
4643                        value: ANFValue::Call {
4644                            func: "unpack".to_string(),
4645                            args: vec!["t0".to_string()],
4646                        },
4647                        source_loc: None,
4648                    },
4649                    ANFBinding {
4650                        name: "t2".to_string(),
4651                        value: ANFValue::LoadConst {
4652                            value: serde_json::Value::Number(serde_json::Number::from(42)),
4653                        },
4654                        source_loc: None,
4655                    },
4656                    ANFBinding {
4657                        name: "t3".to_string(),
4658                        value: ANFValue::BinOp {
4659                            op: "===".to_string(),
4660                            left: "t1".to_string(),
4661                            right: "t2".to_string(),
4662                            result_type: None,
4663                        },
4664                        source_loc: None,
4665                    },
4666                    ANFBinding {
4667                        name: "t4".to_string(),
4668                        value: ANFValue::Assert { value: "t3".to_string() },
4669                        source_loc: None,
4670                    },
4671                ],
4672                is_public: true,
4673            }],
4674        };
4675
4676        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
4677        let opcodes = collect_all_opcodes(&methods[0].ops);
4678        assert!(
4679            opcodes.contains(&"OP_BIN2NUM".to_string()),
4680            "unpack should emit OP_BIN2NUM, got: {:?}",
4681            opcodes
4682        );
4683    }
4684
4685    #[test]
4686    fn test_pack_is_noop() {
4687        let program = ANFProgram {
4688            contract_name: "TestPack".to_string(),
4689            properties: vec![],
4690            methods: vec![ANFMethod {
4691                name: "check".to_string(),
4692                params: vec![
4693                    ANFParam { name: "x".to_string(), param_type: "bigint".to_string() },
4694                ],
4695                body: vec![
4696                    ANFBinding {
4697                        name: "t0".to_string(),
4698                        value: ANFValue::LoadParam { name: "x".to_string() },
4699                        source_loc: None,
4700                    },
4701                    ANFBinding {
4702                        name: "t1".to_string(),
4703                        value: ANFValue::Call {
4704                            func: "pack".to_string(),
4705                            args: vec!["t0".to_string()],
4706                        },
4707                        source_loc: None,
4708                    },
4709                    ANFBinding {
4710                        name: "t2".to_string(),
4711                        value: ANFValue::LoadConst {
4712                            value: serde_json::Value::Bool(true),
4713                        },
4714                        source_loc: None,
4715                    },
4716                    ANFBinding {
4717                        name: "t3".to_string(),
4718                        value: ANFValue::Assert { value: "t2".to_string() },
4719                        source_loc: None,
4720                    },
4721                ],
4722                is_public: true,
4723            }],
4724        };
4725
4726        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
4727        let opcodes = collect_all_opcodes(&methods[0].ops);
4728        // pack should NOT emit any conversion opcode — just pass through
4729        assert!(
4730            !opcodes.contains(&"OP_BIN2NUM".to_string()),
4731            "pack should not emit OP_BIN2NUM, got: {:?}",
4732            opcodes
4733        );
4734        assert!(
4735            !opcodes.contains(&"OP_NUM2BIN".to_string()),
4736            "pack should not emit OP_NUM2BIN, got: {:?}",
4737            opcodes
4738        );
4739    }
4740
4741    #[test]
4742    fn test_to_byte_string_is_noop() {
4743        let program = ANFProgram {
4744            contract_name: "TestToByteString".to_string(),
4745            properties: vec![],
4746            methods: vec![ANFMethod {
4747                name: "check".to_string(),
4748                params: vec![
4749                    ANFParam { name: "x".to_string(), param_type: "bigint".to_string() },
4750                ],
4751                body: vec![
4752                    ANFBinding {
4753                        name: "t0".to_string(),
4754                        value: ANFValue::LoadParam { name: "x".to_string() },
4755                        source_loc: None,
4756                    },
4757                    ANFBinding {
4758                        name: "t1".to_string(),
4759                        value: ANFValue::Call {
4760                            func: "toByteString".to_string(),
4761                            args: vec!["t0".to_string()],
4762                        },
4763                        source_loc: None,
4764                    },
4765                    ANFBinding {
4766                        name: "t2".to_string(),
4767                        value: ANFValue::LoadConst {
4768                            value: serde_json::Value::Bool(true),
4769                        },
4770                        source_loc: None,
4771                    },
4772                    ANFBinding {
4773                        name: "t3".to_string(),
4774                        value: ANFValue::Assert { value: "t2".to_string() },
4775                        source_loc: None,
4776                    },
4777                ],
4778                is_public: true,
4779            }],
4780        };
4781
4782        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
4783        let opcodes = collect_all_opcodes(&methods[0].ops);
4784        // toByteString should NOT emit any conversion opcode — just pass through
4785        assert!(
4786            !opcodes.contains(&"OP_BIN2NUM".to_string()),
4787            "toByteString should not emit OP_BIN2NUM, got: {:?}",
4788            opcodes
4789        );
4790    }
4791
4792    // -----------------------------------------------------------------------
4793    // Fix #25: sqrt(0) guard
4794    // -----------------------------------------------------------------------
4795
4796    #[test]
4797    fn test_sqrt_has_zero_guard() {
4798        let program = ANFProgram {
4799            contract_name: "TestSqrt".to_string(),
4800            properties: vec![],
4801            methods: vec![ANFMethod {
4802                name: "check".to_string(),
4803                params: vec![
4804                    ANFParam { name: "n".to_string(), param_type: "bigint".to_string() },
4805                ],
4806                body: vec![
4807                    ANFBinding {
4808                        name: "t0".to_string(),
4809                        value: ANFValue::LoadParam { name: "n".to_string() },
4810                        source_loc: None,
4811                    },
4812                    ANFBinding {
4813                        name: "t1".to_string(),
4814                        value: ANFValue::Call {
4815                            func: "sqrt".to_string(),
4816                            args: vec!["t0".to_string()],
4817                        },
4818                        source_loc: None,
4819                    },
4820                    ANFBinding {
4821                        name: "t2".to_string(),
4822                        value: ANFValue::LoadConst {
4823                            value: serde_json::Value::Number(serde_json::Number::from(0)),
4824                        },
4825                        source_loc: None,
4826                    },
4827                    ANFBinding {
4828                        name: "t3".to_string(),
4829                        value: ANFValue::BinOp {
4830                            op: ">=".to_string(),
4831                            left: "t1".to_string(),
4832                            right: "t2".to_string(),
4833                            result_type: None,
4834                        },
4835                        source_loc: None,
4836                    },
4837                    ANFBinding {
4838                        name: "t4".to_string(),
4839                        value: ANFValue::Assert { value: "t3".to_string() },
4840                        source_loc: None,
4841                    },
4842                ],
4843                is_public: true,
4844            }],
4845        };
4846
4847        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
4848        let opcodes = collect_all_opcodes(&methods[0].ops);
4849
4850        // The sqrt implementation should have OP_DUP followed by OP_IF (the zero guard).
4851        // The DUP duplicates n, then IF checks if n != 0 before Newton iteration.
4852        let dup_idx = opcodes.iter().position(|o| o == "OP_DUP");
4853        let if_idx = opcodes.iter().position(|o| o == "OP_IF");
4854
4855        assert!(
4856            dup_idx.is_some() && if_idx.is_some(),
4857            "sqrt should have OP_DUP and OP_IF for zero guard, got: {:?}",
4858            opcodes
4859        );
4860        assert!(
4861            dup_idx.unwrap() < if_idx.unwrap(),
4862            "OP_DUP should come before OP_IF in sqrt zero guard, got: {:?}",
4863            opcodes
4864        );
4865    }
4866
4867    // -----------------------------------------------------------------------
4868    // Fix #28: Loop cleanup of unused iteration variables
4869    // -----------------------------------------------------------------------
4870
4871    #[test]
4872    fn test_loop_cleans_up_unused_iter_var() {
4873        // A loop whose body has only asserts (which consume stack values).
4874        // After the body, the iter var ends up on top of the stack (depth 0),
4875        // so it should be dropped. The TS reference does this cleanup.
4876        let program = ANFProgram {
4877            contract_name: "TestLoopCleanup".to_string(),
4878            properties: vec![],
4879            methods: vec![ANFMethod {
4880                name: "check".to_string(),
4881                params: vec![
4882                    ANFParam { name: "x".to_string(), param_type: "bigint".to_string() },
4883                ],
4884                body: vec![
4885                    ANFBinding {
4886                        name: "t0".to_string(),
4887                        value: ANFValue::LoadParam { name: "x".to_string() },
4888                        source_loc: None,
4889                    },
4890                    ANFBinding {
4891                        name: "t_loop".to_string(),
4892                        value: ANFValue::Loop {
4893                            count: 3,
4894                            body: vec![
4895                                // Body uses x but not iter var __i, and asserts consume
4896                                ANFBinding {
4897                                    name: "t1".to_string(),
4898                                    value: ANFValue::LoadParam { name: "x".to_string() },
4899                                    source_loc: None,
4900                                },
4901                                ANFBinding {
4902                                    name: "t2".to_string(),
4903                                    value: ANFValue::Assert { value: "t1".to_string() },
4904                                    source_loc: None,
4905                                },
4906                            ],
4907                            iter_var: "__i".to_string(),
4908                        },
4909                        source_loc: None,
4910                    },
4911                    ANFBinding {
4912                        name: "t_final".to_string(),
4913                        value: ANFValue::LoadConst {
4914                            value: serde_json::Value::Bool(true),
4915                        },
4916                        source_loc: None,
4917                    },
4918                    ANFBinding {
4919                        name: "t_assert".to_string(),
4920                        value: ANFValue::Assert { value: "t_final".to_string() },
4921                        source_loc: None,
4922                    },
4923                ],
4924                is_public: true,
4925            }],
4926        };
4927
4928        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
4929        let opcodes = collect_all_opcodes(&methods[0].ops);
4930
4931        // Each iteration pushes __i, then the body asserts (consuming its value).
4932        // After each iteration, __i is on top (depth 0) and should be dropped.
4933        // With 3 iterations, we expect at least 3 OP_DROP ops (one per iter var cleanup).
4934        let drop_count = opcodes.iter().filter(|o| o.as_str() == "OP_DROP").count();
4935        assert!(
4936            drop_count >= 3,
4937            "unused iter var should be dropped after each iteration; expected >= 3 OP_DROPs, got {}: {:?}",
4938            drop_count,
4939            opcodes
4940        );
4941    }
4942
4943    // -----------------------------------------------------------------------
4944    // Fix #29: PushValue::Int uses i128 (no overflow for large values)
4945    // -----------------------------------------------------------------------
4946
4947    #[test]
4948    fn test_push_value_int_large_values() {
4949        // Verify that PushValue::Int can hold values larger than i64::MAX
4950        let large_val: i128 = (i64::MAX as i128) + 1;
4951        let push = PushValue::Int(large_val);
4952        if let PushValue::Int(v) = push {
4953            assert_eq!(v, large_val, "PushValue::Int should store values > i64::MAX without truncation");
4954        } else {
4955            panic!("expected PushValue::Int");
4956        }
4957
4958        // Also test negative extreme
4959        let neg_val: i128 = (i64::MIN as i128) - 1;
4960        let push_neg = PushValue::Int(neg_val);
4961        if let PushValue::Int(v) = push_neg {
4962            assert_eq!(v, neg_val, "PushValue::Int should store values < i64::MIN without truncation");
4963        } else {
4964            panic!("expected PushValue::Int");
4965        }
4966    }
4967
4968    #[test]
4969    fn test_push_value_int_encodes_large_number() {
4970        // Verify that a large number (> i64::MAX) can be pushed and encoded
4971        use crate::codegen::emit::encode_push_int;
4972
4973        let large_val: i128 = 1i128 << 100;
4974        let (hex, _asm) = encode_push_int(large_val);
4975        // Should produce a valid hex encoding, not panic or truncate
4976        assert!(!hex.is_empty(), "encoding of 2^100 should produce non-empty hex");
4977
4978        // Verify the encoding length is reasonable for a 13-byte number
4979        // 2^100 needs 13 bytes in script number encoding (sign-magnitude)
4980        // Push data: 0x0d (length 13) + 13 bytes = 14 bytes = 28 hex chars
4981        assert!(
4982            hex.len() >= 26,
4983            "2^100 should need at least 13 bytes of push data, got hex length {}: {}",
4984            hex.len(),
4985            hex
4986        );
4987    }
4988
4989    // -----------------------------------------------------------------------
4990    // log2 uses bit-scanning (OP_DIV + OP_GREATERTHAN), not byte approx
4991    // -----------------------------------------------------------------------
4992
4993    #[test]
4994    fn test_log2_uses_bit_scanning_not_byte_approx() {
4995        let program = ANFProgram {
4996            contract_name: "TestLog2".to_string(),
4997            properties: vec![],
4998            methods: vec![ANFMethod {
4999                name: "check".to_string(),
5000                params: vec![
5001                    ANFParam { name: "n".to_string(), param_type: "bigint".to_string() },
5002                ],
5003                body: vec![
5004                    ANFBinding {
5005                        name: "t0".to_string(),
5006                        value: ANFValue::LoadParam { name: "n".to_string() },
5007                        source_loc: None,
5008                    },
5009                    ANFBinding {
5010                        name: "t1".to_string(),
5011                        value: ANFValue::Call {
5012                            func: "log2".to_string(),
5013                            args: vec!["t0".to_string()],
5014                        },
5015                        source_loc: None,
5016                    },
5017                    ANFBinding {
5018                        name: "t2".to_string(),
5019                        value: ANFValue::LoadConst {
5020                            value: serde_json::Value::Number(serde_json::Number::from(0)),
5021                        },
5022                        source_loc: None,
5023                    },
5024                    ANFBinding {
5025                        name: "t3".to_string(),
5026                        value: ANFValue::BinOp {
5027                            op: ">=".to_string(),
5028                            left: "t1".to_string(),
5029                            right: "t2".to_string(),
5030                            result_type: None,
5031                        },
5032                        source_loc: None,
5033                    },
5034                    ANFBinding {
5035                        name: "t4".to_string(),
5036                        value: ANFValue::Assert { value: "t3".to_string() },
5037                        source_loc: None,
5038                    },
5039                ],
5040                is_public: true,
5041            }],
5042        };
5043
5044        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
5045        let opcodes = collect_all_opcodes(&methods[0].ops);
5046
5047        // The bit-scanning implementation must use OP_DIV and OP_GREATERTHAN
5048        assert!(
5049            opcodes.contains(&"OP_DIV".to_string()),
5050            "log2 should use OP_DIV (bit-scanning), got: {:?}",
5051            opcodes
5052        );
5053        assert!(
5054            opcodes.contains(&"OP_GREATERTHAN".to_string()),
5055            "log2 should use OP_GREATERTHAN (bit-scanning), got: {:?}",
5056            opcodes
5057        );
5058
5059        // The old byte-approximation used OP_SIZE and OP_MUL — must NOT be present
5060        assert!(
5061            !opcodes.contains(&"OP_SIZE".to_string()),
5062            "log2 should NOT use OP_SIZE (old byte approximation), got: {:?}",
5063            opcodes
5064        );
5065        assert!(
5066            !opcodes.contains(&"OP_MUL".to_string()),
5067            "log2 should NOT use OP_MUL (old byte approximation), got: {:?}",
5068            opcodes
5069        );
5070
5071        // Should have OP_1ADD for counter increment
5072        assert!(
5073            opcodes.contains(&"OP_1ADD".to_string()),
5074            "log2 should use OP_1ADD (counter increment), got: {:?}",
5075            opcodes
5076        );
5077    }
5078
5079    // -----------------------------------------------------------------------
5080    // reverseBytes uses OP_SPLIT + OP_CAT (not non-existent OP_REVERSE)
5081    // -----------------------------------------------------------------------
5082
5083    #[test]
5084    fn test_reverse_bytes_uses_split_cat_not_op_reverse() {
5085        let program = ANFProgram {
5086            contract_name: "TestReverse".to_string(),
5087            properties: vec![],
5088            methods: vec![ANFMethod {
5089                name: "check".to_string(),
5090                params: vec![
5091                    ANFParam { name: "data".to_string(), param_type: "ByteString".to_string() },
5092                ],
5093                body: vec![
5094                    ANFBinding {
5095                        name: "t0".to_string(),
5096                        value: ANFValue::LoadParam { name: "data".to_string() },
5097                        source_loc: None,
5098                    },
5099                    ANFBinding {
5100                        name: "t1".to_string(),
5101                        value: ANFValue::Call {
5102                            func: "reverseBytes".to_string(),
5103                            args: vec!["t0".to_string()],
5104                        },
5105                        source_loc: None,
5106                    },
5107                    ANFBinding {
5108                        name: "t2".to_string(),
5109                        value: ANFValue::LoadConst {
5110                            value: serde_json::Value::Bool(true),
5111                        },
5112                        source_loc: None,
5113                    },
5114                    ANFBinding {
5115                        name: "t3".to_string(),
5116                        value: ANFValue::Assert { value: "t2".to_string() },
5117                        source_loc: None,
5118                    },
5119                ],
5120                is_public: true,
5121            }],
5122        };
5123
5124        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
5125        let opcodes = collect_all_opcodes(&methods[0].ops);
5126
5127        // Must NOT contain the non-existent OP_REVERSE
5128        assert!(
5129            !opcodes.contains(&"OP_REVERSE".to_string()),
5130            "reverseBytes must NOT emit OP_REVERSE (does not exist), got: {:?}",
5131            opcodes
5132        );
5133
5134        // Must use OP_SPLIT and OP_CAT for byte-by-byte reversal
5135        assert!(
5136            opcodes.contains(&"OP_SPLIT".to_string()),
5137            "reverseBytes should emit OP_SPLIT for byte peeling, got: {:?}",
5138            opcodes
5139        );
5140        assert!(
5141            opcodes.contains(&"OP_CAT".to_string()),
5142            "reverseBytes should emit OP_CAT for reassembly, got: {:?}",
5143            opcodes
5144        );
5145
5146        // Should use OP_SIZE to check remaining length
5147        assert!(
5148            opcodes.contains(&"OP_SIZE".to_string()),
5149            "reverseBytes should emit OP_SIZE for length check, got: {:?}",
5150            opcodes
5151        );
5152    }
5153
5154    // -----------------------------------------------------------------------
5155    // Test: only public methods appear in stack output (method count)
5156    // -----------------------------------------------------------------------
5157
5158    #[test]
5159    fn test_method_count_matches_public_methods() {
5160        // P2PKH program has 1 public method (unlock) and 1 constructor (non-public)
5161        let program = p2pkh_program();
5162        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
5163
5164        // Should have exactly 1 method (unlock) — constructor is skipped
5165        assert_eq!(
5166            methods.len(),
5167            1,
5168            "expected 1 stack method (unlock), got {}: {:?}",
5169            methods.len(),
5170            methods.iter().map(|m| &m.name).collect::<Vec<_>>()
5171        );
5172        assert_eq!(methods[0].name, "unlock");
5173    }
5174
5175    // -----------------------------------------------------------------------
5176    // Test: multi-method contract has correct number of StackMethods
5177    // -----------------------------------------------------------------------
5178
5179    #[test]
5180    fn test_multi_method_dispatch() {
5181        let program = ANFProgram {
5182            contract_name: "Multi".to_string(),
5183            properties: vec![],
5184            methods: vec![
5185                ANFMethod {
5186                    name: "constructor".to_string(),
5187                    params: vec![],
5188                    body: vec![],
5189                    is_public: false,
5190                },
5191                ANFMethod {
5192                    name: "method1".to_string(),
5193                    params: vec![ANFParam {
5194                        name: "x".to_string(),
5195                        param_type: "bigint".to_string(),
5196                    }],
5197                    body: vec![
5198                        ANFBinding {
5199                            name: "t0".to_string(),
5200                            value: ANFValue::LoadParam { name: "x".to_string() },
5201                            source_loc: None,
5202                        },
5203                        ANFBinding {
5204                            name: "t1".to_string(),
5205                            value: ANFValue::LoadConst {
5206                                value: serde_json::Value::Number(serde_json::Number::from(42)),
5207                            },
5208                            source_loc: None,
5209                        },
5210                        ANFBinding {
5211                            name: "t2".to_string(),
5212                            value: ANFValue::BinOp {
5213                                op: "===".to_string(),
5214                                left: "t0".to_string(),
5215                                right: "t1".to_string(),
5216                                result_type: None,
5217                            },
5218                            source_loc: None,
5219                        },
5220                        ANFBinding {
5221                            name: "t3".to_string(),
5222                            value: ANFValue::Assert { value: "t2".to_string() },
5223                            source_loc: None,
5224                        },
5225                    ],
5226                    is_public: true,
5227                },
5228                ANFMethod {
5229                    name: "method2".to_string(),
5230                    params: vec![ANFParam {
5231                        name: "y".to_string(),
5232                        param_type: "bigint".to_string(),
5233                    }],
5234                    body: vec![
5235                        ANFBinding {
5236                            name: "t0".to_string(),
5237                            value: ANFValue::LoadParam { name: "y".to_string() },
5238                            source_loc: None,
5239                        },
5240                        ANFBinding {
5241                            name: "t1".to_string(),
5242                            value: ANFValue::LoadConst {
5243                                value: serde_json::Value::Number(serde_json::Number::from(100)),
5244                            },
5245                            source_loc: None,
5246                        },
5247                        ANFBinding {
5248                            name: "t2".to_string(),
5249                            value: ANFValue::BinOp {
5250                                op: "===".to_string(),
5251                                left: "t0".to_string(),
5252                                right: "t1".to_string(),
5253                                result_type: None,
5254                            },
5255                            source_loc: None,
5256                        },
5257                        ANFBinding {
5258                            name: "t3".to_string(),
5259                            value: ANFValue::Assert { value: "t2".to_string() },
5260                            source_loc: None,
5261                        },
5262                    ],
5263                    is_public: true,
5264                },
5265            ],
5266        };
5267
5268        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
5269        assert_eq!(
5270            methods.len(),
5271            2,
5272            "expected 2 stack methods, got {}: {:?}",
5273            methods.len(),
5274            methods.iter().map(|m| &m.name).collect::<Vec<_>>()
5275        );
5276    }
5277
5278    // -----------------------------------------------------------------------
5279    // Test: extractOutputs uses offset 40, not 44
5280    // -----------------------------------------------------------------------
5281
5282    #[test]
5283    fn test_extract_outputs_uses_offset_40() {
5284        let program = ANFProgram {
5285            contract_name: "OutputsCheck".to_string(),
5286            properties: vec![],
5287            methods: vec![ANFMethod {
5288                name: "check".to_string(),
5289                params: vec![ANFParam {
5290                    name: "preimage".to_string(),
5291                    param_type: "SigHashPreimage".to_string(),
5292                }],
5293                body: vec![
5294                    ANFBinding {
5295                        name: "t0".to_string(),
5296                        value: ANFValue::LoadParam { name: "preimage".to_string() },
5297                        source_loc: None,
5298                    },
5299                    ANFBinding {
5300                        name: "t1".to_string(),
5301                        value: ANFValue::Call {
5302                            func: "extractOutputs".to_string(),
5303                            args: vec!["t0".to_string()],
5304                        },
5305                        source_loc: None,
5306                    },
5307                    ANFBinding {
5308                        name: "t2".to_string(),
5309                        value: ANFValue::Assert { value: "t1".to_string() },
5310                        source_loc: None,
5311                    },
5312                ],
5313                is_public: true,
5314            }],
5315        };
5316
5317        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
5318        let opcodes = collect_all_opcodes(&methods[0].ops);
5319
5320        // The offset for extractOutputs should be 40 (hashOutputs(32) + nLocktime(4) + sighashType(4))
5321        // Encoded as PUSH(40)
5322        assert!(
5323            opcodes.contains(&"PUSH(40)".to_string()),
5324            "expected PUSH(40) for extractOutputs offset, got: {:?}",
5325            opcodes
5326        );
5327        // Must NOT use the old incorrect offset 44
5328        assert!(
5329            !opcodes.contains(&"PUSH(44)".to_string()),
5330            "extractOutputs should NOT use offset 44, got: {:?}",
5331            opcodes
5332        );
5333    }
5334
5335    // -----------------------------------------------------------------------
5336    // Test: arithmetic binary op (a + b) produces OP_ADD in stack output
5337    // Mirrors Go TestLowerToStack_ArithmeticOps
5338    // -----------------------------------------------------------------------
5339
5340    #[test]
5341    fn test_arithmetic_ops_contains_add() {
5342        // Contract: verify(a, b) { assert(a + b === target) }
5343        let program = ANFProgram {
5344            contract_name: "ArithCheck".to_string(),
5345            properties: vec![ANFProperty {
5346                name: "target".to_string(),
5347                prop_type: "bigint".to_string(),
5348                readonly: true,
5349                initial_value: None,
5350            }],
5351            methods: vec![ANFMethod {
5352                name: "verify".to_string(),
5353                params: vec![
5354                    ANFParam { name: "a".to_string(), param_type: "bigint".to_string() },
5355                    ANFParam { name: "b".to_string(), param_type: "bigint".to_string() },
5356                ],
5357                body: vec![
5358                    ANFBinding {
5359                        name: "t0".to_string(),
5360                        value: ANFValue::LoadParam { name: "a".to_string() },
5361                        source_loc: None,
5362                    },
5363                    ANFBinding {
5364                        name: "t1".to_string(),
5365                        value: ANFValue::LoadParam { name: "b".to_string() },
5366                        source_loc: None,
5367                    },
5368                    ANFBinding {
5369                        name: "t2".to_string(),
5370                        value: ANFValue::BinOp {
5371                            op: "+".to_string(),
5372                            left: "t0".to_string(),
5373                            right: "t1".to_string(),
5374                            result_type: None,
5375                        },
5376                        source_loc: None,
5377                    },
5378                    ANFBinding {
5379                        name: "t3".to_string(),
5380                        value: ANFValue::LoadProp { name: "target".to_string() },
5381                        source_loc: None,
5382                    },
5383                    ANFBinding {
5384                        name: "t4".to_string(),
5385                        value: ANFValue::BinOp {
5386                            op: "===".to_string(),
5387                            left: "t2".to_string(),
5388                            right: "t3".to_string(),
5389                            result_type: None,
5390                        },
5391                        source_loc: None,
5392                    },
5393                    ANFBinding {
5394                        name: "t5".to_string(),
5395                        value: ANFValue::Assert { value: "t4".to_string() },
5396                        source_loc: None,
5397                    },
5398                ],
5399                is_public: true,
5400            }],
5401        };
5402
5403        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
5404        let opcodes = collect_all_opcodes(&methods[0].ops);
5405
5406        // The a + b operation should emit OP_ADD
5407        assert!(
5408            opcodes.contains(&"OP_ADD".to_string()),
5409            "expected OP_ADD in stack ops for 'a + b', got: {:?}",
5410            opcodes
5411        );
5412
5413        // The === comparison should emit OP_NUMEQUAL
5414        assert!(
5415            opcodes.contains(&"OP_NUMEQUAL".to_string()),
5416            "expected OP_NUMEQUAL in stack ops for '===', got: {:?}",
5417            opcodes
5418        );
5419    }
5420
5421    // -----------------------------------------------------------------------
5422    // S18: PICK/ROLL depth ≤ max_stack_depth (stack invariant)
5423    // After lowering P2PKH, verify no Pick or Roll references a depth ≥ max_stack_depth
5424    // -----------------------------------------------------------------------
5425
5426    #[test]
5427    fn test_s18_pick_roll_depth_within_max_stack_depth() {
5428        let program = p2pkh_program();
5429        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
5430
5431        let max_depth = methods[0].max_stack_depth;
5432
5433        fn check_ops(ops: &[StackOp], max_depth: usize) {
5434            for op in ops {
5435                match op {
5436                    StackOp::Pick { depth } => {
5437                        assert!(
5438                            *depth < max_depth,
5439                            "Pick depth {} must be < max_stack_depth {}",
5440                            depth,
5441                            max_depth
5442                        );
5443                    }
5444                    StackOp::Roll { depth } => {
5445                        assert!(
5446                            *depth < max_depth,
5447                            "Roll depth {} must be < max_stack_depth {}",
5448                            depth,
5449                            max_depth
5450                        );
5451                    }
5452                    StackOp::If { then_ops, else_ops } => {
5453                        check_ops(then_ops, max_depth);
5454                        check_ops(else_ops, max_depth);
5455                    }
5456                    _ => {}
5457                }
5458            }
5459        }
5460
5461        check_ops(&methods[0].ops, max_depth);
5462    }
5463
5464    // -----------------------------------------------------------------------
5465    // Row 190: ByteString concatenation (bin_op "+", result_type="bytes") → OP_CAT
5466    // Row 189 (bigint add) → OP_ADD is already tested above.
5467    // -----------------------------------------------------------------------
5468
5469    #[test]
5470    fn test_bytestring_concat_emits_op_cat() {
5471        let program = ANFProgram {
5472            contract_name: "CatCheck".to_string(),
5473            properties: vec![],
5474            methods: vec![ANFMethod {
5475                name: "verify".to_string(),
5476                params: vec![
5477                    ANFParam { name: "a".to_string(), param_type: "ByteString".to_string() },
5478                    ANFParam { name: "b".to_string(), param_type: "ByteString".to_string() },
5479                    ANFParam { name: "expected".to_string(), param_type: "ByteString".to_string() },
5480                ],
5481                body: vec![
5482                    ANFBinding {
5483                        name: "t0".to_string(),
5484                        value: ANFValue::LoadParam { name: "a".to_string() },
5485                        source_loc: None,
5486                    },
5487                    ANFBinding {
5488                        name: "t1".to_string(),
5489                        value: ANFValue::LoadParam { name: "b".to_string() },
5490                        source_loc: None,
5491                    },
5492                    ANFBinding {
5493                        name: "t2".to_string(),
5494                        value: ANFValue::BinOp {
5495                            op: "+".to_string(),
5496                            left: "t0".to_string(),
5497                            right: "t1".to_string(),
5498                            result_type: Some("bytes".to_string()), // ByteString concat
5499                        },
5500                        source_loc: None,
5501                    },
5502                    ANFBinding {
5503                        name: "t3".to_string(),
5504                        value: ANFValue::LoadParam { name: "expected".to_string() },
5505                        source_loc: None,
5506                    },
5507                    ANFBinding {
5508                        name: "t4".to_string(),
5509                        value: ANFValue::BinOp {
5510                            op: "===".to_string(),
5511                            left: "t2".to_string(),
5512                            right: "t3".to_string(),
5513                            result_type: Some("bytes".to_string()),
5514                        },
5515                        source_loc: None,
5516                    },
5517                    ANFBinding {
5518                        name: "t5".to_string(),
5519                        value: ANFValue::Assert { value: "t4".to_string() },
5520                        source_loc: None,
5521                    },
5522                ],
5523                is_public: true,
5524            }],
5525        };
5526
5527        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
5528        let opcodes = collect_all_opcodes(&methods[0].ops);
5529
5530        assert!(
5531            opcodes.contains(&"OP_CAT".to_string()),
5532            "ByteString '+' (result_type='bytes') should emit OP_CAT; got opcodes: {:?}",
5533            opcodes
5534        );
5535        assert!(
5536            !opcodes.contains(&"OP_ADD".to_string()),
5537            "ByteString '+' should NOT emit OP_ADD (that's for bigint); got opcodes: {:?}",
5538            opcodes
5539        );
5540    }
5541
5542    // -----------------------------------------------------------------------
5543    // Row 201: log2 emits exactly 64 if-ops with OP_DIV+OP_1ADD
5544    // (bit-scanning: 64 iterations, one per bit of a 64-bit integer)
5545    // -----------------------------------------------------------------------
5546
5547    #[test]
5548    fn test_log2_emits_64_if_ops() {
5549        let program = ANFProgram {
5550            contract_name: "TestLog2Count".to_string(),
5551            properties: vec![],
5552            methods: vec![ANFMethod {
5553                name: "check".to_string(),
5554                params: vec![
5555                    ANFParam { name: "n".to_string(), param_type: "bigint".to_string() },
5556                ],
5557                body: vec![
5558                    ANFBinding {
5559                        name: "t0".to_string(),
5560                        value: ANFValue::LoadParam { name: "n".to_string() },
5561                        source_loc: None,
5562                    },
5563                    ANFBinding {
5564                        name: "t1".to_string(),
5565                        value: ANFValue::Call {
5566                            func: "log2".to_string(),
5567                            args: vec!["t0".to_string()],
5568                        },
5569                        source_loc: None,
5570                    },
5571                    ANFBinding {
5572                        name: "t2".to_string(),
5573                        value: ANFValue::LoadConst {
5574                            value: serde_json::Value::Number(serde_json::Number::from(0)),
5575                        },
5576                        source_loc: None,
5577                    },
5578                    ANFBinding {
5579                        name: "t3".to_string(),
5580                        value: ANFValue::BinOp {
5581                            op: ">=".to_string(),
5582                            left: "t1".to_string(),
5583                            right: "t2".to_string(),
5584                            result_type: None,
5585                        },
5586                        source_loc: None,
5587                    },
5588                    ANFBinding {
5589                        name: "t4".to_string(),
5590                        value: ANFValue::Assert { value: "t3".to_string() },
5591                        source_loc: None,
5592                    },
5593                ],
5594                is_public: true,
5595            }],
5596        };
5597
5598        let methods = lower_to_stack(&program).expect("stack lowering should succeed");
5599
5600        // Count OP_IF occurrences — there should be exactly 64 (one per bit)
5601        fn count_if_ops(ops: &[StackOp]) -> usize {
5602            let mut count = 0;
5603            for op in ops {
5604                match op {
5605                    StackOp::If { then_ops, else_ops } => {
5606                        count += 1;
5607                        count += count_if_ops(then_ops);
5608                        count += count_if_ops(else_ops);
5609                    }
5610                    _ => {}
5611                }
5612            }
5613            count
5614        }
5615
5616        let if_count = count_if_ops(&methods[0].ops);
5617        assert_eq!(
5618            if_count, 64,
5619            "log2 should emit exactly 64 if-ops (one per bit); got {} if-ops",
5620            if_count
5621        );
5622    }
5623}