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