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