Skip to main content

luadec_rust/lua51/
lifter.rs

1use std::collections::{HashMap, HashSet};
2
3use luac_parser::{LuaChunk, LuaConstant, LuaNumber};
4
5use crate::lua51::ast::*;
6use crate::lua51::cfg::{BasicBlock, ControlFlowGraph, EdgeKind};
7use crate::lua51::dominator::{find_loops, DominatorTree, LoopKind, NaturalLoop};
8use crate::lua51::instruction::{is_k, index_k};
9use crate::lua51::liveness::{compute_liveness, is_reg_live_after, LivenessInfo};
10use crate::lua51::opcodes::OpCode;
11
12/// Context for decompiling a single Lua function.
13pub struct Lifter<'a> {
14    chunk: &'a LuaChunk,
15    cfg: ControlFlowGraph,
16    _dom: DominatorTree,
17    loops: Vec<NaturalLoop>,
18    liveness: LivenessInfo,
19    /// Register expressions: tracks what expression is currently held in each register.
20    regs: Vec<Option<Expr>>,
21    /// Pending tables being constructed (register -> accumulated fields).
22    pending_tables: HashMap<u32, Vec<TableField>>,
23    /// Stable references a register value has been assigned into.
24    capture_aliases: HashMap<u32, Expr>,
25    /// Registers that are updated from their own previous value across branches.
26    accumulator_regs: HashSet<u32>,
27    /// Blocks already visited to prevent infinite recursion.
28    visited_blocks: HashSet<usize>,
29    /// Local variable names assigned to registers (reg -> name).
30    local_names: HashMap<u32, String>,
31    /// Registers that have been declared as `local`.
32    declared_locals: HashSet<u32>,
33    /// Number of parameters (these registers are implicitly declared).
34    num_params: u32,
35    /// Whether this chunk has debug info (locals/upvalue names).
36    has_debug_info: bool,
37    /// Upvalue expressions resolved from the parent closure site.
38    resolved_upvalues: Vec<Option<Expr>>,
39    /// Active loop headers being lifted to avoid re-entering the same loop.
40    active_loop_headers: Vec<usize>,
41    /// Exit blocks for active loops, innermost last.
42    active_loop_exits: Vec<usize>,
43}
44
45impl<'a> Lifter<'a> {
46    pub fn decompile(chunk: &'a LuaChunk) -> Function {
47        Self::decompile_with_upvalues(chunk, Vec::new())
48    }
49
50    fn decompile_with_upvalues(
51        chunk: &'a LuaChunk,
52        resolved_upvalues: Vec<Option<Expr>>,
53    ) -> Function {
54        let cfg = ControlFlowGraph::build(&chunk.instructions);
55        let dom = DominatorTree::build(&cfg);
56        let loops = find_loops(&cfg, &dom);
57        let liveness = compute_liveness(&cfg, chunk.max_stack as usize);
58        let has_debug_info = !chunk.locals.is_empty();
59
60        let max_stack = chunk.max_stack as usize;
61        let mut lifter = Lifter {
62            chunk,
63            cfg,
64            _dom: dom,
65            loops,
66            liveness,
67            regs: vec![None; max_stack.max(256)],
68            pending_tables: HashMap::new(),
69            capture_aliases: HashMap::new(),
70            accumulator_regs: HashSet::new(),
71            visited_blocks: HashSet::new(),
72            local_names: HashMap::new(),
73            declared_locals: HashSet::new(),
74            num_params: chunk.num_params as u32,
75            has_debug_info,
76            resolved_upvalues,
77            active_loop_headers: Vec::new(),
78            active_loop_exits: Vec::new(),
79        };
80
81        lifter.accumulator_regs = lifter.find_accumulator_regs();
82
83        let params: Vec<String> = (0..chunk.num_params as u32)
84            .map(|i| {
85                let name = lifter.local_name(i, 0);
86                lifter.local_names.insert(i, name.clone());
87                lifter.declared_locals.insert(i);
88                lifter.set_reg(i, Expr::Name(name.clone()));
89                name
90            })
91            .collect();
92        let is_vararg = chunk.is_vararg.is_some();
93
94        let body = if lifter.cfg.num_blocks() > 0 {
95            lifter.lift_block_range(0, lifter.cfg.num_blocks())
96        } else {
97            Vec::new()
98        };
99
100        Function {
101            params,
102            is_vararg,
103            body,
104        }
105    }
106
107    /// Lift a range of blocks into a statement list, handling control structures.
108    fn lift_block_range(&mut self, start_block: usize, end_block: usize) -> Block {
109        let mut stmts = Vec::new();
110        let mut block_idx = start_block;
111
112        while block_idx < end_block && block_idx < self.cfg.num_blocks() {
113            // Prevent revisiting blocks
114            if self.visited_blocks.contains(&block_idx) {
115                block_idx += 1;
116                continue;
117            }
118
119            // Check if this block is a loop header
120            if let Some(lp) = self.find_loop_at(block_idx) {
121                let lp = lp.clone();
122                let next = self.lift_loop(&lp, &mut stmts);
123                if next <= block_idx {
124                    // Safety: avoid infinite loop
125                    block_idx += 1;
126                } else {
127                    block_idx = next;
128                }
129                continue;
130            }
131
132            self.visited_blocks.insert(block_idx);
133
134            let block = self.cfg.blocks[block_idx].clone();
135            let _last_pc = block.end;
136
137            // Check for conditional (if/elseif/else)
138            if self.is_conditional_block(&block) {
139                let next = self.lift_conditional(block_idx, &mut stmts);
140                if next <= block_idx {
141                    // Safety: avoid infinite loop, lift as normal instructions
142                    self.lift_instructions(block.start, block.end, &mut stmts);
143                    block_idx += 1;
144                } else {
145                    block_idx = next;
146                }
147                continue;
148            }
149
150            // Normal block: lift instructions sequentially
151            self.lift_instructions(block.start, block.end, &mut stmts);
152
153            // Check if this block ends with a JMP to a loop exit (break)
154            let last_inst = self.cfg.instructions[block.end];
155            if last_inst.op == OpCode::Jmp && block.successors.len() == 1 {
156                let target = block.successors[0];
157                if self.current_loop_exit() == Some(target) {
158                    stmts.push(Stat::Break);
159                }
160                // Follow unconditional JMP: skip ahead to target block
161                // (blocks between here and target are only reachable via the target)
162                if target > block_idx + 1 {
163                    block_idx = target;
164                    continue;
165                }
166            }
167
168            block_idx += 1;
169        }
170
171        stmts
172    }
173
174    /// Lift a single loop structure.
175    fn lift_loop(&mut self, lp: &NaturalLoop, stmts: &mut Block) -> usize {
176        match lp.kind {
177            LoopKind::NumericFor => self.lift_numeric_for(lp, stmts),
178            LoopKind::GenericFor => self.lift_generic_for(lp, stmts),
179            LoopKind::WhileRepeat => self.lift_while(lp, stmts),
180        }
181    }
182
183    fn lift_numeric_for(&mut self, lp: &NaturalLoop, stmts: &mut Block) -> usize {
184        let header = &self.cfg.blocks[lp.header].clone();
185        let loop_exit = self.max_loop_block(lp) + 1;
186
187        // Find the FORPREP instruction: it's in the block preceding the header
188        // or in the header itself.  The FORPREP's A register tells us the for-loop slots.
189        let forprep_block = self.find_forprep_block(lp.header);
190        let forprep_inst = if let Some(fb) = forprep_block {
191            let b = &self.cfg.blocks[fb];
192            self.cfg.instructions[b.end]
193        } else {
194            self.cfg.instructions[header.start]
195        };
196
197        let base = forprep_inst.a;
198        let var_name = self.local_name(base + 3, header.start);
199
200        // Lift the pre-loop setup to get init/limit/step
201        if let Some(fb) = forprep_block {
202            if !self.visited_blocks.contains(&fb) {
203                let b = &self.cfg.blocks[fb].clone();
204                self.visited_blocks.insert(fb);
205                // Lift instructions before FORPREP to set up init/limit/step
206                if b.end > b.start {
207                    self.lift_instructions(b.start, b.end - 1, stmts);
208                }
209            }
210        }
211
212        let start_expr = self.reg_expr(base);
213        let limit_expr = self.reg_expr(base + 1);
214        let step_expr = self.reg_expr(base + 2);
215        let step = if matches!(&step_expr, Expr::Number(NumLit::Int(1))) {
216            None
217        } else {
218            Some(step_expr)
219        };
220
221        self.active_loop_headers.push(lp.header);
222        self.active_loop_exits.push(loop_exit);
223        let body = self.lift_block_range(lp.header, lp.latch + 1);
224        self.active_loop_exits.pop();
225        self.active_loop_headers.pop();
226
227        stmts.push(Stat::NumericFor {
228            name: var_name,
229            start: start_expr,
230            limit: limit_expr,
231            step,
232            body,
233        });
234
235        // Return the block after the loop exit
236        loop_exit
237    }
238
239    fn lift_generic_for(&mut self, lp: &NaturalLoop, stmts: &mut Block) -> usize {
240        let header = &self.cfg.blocks[lp.header].clone();
241
242        // Find TFORLOOP instruction in the header or latch block
243        let mut tforloop_inst = None;
244        for pc in header.start..=header.end {
245            if self.cfg.instructions[pc].op == OpCode::TForLoop {
246                tforloop_inst = Some(self.cfg.instructions[pc]);
247                break;
248            }
249        }
250        if tforloop_inst.is_none() {
251            let latch_block = &self.cfg.blocks[lp.latch].clone();
252            for pc in latch_block.start..=latch_block.end {
253                if self.cfg.instructions[pc].op == OpCode::TForLoop {
254                    tforloop_inst = Some(self.cfg.instructions[pc]);
255                    break;
256                }
257            }
258        }
259        let tfl = tforloop_inst.unwrap_or(self.cfg.instructions[header.end]);
260
261        let base = tfl.a;
262        let num_vars = tfl.c();
263
264        let names: Vec<String> = (0..num_vars)
265            .map(|i| self.local_name(base + 3 + i, header.start))
266            .collect();
267
268        // Register loop variable names so the body can reference them
269        for (i, name) in names.iter().enumerate() {
270            let r = base + 3 + i as u32;
271            self.local_names.insert(r, name.clone());
272            self.declared_locals.insert(r);
273            self.set_reg(r, Expr::Name(name.clone()));
274        }
275
276        let iter_expr = self.reg_expr(base);
277
278        // Body: loop blocks excluding the header, sorted by block ID
279        let mut body_blocks: Vec<usize> = lp.body.iter()
280            .filter(|&&b| b != lp.header)
281            .copied()
282            .collect();
283        body_blocks.sort();
284
285        self.active_loop_headers.push(lp.header);
286        self.active_loop_exits.push(self.max_loop_block(lp) + 1);
287        let body = if !body_blocks.is_empty() {
288            let first = *body_blocks.first().unwrap();
289            let last = *body_blocks.last().unwrap();
290            self.lift_block_range(first, last + 1)
291        } else {
292            Vec::new()
293        };
294        self.active_loop_exits.pop();
295        self.active_loop_headers.pop();
296
297        stmts.push(Stat::GenericFor {
298            names,
299            iterators: vec![iter_expr],
300            body,
301        });
302
303        self.max_loop_block(lp) + 1
304    }
305
306    fn lift_while(&mut self, lp: &NaturalLoop, stmts: &mut Block) -> usize {
307        let _header = &self.cfg.blocks[lp.header].clone();
308
309        // Try to extract condition from header block
310        let cond = self.extract_condition(lp.header).unwrap_or(Expr::Bool(true));
311
312        // Body: blocks in the loop excluding header
313        self.active_loop_headers.push(lp.header);
314        self.active_loop_exits.push(self.max_loop_block(lp) + 1);
315        let body_start = lp.header + 1;
316        let body_end = lp.latch + 1;
317        let body = self.lift_block_range(body_start, body_end);
318        self.active_loop_exits.pop();
319        self.active_loop_headers.pop();
320
321        stmts.push(Stat::While { cond, body });
322
323        self.max_loop_block(lp) + 1
324    }
325
326    /// Lift an if/elseif/else chain.
327    fn lift_conditional(&mut self, block_idx: usize, stmts: &mut Block) -> usize {
328        // Try to detect and lift OR/AND short-circuit chains first
329        if let Some(next) = self.try_lift_or_and_chain(block_idx, stmts) {
330            return next;
331        }
332
333        let block = self.cfg.blocks[block_idx].clone();
334
335        // Lift any instructions before the test/JMP at the end of this block.
336        // The test is typically the second-to-last instruction (before JMP).
337        let test_pc = self.find_test_pc(&block);
338        if let Some(tp) = test_pc {
339            if tp > block.start {
340                self.lift_instructions(block.start, tp - 1, stmts);
341            }
342        }
343
344        let cond = self.extract_condition(block_idx).unwrap_or(Expr::Bool(true));
345
346        // Find the two branches
347        let succs = block.successors.clone();
348        if succs.len() != 2 {
349            // Not a proper conditional; just lift as normal
350            self.lift_instructions(block.start, block.end, stmts);
351            return block_idx + 1;
352        }
353
354        // The edge order: ConditionalFalse is added first, ConditionalTrue second
355        // ConditionalFalse = the JMP target (condition NOT met)
356        // ConditionalTrue = fallthrough after JMP (condition met, test passed -> skip JMP)
357        let false_target = succs[0]; // Where JMP goes (condition false)
358        let true_target = succs[1];  // Fallthrough past JMP (condition true)
359
360        // Detect guard clause pattern: `if not cond then return end`
361        // In Lua bytecode this is: TEST/EQ -> JMP past return -> RETURN -> continuation
362        // The true_target block (condition true = skip JMP) is a small block ending in RETURN
363        // and false_target is the continuation.
364        // Only match if the return block is NOT a merge point (has only 1 predecessor).
365        if self.is_return_block(true_target) && false_target > true_target
366            && self.cfg.blocks[true_target].predecessors.len() <= 1
367        {
368            // Guard clause: the "true" path is just a return
369            let guard_body = self.lift_block_range(true_target, true_target + 1);
370            stmts.push(Stat::If {
371                cond,
372                then_block: guard_body,
373                elseif_clauses: Vec::new(),
374                else_block: None,
375            });
376            return false_target;
377        }
378
379        // Detect inverted guard: `if cond then <continue> else return end`
380        // Here false_target is a return block and true_target is the continuation
381        // Only match if the return block is NOT a merge point.
382        if self.is_return_block(false_target) && true_target < false_target
383            && self.cfg.blocks[false_target].predecessors.len() <= 1
384        {
385            let guard_body = self.lift_block_range(false_target, false_target + 1);
386            let inv_cond = negate_expr(cond);
387            stmts.push(Stat::If {
388                cond: inv_cond,
389                then_block: guard_body,
390                elseif_clauses: Vec::new(),
391                else_block: None,
392            });
393            return true_target;
394        }
395
396        // Find the merge point
397        let merge = self.find_merge_point(block_idx, true_target, false_target);
398
399        let then_end = merge.unwrap_or(false_target);
400        let then_block = self.lift_block_range(true_target, then_end);
401
402        let else_block = if let Some(merge) = merge {
403            if false_target < merge {
404                let eb = self.lift_block_range(false_target, merge);
405                if eb.is_empty() { None } else { Some(eb) }
406            } else {
407                None
408            }
409        } else {
410            None
411        };
412
413        stmts.push(Stat::If {
414            cond,
415            then_block,
416            elseif_clauses: Vec::new(),
417            else_block,
418        });
419
420        merge.unwrap_or(false_target.max(true_target) + 1)
421    }
422
423    /// Detect and lift OR/AND short-circuit conditional chains.
424    ///
425    /// OR pattern (`if a or b then T end`):
426    ///   Block A: ConditionalFalse → T, ConditionalTrue → Block B
427    ///   Block B: ConditionalTrue → T, ConditionalFalse → continuation
428    ///   (Intermediate blocks: ConditionalFalse → T, ConditionalTrue → next)
429    ///
430    /// AND pattern (`if a and b then body end`):
431    ///   Block A: ConditionalFalse → END, ConditionalTrue → Block B
432    ///   Block B: ConditionalFalse → END, ConditionalTrue → body
433    fn try_lift_or_and_chain(&mut self, start: usize, stmts: &mut Block) -> Option<usize> {
434        let block = &self.cfg.blocks[start];
435        if block.successors.len() != 2 { return None; }
436        if self.block_contains_testset(start) {
437            return None;
438        }
439
440        let _false0 = block.successors[0]; // ConditionalFalse (JMP target)
441        let true0 = block.successors[1];  // ConditionalTrue (fallthrough)
442
443        // true0 must be a conditional block (next test in chain)
444        if true0 >= self.cfg.num_blocks() { return None; }
445        if !self.is_conditional_block(&self.cfg.blocks[true0]) { return None; }
446        if self.block_contains_testset(true0) {
447            return None;
448        }
449
450        // Try OR chain detection
451        if let Some(result) = self.try_or_chain(start, stmts) {
452            return Some(result);
453        }
454
455        // Try AND chain detection
456        if let Some(result) = self.try_and_chain(start, stmts) {
457            return Some(result);
458        }
459
460        None
461    }
462
463    /// Detect and lift an OR chain: `if A or B or ... then T`.
464    ///
465    /// Pattern: intermediate blocks have ConditionalFalse → T (common body),
466    /// last block has ConditionalTrue → T.
467    fn try_or_chain(&mut self, start: usize, stmts: &mut Block) -> Option<usize> {
468        let block = &self.cfg.blocks[start];
469        let false0 = block.successors[0]; // ConditionalFalse = JMP target = T (body)
470        let true0 = block.successors[1];  // ConditionalTrue = next test
471
472        let body_target = false0;
473        let mut chain = vec![start]; // blocks in the chain
474        let mut current = true0;
475
476        // Follow the chain
477        loop {
478            if current >= self.cfg.num_blocks() { return None; }
479            if !self.is_conditional_block(&self.cfg.blocks[current]) { return None; }
480            if self.block_contains_testset(current) { return None; }
481
482            let cur_block = &self.cfg.blocks[current];
483            let cur_false = cur_block.successors[0]; // ConditionalFalse
484            let cur_true = cur_block.successors[1];  // ConditionalTrue
485
486            if cur_false == body_target {
487                // Intermediate block: false → T, true → next
488                chain.push(current);
489                current = cur_true;
490            } else if cur_true == body_target {
491                // Last block: true → T, false → continuation
492                chain.push(current);
493                let continuation = cur_false;
494
495                // Build the combined OR condition
496                return Some(self.emit_or_chain(&chain, body_target, continuation, stmts));
497            } else {
498                // Doesn't match the OR pattern
499                return None;
500            }
501        }
502    }
503
504    /// Emit an OR chain as a single `if` statement.
505    fn emit_or_chain(
506        &mut self,
507        chain: &[usize],
508        body_target: usize,
509        continuation: usize,
510        stmts: &mut Block,
511    ) -> usize {
512        let mut parts = Vec::new();
513
514        for (i, &block_idx) in chain.iter().enumerate() {
515            let block = self.cfg.blocks[block_idx].clone();
516            let test_pc = self.find_test_pc(&block);
517
518            // Lift pre-test instructions (updates register state)
519            if let Some(tp) = test_pc {
520                if tp > block.start {
521                    self.lift_instructions(block.start, tp - 1, stmts);
522                }
523            }
524
525            let cond = self.extract_condition(block_idx).unwrap_or(Expr::Bool(true));
526            self.visited_blocks.insert(block_idx);
527
528            let is_last = i == chain.len() - 1;
529            if is_last {
530                // Last block: ConditionalTrue → body, condition as-is
531                parts.push(cond);
532            } else {
533                // Intermediate block: ConditionalFalse → body, negate condition
534                parts.push(negate_expr(cond));
535            }
536        }
537
538        // Combine with OR
539        let combined = parts.into_iter().reduce(|a, b| {
540            Expr::BinOp(BinOp::Or, Box::new(a), Box::new(b))
541        }).unwrap_or(Expr::Bool(true));
542
543        // Lift the body (target T)
544        // Check if body is a guard clause (return)
545        if self.is_return_block(body_target) {
546            let then_block = self.lift_block_range(body_target, body_target + 1);
547            stmts.push(Stat::If {
548                cond: combined,
549                then_block,
550                elseif_clauses: Vec::new(),
551                else_block: None,
552            });
553            return continuation;
554        }
555
556        // Normal if: body with potential else
557        let merge = if self.block_flows_to(body_target, continuation) {
558            Some(continuation)
559        } else {
560            self.find_merge_point(
561                *chain.first().unwrap(),
562                body_target,
563                continuation,
564            )
565        };
566        let then_end = merge.unwrap_or(continuation);
567        let then_block = self.lift_block_range(body_target, then_end);
568
569        let else_block = if let Some(m) = merge {
570            if continuation < m {
571                let eb = self.lift_block_range(continuation, m);
572                if eb.is_empty() { None } else { Some(eb) }
573            } else {
574                None
575            }
576        } else {
577            None
578        };
579
580        stmts.push(Stat::If {
581            cond: combined,
582            then_block,
583            elseif_clauses: Vec::new(),
584            else_block,
585        });
586
587        merge.unwrap_or(continuation.max(body_target) + 1)
588    }
589
590    /// Detect and lift an AND chain: `if A and B and ... then body end`.
591    ///
592    /// Pattern: all blocks have ConditionalFalse → END (common else/end target),
593    /// ConditionalTrue chains to next test, last true → body.
594    fn try_and_chain(&mut self, start: usize, stmts: &mut Block) -> Option<usize> {
595        let block = &self.cfg.blocks[start];
596        let false0 = block.successors[0]; // ConditionalFalse = JMP target = END
597        let true0 = block.successors[1];  // ConditionalTrue = next test
598
599        let end_target = false0;
600        let mut chain = vec![start];
601        let mut current = true0;
602
603        // Follow the chain
604        loop {
605            if current >= self.cfg.num_blocks() {
606                // Reached the end of blocks; body is current
607                break;
608            }
609            if !self.is_conditional_block(&self.cfg.blocks[current]) {
610                // Non-conditional block = body
611                break;
612            }
613            if self.block_contains_testset(current) {
614                return None;
615            }
616
617            let cur_block = &self.cfg.blocks[current];
618            let cur_false = cur_block.successors[0];
619            let cur_true = cur_block.successors[1];
620
621            if cur_false == end_target {
622                // Another AND block: false → END, true → next
623                chain.push(current);
624                current = cur_true;
625            } else {
626                // Doesn't match AND pattern
627                return None;
628            }
629        }
630
631        // Need at least 2 blocks for a chain
632        if chain.len() < 2 { return None; }
633
634        let body_target = current;
635
636        // Build and emit the AND chain
637        let mut parts = Vec::new();
638
639        for &block_idx in &chain {
640            let block = self.cfg.blocks[block_idx].clone();
641            let test_pc = self.find_test_pc(&block);
642
643            if let Some(tp) = test_pc {
644                if tp > block.start {
645                    self.lift_instructions(block.start, tp - 1, stmts);
646                }
647            }
648
649            let cond = self.extract_condition(block_idx).unwrap_or(Expr::Bool(true));
650            self.visited_blocks.insert(block_idx);
651            parts.push(cond);
652        }
653
654        // Combine with AND
655        let combined = parts.into_iter().reduce(|a, b| {
656            Expr::BinOp(BinOp::And, Box::new(a), Box::new(b))
657        }).unwrap_or(Expr::Bool(true));
658
659        // Lift body and else
660        let merge = if self.block_flows_to(body_target, end_target) {
661            Some(end_target)
662        } else {
663            self.find_merge_point(
664                *chain.first().unwrap(),
665                body_target,
666                end_target,
667            )
668        };
669        let then_end = merge.unwrap_or(end_target);
670        let then_block = self.lift_block_range(body_target, then_end);
671
672        let else_block = if let Some(m) = merge {
673            if end_target < m {
674                let eb = self.lift_block_range(end_target, m);
675                if eb.is_empty() { None } else { Some(eb) }
676            } else {
677                None
678            }
679        } else {
680            None
681        };
682
683        stmts.push(Stat::If {
684            cond: combined,
685            then_block,
686            elseif_clauses: Vec::new(),
687            else_block,
688        });
689
690        Some(merge.unwrap_or(end_target.max(body_target) + 1))
691    }
692
693    /// Lift a range of instructions into statements.
694    fn lift_instructions(&mut self, start_pc: usize, end_pc: usize, stmts: &mut Block) {
695        let mut pc = start_pc;
696        while pc <= end_pc {
697            let inst = self.cfg.instructions[pc];
698            match inst.op {
699                OpCode::Move => {
700                    let src = self.reg_expr(inst.b());
701                    self.assign_reg_expr(pc, inst.a, src, stmts);
702                }
703                OpCode::LoadK => {
704                    let expr = self.const_expr(inst.bx());
705                    self.assign_reg_expr(pc, inst.a, expr, stmts);
706                }
707                OpCode::LoadBool => {
708                    self.assign_reg_expr(pc, inst.a, Expr::Bool(inst.b() != 0), stmts);
709                    if inst.c() != 0 {
710                        pc += 1; // skip next instruction
711                    }
712                }
713                OpCode::LoadNil => {
714                    for r in inst.a..=inst.b() {
715                        self.assign_reg_expr(pc, r, Expr::Nil, stmts);
716                    }
717                }
718                OpCode::GetUpval => {
719                    let expr = self.upvalue_expr(inst.b());
720                    self.assign_reg_expr(pc, inst.a, expr, stmts);
721                }
722                OpCode::GetGlobal => {
723                    let name = self.const_string(inst.bx());
724                    self.assign_reg_expr(pc, inst.a, Expr::Global(name), stmts);
725                }
726                OpCode::GetTable => {
727                    let table = self.reg_expr(inst.b());
728                    let key = self.rk_expr(inst.c());
729                    let expr = make_index(table, key);
730                    self.assign_reg_expr(pc, inst.a, expr, stmts);
731                }
732                OpCode::SetGlobal => {
733                    self.flush_pending_table(inst.a);
734                    let name = self.const_string(inst.bx());
735                    let val = self.reg_expr(inst.a);
736                    stmts.push(Stat::Assign {
737                        targets: vec![Expr::Global(name)],
738                        values: vec![val],
739                    });
740                    self.capture_aliases
741                        .insert(inst.a, Expr::Global(self.const_string(inst.bx())));
742                }
743                OpCode::SetUpval => {
744                    let val = self.reg_expr(inst.a);
745                    let uv = self.upvalue_expr(inst.b());
746                    stmts.push(Stat::Assign {
747                        targets: vec![uv],
748                        values: vec![val],
749                    });
750                }
751                OpCode::SetTable => {
752                    // Check if this is part of table construction
753                    let is_pending = self.pending_tables.contains_key(&inst.a);
754                    if is_pending {
755                        let key = self.rk_expr(inst.b());
756                        let val = self.rk_expr(inst.c());
757                        let fields = self.pending_tables.get_mut(&inst.a).unwrap();
758                        // If key is a string identifier, use NameField
759                        if let Expr::StringLit(ref s) = key {
760                            if let Ok(name) = std::str::from_utf8(s) {
761                                if is_identifier(name) {
762                                    fields.push(TableField::NameField(
763                                        name.to_string(),
764                                        val,
765                                    ));
766                                    pc += 1;
767                                    continue;
768                                }
769                            }
770                        }
771                        fields.push(TableField::IndexField(key, val));
772                        pc += 1;
773                        continue;
774                    }
775                    // Flush any pending table first
776                    self.flush_pending_table(inst.a);
777                    let table = self.reg_expr(inst.a);
778                    let key = self.rk_expr(inst.b());
779                    let val = self.rk_expr(inst.c());
780                    let target = make_index(table, key);
781                    stmts.push(Stat::Assign {
782                        targets: vec![target.clone()],
783                        values: vec![val],
784                    });
785                    if !is_k(inst.c()) {
786                        self.capture_aliases.insert(inst.c(), target);
787                    }
788                }
789                OpCode::NewTable => {
790                    self.assign_reg_expr(pc, inst.a, Expr::Table(Vec::new()), stmts);
791                    self.pending_tables.insert(inst.a, Vec::new());
792                }
793                OpCode::Self_ => {
794                    let table = self.reg_expr(inst.b());
795                    let method = self.rk_expr(inst.c());
796                    let method_ref = make_index(table.clone(), method);
797                    self.assign_reg_expr(pc, inst.a + 1, table, stmts);
798                    self.assign_reg_expr(pc, inst.a, method_ref, stmts);
799                }
800                OpCode::Add => {
801                    let expr = Expr::BinOp(
802                        BinOp::Add,
803                        Box::new(self.rk_expr(inst.b())),
804                        Box::new(self.rk_expr(inst.c())),
805                    );
806                    self.assign_reg_expr(pc, inst.a, expr, stmts);
807                }
808                OpCode::Sub => {
809                    let expr = Expr::BinOp(
810                        BinOp::Sub,
811                        Box::new(self.rk_expr(inst.b())),
812                        Box::new(self.rk_expr(inst.c())),
813                    );
814                    self.assign_reg_expr(pc, inst.a, expr, stmts);
815                }
816                OpCode::Mul => {
817                    let expr = Expr::BinOp(
818                        BinOp::Mul,
819                        Box::new(self.rk_expr(inst.b())),
820                        Box::new(self.rk_expr(inst.c())),
821                    );
822                    self.assign_reg_expr(pc, inst.a, expr, stmts);
823                }
824                OpCode::Div => {
825                    let expr = Expr::BinOp(
826                        BinOp::Div,
827                        Box::new(self.rk_expr(inst.b())),
828                        Box::new(self.rk_expr(inst.c())),
829                    );
830                    self.assign_reg_expr(pc, inst.a, expr, stmts);
831                }
832                OpCode::Mod => {
833                    let expr = Expr::BinOp(
834                        BinOp::Mod,
835                        Box::new(self.rk_expr(inst.b())),
836                        Box::new(self.rk_expr(inst.c())),
837                    );
838                    self.assign_reg_expr(pc, inst.a, expr, stmts);
839                }
840                OpCode::Pow => {
841                    let expr = Expr::BinOp(
842                        BinOp::Pow,
843                        Box::new(self.rk_expr(inst.b())),
844                        Box::new(self.rk_expr(inst.c())),
845                    );
846                    self.assign_reg_expr(pc, inst.a, expr, stmts);
847                }
848                OpCode::Unm => {
849                    let expr = Expr::UnOp(UnOp::Neg, Box::new(self.reg_expr(inst.b())));
850                    self.assign_reg_expr(pc, inst.a, expr, stmts);
851                }
852                OpCode::Not => {
853                    let expr = Expr::UnOp(UnOp::Not, Box::new(self.reg_expr(inst.b())));
854                    self.assign_reg_expr(pc, inst.a, expr, stmts);
855                }
856                OpCode::Len => {
857                    let expr = Expr::UnOp(UnOp::Len, Box::new(self.reg_expr(inst.b())));
858                    self.assign_reg_expr(pc, inst.a, expr, stmts);
859                }
860                OpCode::Concat => {
861                    let b = inst.b();
862                    let c = inst.c();
863                    let mut expr = self.reg_expr(b);
864                    for r in (b + 1)..=c {
865                        expr = Expr::BinOp(
866                            BinOp::Concat,
867                            Box::new(expr),
868                            Box::new(self.reg_expr(r)),
869                        );
870                    }
871                    self.assign_reg_expr(pc, inst.a, expr, stmts);
872                }
873                OpCode::Jmp => {
874                    // Jumps are handled by the CFG; skip here
875                }
876                OpCode::Eq | OpCode::Lt | OpCode::Le => {
877                    // Comparison tests: handled at block level for conditionals
878                    pc += 1; // skip following JMP
879                }
880                OpCode::Test => {
881                    // Handled at conditional level
882                    pc += 1;
883                }
884                OpCode::TestSet => {
885                    // R(A) := R(B) if R(B) <=> C, else skip
886                    pc += 1;
887                }
888                OpCode::Call => {
889                    let func = self.reg_expr(inst.a);
890                    let num_args = if inst.b() == 0 {
891                        0 // variable args - simplified
892                    } else {
893                        inst.b() - 1
894                    };
895                    let args: Vec<Expr> = (0..num_args)
896                        .map(|i| self.reg_expr(inst.a + 1 + i))
897                        .collect();
898                    let call = CallExpr { func, args };
899
900                    if inst.c() == 1 {
901                        // C==1: no return values -> statement call
902                        stmts.push(Stat::Call(call));
903                    } else if inst.c() == 0 {
904                        // C==0: variable return values (used by next CALL/RETURN/SETLIST)
905                        self.set_reg(inst.a, Expr::FuncCall(Box::new(call)));
906                    } else {
907                        // C>=2: fixed return values (C-1 results)
908                        let num_results = inst.c() - 1;
909                        if num_results == 1 {
910                            let call_expr = Expr::FuncCall(Box::new(call));
911                            // Use liveness: only create a local if the result
912                            // is used later
913                            let live = is_reg_live_after(
914                                &self.cfg, &self.liveness, pc, inst.a,
915                            );
916                            if live {
917                                self.set_reg_local(inst.a, call_expr, stmts);
918                            } else {
919                                // Result not used -> emit as statement
920                                if let Expr::FuncCall(c) = call_expr {
921                                    stmts.push(Stat::Call(*c));
922                                }
923                            }
924                        } else {
925                            // Multiple fixed return values -> local multi-assign
926                            let names: Vec<String> = (0..num_results)
927                                .map(|i| {
928                                    let r = inst.a + i;
929                                    let name = self.make_local_name(r);
930                                    self.local_names.insert(r, name.clone());
931                                    self.declared_locals.insert(r);
932                                    name
933                                })
934                                .collect();
935                            stmts.push(Stat::LocalAssign {
936                                names: names.clone(),
937                                exprs: vec![Expr::FuncCall(Box::new(call))],
938                            });
939                            for (i, name) in names.iter().enumerate() {
940                                let r = (inst.a + i as u32) as usize;
941                                if r < self.regs.len() {
942                                    self.regs[r] = Some(Expr::Name(name.clone()));
943                                }
944                            }
945                        }
946                    }
947                }
948                OpCode::TailCall => {
949                    let func = self.reg_expr(inst.a);
950                    let num_args = if inst.b() == 0 {
951                        0
952                    } else {
953                        inst.b() - 1
954                    };
955                    let args: Vec<Expr> = (0..num_args)
956                        .map(|i| self.reg_expr(inst.a + 1 + i))
957                        .collect();
958                    let call = CallExpr { func, args };
959                    stmts.push(Stat::Return(vec![Expr::FuncCall(Box::new(call))]));
960                }
961                OpCode::Return => {
962                    let num_ret = if inst.b() == 0 {
963                        0
964                    } else {
965                        inst.b() - 1
966                    };
967                    if num_ret == 0 && inst.a == 0 {
968                        // `return` with no values at end of function - may be implicit
969                        if pc != end_pc || end_pc != self.cfg.instructions.len() - 1 {
970                            stmts.push(Stat::Return(Vec::new()));
971                        }
972                    } else {
973                        let vals: Vec<Expr> = (0..num_ret)
974                            .map(|i| self.reg_expr(inst.a + i))
975                            .collect();
976                        stmts.push(Stat::Return(vals));
977                    }
978                }
979                OpCode::ForLoop | OpCode::ForPrep => {
980                    // Handled by loop lifting
981                }
982                OpCode::TForLoop => {
983                    // Handled by loop lifting
984                }
985                OpCode::SetList => {
986                    let table_reg = inst.a;
987                    let num = if inst.b() == 0 { 0 } else { inst.b() };
988                    // Items are in registers A+1 .. A+num
989                    // Flush any pending subtables in those registers first
990                    for i in 1..=num {
991                        self.flush_pending_table(table_reg + i);
992                    }
993                    // Collect values
994                    let values: Vec<Expr> = (1..=num)
995                        .map(|i| self.reg_expr(table_reg + i))
996                        .collect();
997                    if let Some(fields) = self.pending_tables.get_mut(&table_reg) {
998                        for val in values {
999                            fields.push(TableField::Value(val));
1000                        }
1001                    }
1002                }
1003                OpCode::Close => {
1004                    // Internal VM operation, no visible effect in source
1005                }
1006                OpCode::Closure => {
1007                    let closure_pc = pc;
1008                    let proto_idx = inst.bx() as usize;
1009                    let sub_func = if proto_idx < self.chunk.prototypes.len() {
1010                        let sub_chunk = &self.chunk.prototypes[proto_idx];
1011                        let resolved = self.resolve_closure_upvalues(pc, sub_chunk, stmts);
1012                        Lifter::decompile_with_upvalues(sub_chunk, resolved)
1013                    } else {
1014                        Function {
1015                            params: Vec::new(),
1016                            is_vararg: false,
1017                            body: Vec::new(),
1018                        }
1019                    };
1020                    if proto_idx < self.chunk.prototypes.len() {
1021                        pc += self.chunk.prototypes[proto_idx].num_upvalues as usize;
1022                    }
1023                    self.assign_reg_expr(
1024                        closure_pc,
1025                        inst.a,
1026                        Expr::FunctionDef(Box::new(sub_func)),
1027                        stmts,
1028                    );
1029                }
1030                OpCode::VarArg => {
1031                    self.assign_reg_expr(pc, inst.a, Expr::VarArg, stmts);
1032                }
1033            }
1034            pc += 1;
1035        }
1036    }
1037
1038    // -- Helper methods --
1039
1040    fn set_reg(&mut self, reg: u32, expr: Expr) {
1041        let r = reg as usize;
1042        if r < self.regs.len() {
1043            self.regs[r] = Some(expr);
1044        }
1045        // Clear local name mapping so reg_expr returns the actual expression
1046        // instead of the old local name. The local is now dead (overwritten).
1047        self.local_names.remove(&reg);
1048        // Clear any pending table — the register is being overwritten with
1049        // a completely new value, so any table construction for this register
1050        // is finished (or abandoned).
1051        self.pending_tables.remove(&reg);
1052        self.capture_aliases.remove(&reg);
1053    }
1054
1055    /// Set a register and potentially emit a `local` declaration if this is a
1056    /// new local variable (register above params, first assignment).
1057    fn set_reg_local(&mut self, reg: u32, expr: Expr, stmts: &mut Block) {
1058        if reg >= self.num_params && !self.declared_locals.contains(&reg) {
1059            // First assignment to this register -> declare local
1060            self.declared_locals.insert(reg);
1061            let name = self.make_local_name_for_expr(reg, &expr);
1062            self.local_names.insert(reg, name.clone());
1063            let r = reg as usize;
1064            if r < self.regs.len() {
1065                self.regs[r] = Some(Expr::Name(name.clone()));
1066            }
1067            stmts.push(Stat::LocalAssign {
1068                names: vec![name],
1069                exprs: vec![expr],
1070            });
1071        } else if let Some(name) = self.local_names.get(&reg).cloned() {
1072            // Already declared -> emit assignment
1073            let r = reg as usize;
1074            if r < self.regs.len() {
1075                self.regs[r] = Some(Expr::Name(name.clone()));
1076            }
1077            stmts.push(Stat::Assign {
1078                targets: vec![Expr::Name(name)],
1079                values: vec![expr],
1080            });
1081        } else {
1082            // Parameter register -> just update
1083            let r = reg as usize;
1084            if r < self.regs.len() {
1085                self.regs[r] = Some(expr);
1086            }
1087        }
1088    }
1089
1090    fn assign_reg_expr(&mut self, pc: usize, reg: u32, expr: Expr, stmts: &mut Block) {
1091        let block_id = self.cfg.block_of(pc);
1092        let live_out_of_block = (reg as usize) < self.liveness.max_reg
1093            && self.liveness.live_out[block_id][reg as usize];
1094
1095        if self.accumulator_regs.contains(&reg)
1096            && reg >= self.num_params
1097            && live_out_of_block
1098        {
1099            self.set_reg_local(reg, expr, stmts);
1100        } else {
1101            self.set_reg(reg, expr);
1102        }
1103    }
1104
1105    fn resolve_closure_upvalues(
1106        &mut self,
1107        pc: usize,
1108        sub_chunk: &LuaChunk,
1109        stmts: &mut Block,
1110    ) -> Vec<Option<Expr>> {
1111        let num_upvalues = sub_chunk.num_upvalues as usize;
1112        let mut resolved = Vec::with_capacity(num_upvalues);
1113
1114        for offset in 0..num_upvalues {
1115            let capture_pc = pc + 1 + offset;
1116            if capture_pc >= self.cfg.instructions.len() {
1117                resolved.push(None);
1118                continue;
1119            }
1120
1121            let capture = self.cfg.instructions[capture_pc];
1122            let expr = match capture.op {
1123                OpCode::Move => Some(self.capture_stack_upvalue(capture.b(), capture_pc, stmts)),
1124                OpCode::GetUpval => Some(self.upvalue_expr(capture.b())),
1125                _ => None,
1126            };
1127            resolved.push(expr);
1128        }
1129
1130        resolved
1131    }
1132
1133    fn capture_stack_upvalue(&mut self, reg: u32, pc: usize, stmts: &mut Block) -> Expr {
1134        if let Some(name) = self.local_names.get(&reg).cloned() {
1135            return Expr::Name(name);
1136        }
1137
1138        if reg < self.num_params {
1139            let name = self.local_name(reg, pc);
1140            self.local_names.insert(reg, name.clone());
1141            let r = reg as usize;
1142            if r < self.regs.len() {
1143                self.regs[r] = Some(Expr::Name(name.clone()));
1144            }
1145            return Expr::Name(name);
1146        }
1147
1148        if let Some(alias) = self.capture_aliases.get(&reg).cloned() {
1149            return alias;
1150        }
1151
1152        match self.reg_expr(reg) {
1153            Expr::Name(name) => Expr::Name(name),
1154            Expr::Global(name) => Expr::Global(name),
1155            Expr::Upvalue(idx) => self.upvalue_expr(idx),
1156            Expr::Field(table, field) => Expr::Field(table, field),
1157            expr => {
1158                let name = self.local_name(reg, pc);
1159                self.local_names.insert(reg, name.clone());
1160                self.declared_locals.insert(reg);
1161                let r = reg as usize;
1162                if r < self.regs.len() {
1163                    self.regs[r] = Some(Expr::Name(name.clone()));
1164                }
1165                self.pending_tables.remove(&reg);
1166                self.capture_aliases.remove(&reg);
1167                stmts.push(Stat::LocalAssign {
1168                    names: vec![name.clone()],
1169                    exprs: vec![expr],
1170                });
1171                Expr::Name(name)
1172            }
1173        }
1174    }
1175
1176    fn reg_expr(&self, reg: u32) -> Expr {
1177        // If this register has a local name, return the name reference
1178        if let Some(name) = self.local_names.get(&reg) {
1179            return Expr::Name(name.clone());
1180        }
1181        // If this register has a pending table with accumulated fields, return them
1182        if let Some(fields) = self.pending_tables.get(&reg) {
1183            if !fields.is_empty() {
1184                return Expr::Table(fields.clone());
1185            }
1186        }
1187        let r = reg as usize;
1188        if r < self.regs.len() {
1189            self.regs[r].clone().unwrap_or(Expr::Register(reg))
1190        } else {
1191            Expr::Register(reg)
1192        }
1193    }
1194
1195    fn rk_expr(&self, rk: u32) -> Expr {
1196        if is_k(rk) {
1197            self.const_expr(index_k(rk))
1198        } else {
1199            self.reg_expr(rk)
1200        }
1201    }
1202
1203    fn const_expr(&self, idx: u32) -> Expr {
1204        let i = idx as usize;
1205        if i >= self.chunk.constants.len() {
1206            return Expr::Nil;
1207        }
1208        match &self.chunk.constants[i] {
1209            LuaConstant::Null => Expr::Nil,
1210            LuaConstant::Bool(b) => Expr::Bool(*b),
1211            LuaConstant::Number(n) => match n {
1212                LuaNumber::Integer(v) => Expr::Number(NumLit::Int(*v)),
1213                LuaNumber::Float(v) => Expr::Number(NumLit::Float(*v)),
1214            },
1215            LuaConstant::String(s) => Expr::StringLit(s.as_ref().to_vec()),
1216            _ => Expr::Nil,
1217        }
1218    }
1219
1220    fn const_string(&self, idx: u32) -> String {
1221        let i = idx as usize;
1222        if i < self.chunk.constants.len() {
1223            if let LuaConstant::String(s) = &self.chunk.constants[i] {
1224                return String::from_utf8_lossy(s.as_ref()).into_owned();
1225            }
1226        }
1227        format!("_K{}", idx)
1228    }
1229
1230    fn upvalue_expr(&self, idx: u32) -> Expr {
1231        let i = idx as usize;
1232        if i < self.resolved_upvalues.len() {
1233            if let Some(expr) = &self.resolved_upvalues[i] {
1234                return expr.clone();
1235            }
1236        }
1237        if i < self.chunk.upvalue_names.len() {
1238            let name = String::from_utf8_lossy(&self.chunk.upvalue_names[i]).into_owned();
1239            if !name.is_empty() {
1240                return Expr::Name(name);
1241            }
1242        }
1243        Expr::Upvalue(idx)
1244    }
1245
1246    fn local_name(&self, reg: u32, pc: usize) -> String {
1247        // Try debug info: locals are ordered by register assignment
1248        // In standard Lua 5.1 bytecode, the i-th local in the array corresponds
1249        // to register i (for the scope range start_pc..end_pc)
1250        for (i, local) in self.chunk.locals.iter().enumerate() {
1251            if i == reg as usize
1252                && local.start_pc as usize <= pc + 1
1253                && pc < local.end_pc as usize
1254            {
1255                if !local.name.is_empty() && !local.name.starts_with('(') {
1256                    return local.name.clone();
1257                }
1258            }
1259        }
1260        // Also scan all locals for any that match this register
1261        for local in &self.chunk.locals {
1262            if local.start_pc as usize <= pc + 1 && pc < local.end_pc as usize {
1263                // Use locals array index as register mapping
1264            }
1265        }
1266        // Fall back to matching by position in locals array for params
1267        let r = reg as usize;
1268        if r < self.chunk.locals.len() {
1269            let name = &self.chunk.locals[r].name;
1270            if !name.is_empty() && !name.starts_with('(') {
1271                return name.clone();
1272            }
1273        }
1274        self.make_local_name(reg)
1275    }
1276
1277    fn make_local_name(&self, reg: u32) -> String {
1278        let current_expr = self.regs.get(reg as usize).and_then(|e| e.as_ref());
1279        self.make_local_name_from_known_expr(reg, current_expr)
1280    }
1281
1282    fn make_local_name_for_expr(&self, reg: u32, expr: &Expr) -> String {
1283        self.make_local_name_from_known_expr(reg, Some(expr))
1284    }
1285
1286    fn make_local_name_from_known_expr(&self, reg: u32, current_expr: Option<&Expr>) -> String {
1287        if reg < self.num_params {
1288            if self.has_debug_info {
1289                return self.local_name(reg, 0);
1290            }
1291            return format!("a{}", reg);
1292        }
1293        if let Some(name) = self.infer_accumulator_name(reg, current_expr) {
1294            return self.uniquify_local_name(name);
1295        }
1296        if let Some(fields) = self.pending_tables.get(&reg) {
1297            if !fields.is_empty() {
1298                if let Some(name) = self.infer_table_local_name(fields) {
1299                    return self.uniquify_local_name(name);
1300                }
1301            }
1302        }
1303        // For stripped bytecode, try to infer a meaningful name from context.
1304        // Look at what was last stored in this register.
1305        if let Some(expr) = current_expr {
1306            match expr {
1307                Expr::Table(fields) => {
1308                    if let Some(name) = self.infer_table_local_name(fields) {
1309                        return self.uniquify_local_name(name);
1310                    }
1311                }
1312                // If it's a global function call, name after the function
1313                Expr::FuncCall(call) => {
1314                    if let Some(name) = self.infer_call_local_name(call) {
1315                        return self.uniquify_local_name(name);
1316                    }
1317                    if let Expr::Global(name) = &call.func {
1318                        let short = normalize_call_name(name);
1319                        if short.len() <= 20 {
1320                            return self.uniquify_local_name(short);
1321                        }
1322                    }
1323                    // Method call: use method name
1324                    if let Expr::Field(_, method) = &call.func {
1325                        return self.uniquify_local_name(normalize_call_name(method));
1326                    }
1327                }
1328                _ => {}
1329            }
1330        }
1331        format!("l_{}", reg)
1332    }
1333
1334    fn infer_accumulator_name(&self, reg: u32, expr: Option<&Expr>) -> Option<String> {
1335        if !self.accumulator_regs.contains(&reg) || !self.is_returned_reg(reg) {
1336            return None;
1337        }
1338
1339        match expr {
1340            Some(Expr::Number(_)) | None => Some("result".to_string()),
1341            _ => None,
1342        }
1343    }
1344
1345    fn infer_call_local_name(&self, call: &CallExpr) -> Option<String> {
1346        let method = match &call.func {
1347            Expr::Field(_, method) => method.as_str(),
1348            Expr::Global(name) => name.as_str(),
1349            _ => return None,
1350        };
1351
1352        let first_int_arg = match call.args.first() {
1353            Some(Expr::Number(NumLit::Int(value))) => Some(*value),
1354            Some(Expr::Number(NumLit::Float(value))) if value.fract() == 0.0 => Some(*value as i64),
1355            _ => None,
1356        };
1357
1358        match method {
1359            "IsHaveBuff" => Some(match first_int_arg {
1360                Some(id) => format!("has_buff_{}", id),
1361                None => "has_buff".to_string(),
1362            }),
1363            "GetBuff" | "GetBuffByOwner" => Some(match first_int_arg {
1364                Some(id) => format!("buff_{}", id),
1365                None => "buff".to_string(),
1366            }),
1367            "GetSkillLevel" => first_int_arg.map(|id| format!("skill_{}", id)),
1368            "GetEndTime" => Some("end_time".to_string()),
1369            "GetLogicFrameCount" => Some("logic_frame_count".to_string()),
1370            _ => None,
1371        }
1372    }
1373
1374    fn is_returned_reg(&self, reg: u32) -> bool {
1375        self.cfg.instructions.iter().any(|inst| {
1376            inst.op == OpCode::Return && inst.a == reg && inst.b() == 2
1377        })
1378    }
1379
1380    fn uniquify_local_name(&self, base: String) -> String {
1381        if !self.local_names.values().any(|name| name == &base) {
1382            return base;
1383        }
1384
1385        let mut suffix = 1;
1386        loop {
1387            let candidate = format!("{}_{}", base, suffix);
1388            if !self.local_names.values().any(|name| name == &candidate) {
1389                return candidate;
1390            }
1391            suffix += 1;
1392        }
1393    }
1394
1395    fn infer_table_local_name(&self, fields: &[TableField]) -> Option<String> {
1396        if fields.is_empty() {
1397            return None;
1398        }
1399
1400        if fields.iter().all(|field| matches!(field, TableField::IndexField(_, Expr::Bool(true)))) {
1401            if fields.iter().any(|field| {
1402                matches!(field, TableField::IndexField(key, _) if self.expr_mentions_field(key, "ENUM"))
1403            }) {
1404                return Some("enum_lookup".to_string());
1405            }
1406            return Some("lookup".to_string());
1407        }
1408
1409        if fields.iter().all(|field| matches!(field, TableField::NameField(_, Expr::Number(_)))) {
1410            let keys: Vec<&str> = fields
1411                .iter()
1412                .filter_map(|field| match field {
1413                    TableField::NameField(name, _) => Some(name.as_str()),
1414                    _ => None,
1415                })
1416                .collect();
1417            if keys.iter().any(|name| name.contains("NOT_")) {
1418                return Some("penalties".to_string());
1419            }
1420            if keys.iter().all(|name| is_upper_ident(name)) {
1421                return Some("modifiers".to_string());
1422            }
1423        }
1424
1425        None
1426    }
1427
1428    fn expr_mentions_field(&self, expr: &Expr, field_name: &str) -> bool {
1429        match expr {
1430            Expr::Field(table, field) => field == field_name || self.expr_mentions_field(table, field_name),
1431            Expr::Index(table, key) => {
1432                self.expr_mentions_field(table, field_name)
1433                    || self.expr_mentions_field(key, field_name)
1434            }
1435            Expr::MethodCall(call) | Expr::FuncCall(call) => {
1436                self.expr_mentions_field(&call.func, field_name)
1437                    || call.args.iter().any(|arg| self.expr_mentions_field(arg, field_name))
1438            }
1439            Expr::BinOp(_, lhs, rhs) => {
1440                self.expr_mentions_field(lhs, field_name)
1441                    || self.expr_mentions_field(rhs, field_name)
1442            }
1443            Expr::UnOp(_, inner) => self.expr_mentions_field(inner, field_name),
1444            Expr::Table(fields) => fields.iter().any(|field| match field {
1445                TableField::IndexField(key, value) => {
1446                    self.expr_mentions_field(key, field_name)
1447                        || self.expr_mentions_field(value, field_name)
1448                }
1449                TableField::NameField(_, value) | TableField::Value(value) => {
1450                    self.expr_mentions_field(value, field_name)
1451                }
1452            }),
1453            _ => false,
1454        }
1455    }
1456
1457    /// Flush the pending table construction for a specific register.
1458    fn flush_pending_table(&mut self, reg: u32) {
1459        if let Some(fields) = self.pending_tables.remove(&reg) {
1460            self.set_reg(reg, Expr::Table(fields));
1461        }
1462    }
1463
1464    /// Check if a block consists of only a RETURN (or RETURN with values).
1465    fn is_return_block(&self, block_idx: usize) -> bool {
1466        if block_idx >= self.cfg.num_blocks() {
1467            return false;
1468        }
1469        let block = &self.cfg.blocks[block_idx];
1470        let last = self.cfg.instructions[block.end];
1471        matches!(last.op, OpCode::Return | OpCode::TailCall)
1472            && block.successors.is_empty()
1473    }
1474
1475    /// Find the PC of the test instruction in a conditional block.
1476    fn find_test_pc(&self, block: &BasicBlock) -> Option<usize> {
1477        for pc in block.start..=block.end {
1478            let inst = self.cfg.instructions[pc];
1479            if matches!(
1480                inst.op,
1481                OpCode::Eq | OpCode::Lt | OpCode::Le | OpCode::Test | OpCode::TestSet
1482            ) {
1483                return Some(pc);
1484            }
1485        }
1486        None
1487    }
1488
1489    fn find_loop_at(&self, block_idx: usize) -> Option<&NaturalLoop> {
1490        if self.active_loop_headers.contains(&block_idx) {
1491            return None;
1492        }
1493        self.loops.iter().find(|l| l.header == block_idx)
1494    }
1495
1496    fn current_loop_exit(&self) -> Option<usize> {
1497        self.active_loop_exits.last().copied()
1498    }
1499
1500    fn find_forprep_block(&self, header: usize) -> Option<usize> {
1501        // Look for a predecessor of the header that ends with FORPREP
1502        for &pred in &self.cfg.blocks[header].predecessors {
1503            let pred_block = &self.cfg.blocks[pred];
1504            let last = self.cfg.instructions[pred_block.end];
1505            if last.op == OpCode::ForPrep {
1506                return Some(pred);
1507            }
1508        }
1509        // Also check if it's one block before header
1510        if header > 0 {
1511            let prev = &self.cfg.blocks[header - 1];
1512            let last = self.cfg.instructions[prev.end];
1513            if last.op == OpCode::ForPrep {
1514                return Some(header - 1);
1515            }
1516        }
1517        None
1518    }
1519
1520    fn max_loop_block(&self, lp: &NaturalLoop) -> usize {
1521        lp.body.iter().copied().max().unwrap_or(lp.header)
1522    }
1523
1524    fn is_conditional_block(&self, block: &BasicBlock) -> bool {
1525        block.successors.len() == 2
1526            && self.cfg.edges.iter().any(|e| {
1527                e.from == block.id
1528                    && matches!(
1529                        e.kind,
1530                        EdgeKind::ConditionalTrue | EdgeKind::ConditionalFalse
1531                    )
1532            })
1533    }
1534
1535    fn block_contains_testset(&self, block_idx: usize) -> bool {
1536        if block_idx >= self.cfg.num_blocks() {
1537            return false;
1538        }
1539        let block = &self.cfg.blocks[block_idx];
1540        (block.start..=block.end).any(|pc| self.cfg.instructions[pc].op == OpCode::TestSet)
1541    }
1542
1543    fn block_flows_to(&self, from_block: usize, target_block: usize) -> bool {
1544        if from_block >= self.cfg.num_blocks() || target_block >= self.cfg.num_blocks() {
1545            return false;
1546        }
1547        self.cfg.blocks[from_block].successors.iter().all(|&succ| succ == target_block)
1548    }
1549
1550    fn find_accumulator_regs(&self) -> HashSet<u32> {
1551        let mut regs = HashSet::new();
1552
1553        for inst in &self.cfg.instructions {
1554            match inst.op {
1555                OpCode::Add | OpCode::Sub | OpCode::Mul | OpCode::Div | OpCode::Mod | OpCode::Pow => {
1556                    let uses_target = (!is_k(inst.b()) && inst.a == inst.b())
1557                        || (!is_k(inst.c()) && inst.a == inst.c());
1558                    if uses_target {
1559                        regs.insert(inst.a);
1560                    }
1561                }
1562                OpCode::Concat => {
1563                    if inst.a >= inst.b() && inst.a <= inst.c() {
1564                        regs.insert(inst.a);
1565                    }
1566                }
1567                _ => {}
1568            }
1569        }
1570
1571        regs
1572    }
1573
1574    fn extract_condition(&self, block_idx: usize) -> Option<Expr> {
1575        let block = &self.cfg.blocks[block_idx];
1576        // Look for the test instruction (second-to-last, before JMP)
1577        for pc in block.start..=block.end {
1578            let inst = self.cfg.instructions[pc];
1579            match inst.op {
1580                OpCode::Eq => {
1581                    let lhs = self.rk_expr(inst.b());
1582                    let rhs = self.rk_expr(inst.c());
1583                    return Some(if inst.a == 0 {
1584                        Expr::BinOp(BinOp::Eq, Box::new(lhs), Box::new(rhs))
1585                    } else {
1586                        Expr::BinOp(BinOp::Ne, Box::new(lhs), Box::new(rhs))
1587                    });
1588                }
1589                OpCode::Lt => {
1590                    let lhs = self.rk_expr(inst.b());
1591                    let rhs = self.rk_expr(inst.c());
1592                    return Some(if inst.a == 0 {
1593                        Expr::BinOp(BinOp::Lt, Box::new(lhs), Box::new(rhs))
1594                    } else {
1595                        Expr::BinOp(BinOp::Ge, Box::new(lhs), Box::new(rhs))
1596                    });
1597                }
1598                OpCode::Le => {
1599                    let lhs = self.rk_expr(inst.b());
1600                    let rhs = self.rk_expr(inst.c());
1601                    return Some(if inst.a == 0 {
1602                        Expr::BinOp(BinOp::Le, Box::new(lhs), Box::new(rhs))
1603                    } else {
1604                        Expr::BinOp(BinOp::Gt, Box::new(lhs), Box::new(rhs))
1605                    });
1606                }
1607                OpCode::Test => {
1608                    let expr = self.reg_expr(inst.a);
1609                    return Some(if inst.c() == 0 {
1610                        expr
1611                    } else {
1612                        Expr::UnOp(UnOp::Not, Box::new(expr))
1613                    });
1614                }
1615                OpCode::TestSet => {
1616                    let expr = self.reg_expr(inst.b());
1617                    return Some(if inst.c() == 0 {
1618                        expr
1619                    } else {
1620                        Expr::UnOp(UnOp::Not, Box::new(expr))
1621                    });
1622                }
1623                _ => {}
1624            }
1625        }
1626        None
1627    }
1628
1629    fn find_merge_point(
1630        &self,
1631        cond_block: usize,
1632        true_block: usize,
1633        false_block: usize,
1634    ) -> Option<usize> {
1635        if false_block < self.cfg.num_blocks() {
1636            let false_preds = &self.cfg.blocks[false_block].predecessors;
1637            if false_preds.len() >= 3
1638                && false_preds.contains(&cond_block)
1639                && false_preds
1640                    .iter()
1641                    .any(|&pred| pred != cond_block && pred >= true_block && pred < false_block)
1642            {
1643                return Some(false_block);
1644            }
1645        }
1646
1647        // Simple heuristic: the merge point is the smallest block ID
1648        // that is a successor of both branches (or the false_block if
1649        // the true block falls through to it).
1650        let max_branch = true_block.max(false_block);
1651
1652        // Look for a block after both branches where control re-merges.
1653        for b in (max_branch + 1)..self.cfg.num_blocks() {
1654            let block = &self.cfg.blocks[b];
1655            if block.predecessors.len() >= 2 {
1656                return Some(b);
1657            }
1658            if !block
1659                .predecessors
1660                .iter()
1661                .all(|&p| p >= true_block && p <= max_branch)
1662                && block.predecessors.iter().any(|&p| p >= true_block)
1663            {
1664                return Some(b);
1665            }
1666        }
1667
1668        if false_block > true_block && false_block > cond_block {
1669            return Some(false_block);
1670        }
1671
1672        None
1673    }
1674}
1675
1676/// Build a field access or index expression.
1677fn make_index(table: Expr, key: Expr) -> Expr {
1678    // If key is a string that's a valid identifier, use Field syntax
1679    if let Expr::StringLit(ref s) = key {
1680        if let Ok(name) = std::str::from_utf8(s) {
1681            if is_identifier(name) {
1682                return Expr::Field(Box::new(table), name.to_string());
1683            }
1684        }
1685    }
1686    Expr::Index(Box::new(table), Box::new(key))
1687}
1688
1689/// Check if a string is a valid Lua identifier.
1690fn is_identifier(s: &str) -> bool {
1691    if s.is_empty() {
1692        return false;
1693    }
1694    let mut chars = s.chars();
1695    let first = chars.next().unwrap();
1696    if !first.is_ascii_alphabetic() && first != '_' {
1697        return false;
1698    }
1699    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
1700        && !is_lua_keyword(s)
1701}
1702
1703fn is_upper_ident(s: &str) -> bool {
1704    !s.is_empty()
1705        && s
1706            .chars()
1707            .all(|c| c == '_' || c.is_ascii_uppercase() || c.is_ascii_digit())
1708}
1709
1710fn normalize_call_name(name: &str) -> String {
1711    let snake = camel_to_snake(name);
1712    snake
1713        .strip_prefix("get_")
1714        .or_else(|| snake.strip_prefix("is_"))
1715        .map(ToOwned::to_owned)
1716        .unwrap_or(snake)
1717}
1718
1719fn camel_to_snake(name: &str) -> String {
1720    let mut out = String::new();
1721    for (idx, ch) in name.chars().enumerate() {
1722        if ch.is_ascii_uppercase() {
1723            if idx != 0 {
1724                out.push('_');
1725            }
1726            out.push(ch.to_ascii_lowercase());
1727        } else {
1728            out.push(ch);
1729        }
1730    }
1731    out
1732}
1733
1734fn is_lua_keyword(s: &str) -> bool {
1735    matches!(
1736        s,
1737        "and"
1738            | "break"
1739            | "do"
1740            | "else"
1741            | "elseif"
1742            | "end"
1743            | "false"
1744            | "for"
1745            | "function"
1746            | "if"
1747            | "in"
1748            | "local"
1749            | "nil"
1750            | "not"
1751            | "or"
1752            | "repeat"
1753            | "return"
1754            | "then"
1755            | "true"
1756            | "until"
1757            | "while"
1758    )
1759}
1760
1761/// Negate an expression (for inverting conditions).
1762fn negate_expr(expr: Expr) -> Expr {
1763    match expr {
1764        Expr::UnOp(UnOp::Not, inner) => *inner,
1765        Expr::BinOp(BinOp::Eq, a, b) => Expr::BinOp(BinOp::Ne, a, b),
1766        Expr::BinOp(BinOp::Ne, a, b) => Expr::BinOp(BinOp::Eq, a, b),
1767        Expr::BinOp(BinOp::Lt, a, b) => Expr::BinOp(BinOp::Ge, a, b),
1768        Expr::BinOp(BinOp::Ge, a, b) => Expr::BinOp(BinOp::Lt, a, b),
1769        Expr::BinOp(BinOp::Le, a, b) => Expr::BinOp(BinOp::Gt, a, b),
1770        Expr::BinOp(BinOp::Gt, a, b) => Expr::BinOp(BinOp::Le, a, b),
1771        Expr::Bool(b) => Expr::Bool(!b),
1772        other => Expr::UnOp(UnOp::Not, Box::new(other)),
1773    }
1774}