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