Skip to main content

luadec_rust/lua51/
emit.rs

1use std::fmt::Write;
2
3use crate::lua51::ast::*;
4
5/// Emit a Lua function as source code.
6pub fn emit_function(func: &Function) -> String {
7    let mut out = String::new();
8    let body = normalize_block(&func.body);
9    let body = fold_elseif_chain(&body);
10    emit_block(&body, &mut out, 0);
11    out
12}
13
14/// Emit a complete chunk (wrapping the main function body).
15pub fn emit_chunk(func: &Function) -> String {
16    let mut out = String::new();
17    let body = normalize_block(&func.body);
18    let body = fold_elseif_chain(&body);
19    emit_block(&body, &mut out, 0);
20    // Remove trailing blank lines
21    while out.ends_with("\n\n") {
22        out.pop();
23    }
24    if !out.ends_with('\n') {
25        out.push('\n');
26    }
27    out
28}
29
30fn normalize_block(block: &Block) -> Block {
31    let mut out = Vec::new();
32    for stat in block {
33        out.extend(normalize_stat(stat));
34    }
35    let out = cache_shared_negated_getter_calls(&out);
36    collapse_nested_table_assignments(&out)
37}
38
39fn collapse_nested_table_assignments(block: &Block) -> Block {
40    let mut out = Vec::new();
41    let mut index = 0;
42
43    while index < block.len() {
44        let Some((seed, base_expr, mut base_fields)) = extract_table_assignment_seed(&block[index]) else {
45            out.push(block[index].clone());
46            index += 1;
47            continue;
48        };
49
50        let mut next = index + 1;
51        while next < block.len() {
52            let Some((field_name, value)) = extract_nested_table_assignment(&block[next], &base_expr) else {
53                break;
54            };
55            base_fields.push(TableField::NameField(field_name, value));
56            next += 1;
57        }
58
59        if next == index + 1 {
60            out.push(block[index].clone());
61            index += 1;
62            continue;
63        }
64
65        out.push(rebuild_table_assignment_seed(seed, base_fields));
66        index = next;
67    }
68
69    out
70}
71
72enum TableAssignmentSeed {
73    Assign(Expr),
74    LocalAssign(String),
75}
76
77fn extract_table_assignment_seed(
78    stat: &Stat,
79) -> Option<(TableAssignmentSeed, Expr, Vec<TableField>)> {
80    match stat {
81        Stat::Assign { targets, values } if targets.len() == 1 && values.len() == 1 => {
82            let Expr::Table(fields) = &values[0] else {
83                return None;
84            };
85            let base = targets[0].clone();
86            let fields = fields.clone();
87            Some((TableAssignmentSeed::Assign(base.clone()), base, fields))
88        }
89        Stat::LocalAssign { names, exprs } if names.len() == 1 && exprs.len() == 1 => {
90            let Expr::Table(fields) = &exprs[0] else {
91                return None;
92            };
93            let local_name = names[0].clone();
94            let fields = fields.clone();
95            Some((
96                TableAssignmentSeed::LocalAssign(local_name.clone()),
97                Expr::Name(local_name),
98                fields,
99            ))
100        }
101        _ => None,
102    }
103}
104
105fn rebuild_table_assignment_seed(seed: TableAssignmentSeed, fields: Vec<TableField>) -> Stat {
106    match seed {
107        TableAssignmentSeed::Assign(target) => Stat::Assign {
108            targets: vec![target],
109            values: vec![Expr::Table(fields)],
110        },
111        TableAssignmentSeed::LocalAssign(name) => Stat::LocalAssign {
112            names: vec![name],
113            exprs: vec![Expr::Table(fields)],
114        },
115    }
116}
117
118fn extract_nested_table_assignment(stat: &Stat, base_expr: &Expr) -> Option<(String, Expr)> {
119    let Stat::Assign { targets, values } = stat else {
120        return None;
121    };
122    if targets.len() != 1 || values.len() != 1 {
123        return None;
124    }
125    let Expr::Table(_) = &values[0] else {
126        return None;
127    };
128
129    let field_name = match &targets[0] {
130        Expr::Field(table, field) if table.as_ref() == base_expr => field.clone(),
131        Expr::Index(table, key) if table.as_ref() == base_expr => match key.as_ref() {
132            Expr::StringLit(bytes) => {
133                let field = std::str::from_utf8(bytes).ok()?;
134                if !is_identifier(field) {
135                    return None;
136                }
137                field.to_string()
138            }
139            _ => return None,
140        },
141        _ => return None,
142    };
143
144    Some((field_name, values[0].clone()))
145}
146
147fn is_identifier(name: &str) -> bool {
148    if name.is_empty() {
149        return false;
150    }
151    let mut chars = name.chars();
152    let first = chars.next().unwrap();
153    if !first.is_ascii_alphabetic() && first != '_' {
154        return false;
155    }
156    chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_') && !is_lua_keyword(name)
157}
158
159fn is_lua_keyword(name: &str) -> bool {
160    matches!(
161        name,
162        "and"
163            | "break"
164            | "do"
165            | "else"
166            | "elseif"
167            | "end"
168            | "false"
169            | "for"
170            | "function"
171            | "if"
172            | "in"
173            | "local"
174            | "nil"
175            | "not"
176            | "or"
177            | "repeat"
178            | "return"
179            | "then"
180            | "true"
181            | "until"
182            | "while"
183    )
184}
185
186fn normalize_stat(stat: &Stat) -> Block {
187    match stat {
188        Stat::LocalAssign { names, exprs } => vec![Stat::LocalAssign {
189            names: names.clone(),
190            exprs: exprs.iter().cloned().map(simplify_expr).collect(),
191        }],
192        Stat::Assign { targets, values } => vec![Stat::Assign {
193            targets: targets.iter().cloned().map(simplify_expr).collect(),
194            values: values.iter().cloned().map(simplify_expr).collect(),
195        }],
196        Stat::Call(call) => vec![Stat::Call(simplify_call(call.clone()))],
197        Stat::DoBlock(body) => vec![Stat::DoBlock(normalize_block(body))],
198        Stat::While { cond, body } => vec![Stat::While {
199            cond: simplify_expr(cond.clone()),
200            body: normalize_block(body),
201        }],
202        Stat::Repeat { body, cond } => vec![Stat::Repeat {
203            body: normalize_block(body),
204            cond: simplify_expr(cond.clone()),
205        }],
206        Stat::If {
207            cond,
208            then_block,
209            elseif_clauses,
210            else_block,
211        } => normalize_if(cond, then_block, elseif_clauses, else_block),
212        Stat::NumericFor {
213            name,
214            start,
215            limit,
216            step,
217            body,
218        } => vec![Stat::NumericFor {
219            name: name.clone(),
220            start: simplify_expr(start.clone()),
221            limit: simplify_expr(limit.clone()),
222            step: step.clone().map(simplify_expr),
223            body: normalize_block(body),
224        }],
225        Stat::GenericFor { names, iterators, body } => vec![Stat::GenericFor {
226            names: names.clone(),
227            iterators: iterators.iter().cloned().map(simplify_expr).collect(),
228            body: normalize_block(body),
229        }],
230        Stat::Return(exprs) => vec![Stat::Return(
231            exprs.iter().cloned().map(simplify_expr).collect(),
232        )],
233        Stat::Break => vec![Stat::Break],
234        Stat::Comment(text) => vec![Stat::Comment(text.clone())],
235    }
236}
237
238fn normalize_if(
239    cond: &Expr,
240    then_block: &Block,
241    elseif_clauses: &[(Expr, Block)],
242    else_block: &Option<Block>,
243) -> Block {
244    let cond = simplify_expr(cond.clone());
245    let then_block = normalize_block(then_block);
246    let mut elseif_clauses: Vec<(Expr, Block)> = elseif_clauses
247        .iter()
248        .map(|(expr, block)| (simplify_expr(expr.clone()), normalize_block(block)))
249        .collect();
250    let mut else_block = else_block.as_ref().map(normalize_block);
251
252    loop {
253        let Some(block) = else_block.as_ref() else {
254            break;
255        };
256        if block.len() != 1 {
257            break;
258        }
259        let Stat::If {
260            cond: inner_cond,
261            then_block: inner_then,
262            elseif_clauses: inner_elseifs,
263            else_block: inner_else,
264        } = &block[0] else {
265            break;
266        };
267
268        elseif_clauses.push((simplify_expr(inner_cond.clone()), inner_then.clone()));
269        elseif_clauses.extend(inner_elseifs.iter().cloned());
270        else_block = inner_else.clone();
271    }
272
273    if then_block.is_empty() && elseif_clauses.is_empty() {
274        if let Some(else_block) = else_block.as_ref() {
275            if !else_block.is_empty() {
276                return vec![Stat::If {
277                    cond: negate_condition(cond),
278                    then_block: else_block.clone(),
279                    elseif_clauses: Vec::new(),
280                    else_block: None,
281                }];
282            }
283        }
284    }
285
286    if elseif_clauses.is_empty() && else_block.is_none() && then_block.len() == 1 {
287        if let Stat::If {
288            cond: inner_cond,
289            then_block: inner_then,
290            elseif_clauses: inner_elseifs,
291            else_block: inner_else,
292        } = &then_block[0]
293        {
294            let merged_terms = count_and_terms(&cond) + count_and_terms(inner_cond);
295            if inner_elseifs.is_empty() && inner_else.is_none() && !inner_then.is_empty() && merged_terms <= 3 {
296                return vec![Stat::If {
297                    cond: simplify_expr(Expr::BinOp(
298                        BinOp::And,
299                        Box::new(cond),
300                        Box::new(inner_cond.clone()),
301                    )),
302                    then_block: inner_then.clone(),
303                    elseif_clauses: Vec::new(),
304                    else_block: None,
305                }];
306            }
307        }
308    }
309
310    if elseif_clauses.is_empty() && else_block.is_none() && then_block.len() > 1 {
311        if let Stat::If {
312            cond: inner_cond,
313            then_block: inner_then,
314            elseif_clauses: inner_elseifs,
315            else_block: inner_else,
316        } = &then_block[0]
317        {
318            let merged_terms = count_and_terms(&cond) + count_and_terms(inner_cond);
319            if inner_elseifs.is_empty()
320                && inner_else.is_none()
321                && block_ends_with_terminator(inner_then)
322                && merged_terms <= 3
323            {
324                let mut out = vec![Stat::If {
325                    cond: simplify_expr(Expr::BinOp(
326                        BinOp::And,
327                        Box::new(cond),
328                        Box::new(inner_cond.clone()),
329                    )),
330                    then_block: inner_then.clone(),
331                    elseif_clauses: Vec::new(),
332                    else_block: None,
333                }];
334                out.extend(then_block[1..].iter().cloned());
335                return out;
336            }
337        }
338    }
339
340    if else_block.is_none()
341        && !then_block.is_empty()
342        && !elseif_clauses.is_empty()
343        && elseif_clauses.iter().all(|(_, block)| *block == then_block)
344    {
345        let merged_cond = elseif_clauses.iter().fold(cond.clone(), |acc, (expr, _)| {
346            simplify_expr(Expr::BinOp(BinOp::Or, Box::new(acc), Box::new(expr.clone())))
347        });
348        return vec![Stat::If {
349            cond: merged_cond,
350            then_block,
351            elseif_clauses: Vec::new(),
352            else_block: None,
353        }];
354    }
355
356    if elseif_clauses.is_empty() && else_block.is_none() {
357        if let Some(rewritten) = cache_repeated_trailing_getter_call(&cond, &then_block) {
358            return rewritten;
359        }
360    }
361
362    let (trimmed_then, trailing_then) = split_trailing_terminator_tail(&then_block);
363    if trailing_then.is_empty() {
364        return vec![Stat::If {
365            cond,
366            then_block,
367            elseif_clauses,
368            else_block,
369        }];
370    }
371
372    if !elseif_clauses.is_empty() || else_block.is_some() {
373        return vec![Stat::If {
374            cond,
375            then_block: trimmed_then,
376            elseif_clauses,
377            else_block,
378        }];
379    }
380
381    let mut out = vec![Stat::If {
382        cond,
383        then_block: trimmed_then,
384        elseif_clauses,
385        else_block,
386    }];
387    out.extend(trailing_then);
388    out
389}
390
391fn split_trailing_terminator_tail(block: &Block) -> (Block, Block) {
392    for (index, stat) in block.iter().enumerate() {
393        if is_terminator(stat) {
394            let prefix = block[..=index].to_vec();
395            let tail = block[index + 1..].to_vec();
396            return (prefix, tail);
397        }
398    }
399    (block.clone(), Vec::new())
400}
401
402fn block_ends_with_terminator(block: &Block) -> bool {
403    block.last().is_some_and(is_terminator)
404}
405
406fn is_terminator(stat: &Stat) -> bool {
407    matches!(stat, Stat::Return(_) | Stat::Break)
408}
409
410fn count_and_terms(expr: &Expr) -> usize {
411    match expr {
412        Expr::BinOp(BinOp::And, lhs, rhs) => count_and_terms(lhs) + count_and_terms(rhs),
413        _ => 1,
414    }
415}
416
417fn simplify_expr(expr: Expr) -> Expr {
418    match expr {
419        Expr::Index(table, key) => Expr::Index(
420            Box::new(simplify_expr(*table)),
421            Box::new(simplify_expr(*key)),
422        ),
423        Expr::Field(table, field) => Expr::Field(Box::new(simplify_expr(*table)), field),
424        Expr::MethodCall(call) => Expr::MethodCall(Box::new(simplify_call(*call))),
425        Expr::FuncCall(call) => Expr::FuncCall(Box::new(simplify_call(*call))),
426        Expr::BinOp(op, lhs, rhs) => {
427            let lhs = simplify_expr(*lhs);
428            let rhs = simplify_expr(*rhs);
429            match (op, &lhs) {
430                (BinOp::And, Expr::Bool(true)) => rhs,
431                (BinOp::And, Expr::Bool(false)) => Expr::Bool(false),
432                (BinOp::Or, Expr::Bool(false)) => rhs,
433                (BinOp::Or, Expr::Bool(true)) => Expr::Bool(true),
434                _ => Expr::BinOp(op, Box::new(lhs), Box::new(rhs)),
435            }
436        }
437        Expr::UnOp(UnOp::Not, operand) => {
438            let operand = simplify_expr(*operand);
439            match operand {
440                Expr::Bool(value) => Expr::Bool(!value),
441                Expr::UnOp(UnOp::Not, inner) => Expr::UnOp(UnOp::Not, inner),
442                other => Expr::UnOp(UnOp::Not, Box::new(other)),
443            }
444        }
445        Expr::UnOp(op, operand) => Expr::UnOp(op, Box::new(simplify_expr(*operand))),
446        Expr::FunctionDef(func) => Expr::FunctionDef(Box::new(Function {
447            params: func.params.clone(),
448            is_vararg: func.is_vararg,
449            body: normalize_block(&func.body),
450        })),
451        Expr::Table(fields) => Expr::Table(
452            fields
453                .into_iter()
454                .map(|field| match field {
455                    TableField::IndexField(key, value) => {
456                        TableField::IndexField(simplify_expr(key), simplify_expr(value))
457                    }
458                    TableField::NameField(name, value) => {
459                        TableField::NameField(name, simplify_expr(value))
460                    }
461                    TableField::Value(value) => TableField::Value(simplify_expr(value)),
462                })
463                .collect(),
464        ),
465        other => other,
466    }
467}
468
469fn simplify_call(call: CallExpr) -> CallExpr {
470    CallExpr {
471        func: simplify_expr(call.func),
472        args: call.args.into_iter().map(simplify_expr).collect(),
473    }
474}
475
476fn negate_condition(expr: Expr) -> Expr {
477    match expr {
478        Expr::Bool(value) => Expr::Bool(!value),
479        Expr::UnOp(UnOp::Not, inner) => simplify_expr(*inner),
480        other => Expr::UnOp(UnOp::Not, Box::new(other)),
481    }
482}
483
484fn cache_repeated_trailing_getter_call(cond: &Expr, then_block: &Block) -> Option<Block> {
485    let mut terms = Vec::new();
486    collect_and_terms(cond, &mut terms);
487    let last_term = terms.last()?.clone();
488    let call_expr = match &last_term {
489        Expr::FuncCall(call) if is_cacheable_getter_call(call) => last_term.clone(),
490        _ => return None,
491    };
492
493    if !block_contains_expr(then_block, &call_expr) {
494        return None;
495    }
496
497    let cache_name = uniquify_cache_name(suggest_cached_call_name(&call_expr), cond, then_block);
498    let outer_terms = &terms[..terms.len() - 1];
499    let rewritten_then = replace_exprs_in_block(then_block, &call_expr, &Expr::Name(cache_name.clone()));
500    let inner_if = Stat::If {
501        cond: Expr::Name(cache_name.clone()),
502        then_block: rewritten_then,
503        elseif_clauses: Vec::new(),
504        else_block: None,
505    };
506    let cached_local = Stat::LocalAssign {
507        names: vec![cache_name.clone()],
508        exprs: vec![call_expr],
509    };
510
511    if outer_terms.is_empty() {
512        return Some(vec![Stat::DoBlock(vec![cached_local, inner_if])]);
513    }
514
515    Some(vec![Stat::If {
516        cond: combine_and_terms(outer_terms),
517        then_block: vec![cached_local, inner_if],
518        elseif_clauses: Vec::new(),
519        else_block: None,
520    }])
521}
522
523fn cache_shared_negated_getter_calls(block: &Block) -> Block {
524    let mut out = Vec::new();
525    let mut index = 0;
526
527    while index < block.len() {
528        let Some((shared_call, mut group)) = collect_negated_getter_group(block, index) else {
529            out.push(block[index].clone());
530            index += 1;
531            continue;
532        };
533
534        let group_len = group.len();
535        if group_len < 2 {
536            out.push(block[index].clone());
537            index += 1;
538            continue;
539        }
540
541        let cache_name = uniquify_cache_name(suggest_cached_call_name(&shared_call), &Expr::Bool(false), block);
542        let replacement = Expr::UnOp(UnOp::Not, Box::new(Expr::Name(cache_name.clone())));
543        let prefixes: Vec<Expr> = group
544            .iter()
545            .filter_map(|(prefix, _)| prefix.clone())
546            .collect();
547
548        let rewritten_ifs: Block = group
549            .drain(..)
550            .map(|(prefix, stat)| rewrite_grouped_if(prefix, stat, &replacement))
551            .collect();
552
553        let mut outer_body = Vec::with_capacity(rewritten_ifs.len() + 1);
554        outer_body.push(Stat::LocalAssign {
555            names: vec![cache_name],
556            exprs: vec![shared_call],
557        });
558        outer_body.extend(rewritten_ifs);
559
560        if prefixes.len() == group_len && !prefixes.is_empty() {
561            out.push(Stat::If {
562                cond: combine_or_terms(&prefixes),
563                then_block: outer_body,
564                elseif_clauses: Vec::new(),
565                else_block: None,
566            });
567        } else {
568            out.push(Stat::DoBlock(outer_body));
569        }
570
571        index += group_len;
572    }
573
574    out
575}
576
577fn collect_negated_getter_group(block: &Block, start: usize) -> Option<(Expr, Vec<(Option<Expr>, Stat)>)> {
578    let (shared_call, first_prefix) = extract_negated_trailing_getter_from_if(block.get(start)?)?;
579    let mut group = vec![(first_prefix, block[start].clone())];
580    let mut index = start + 1;
581
582    while let Some((other_call, other_prefix)) = block.get(index).and_then(extract_negated_trailing_getter_from_if) {
583        if other_call != shared_call {
584            break;
585        }
586        group.push((other_prefix, block[index].clone()));
587        index += 1;
588    }
589
590    Some((shared_call, group))
591}
592
593fn extract_negated_trailing_getter_from_if(stat: &Stat) -> Option<(Expr, Option<Expr>)> {
594    let Stat::If {
595        cond,
596        then_block: _,
597        elseif_clauses,
598        else_block,
599    } = stat else {
600        return None;
601    };
602    if !elseif_clauses.is_empty() || else_block.is_some() {
603        return None;
604    }
605
606    match cond {
607        Expr::BinOp(BinOp::And, prefix, tail) => match &**tail {
608            Expr::UnOp(UnOp::Not, inner) => match &**inner {
609                Expr::FuncCall(call) if is_cacheable_getter_call(call) => {
610                    Some((*inner.clone(), Some(*prefix.clone())))
611                }
612                _ => None,
613            },
614            _ => None,
615        },
616        Expr::UnOp(UnOp::Not, inner) => match &**inner {
617            Expr::FuncCall(call) if is_cacheable_getter_call(call) => Some((*inner.clone(), None)),
618            _ => None,
619        },
620        _ => None,
621    }
622}
623
624fn rewrite_grouped_if(
625    prefix: Option<Expr>,
626    stat: Stat,
627    replacement: &Expr,
628) -> Stat {
629    let Stat::If {
630        cond: _,
631        then_block,
632        elseif_clauses: _,
633        else_block: _,
634    } = stat else {
635        unreachable!();
636    };
637
638    let cond = match prefix {
639        Some(prefix) => simplify_expr(Expr::BinOp(
640            BinOp::And,
641            Box::new(prefix),
642            Box::new(replacement.clone()),
643        )),
644        None => replacement.clone(),
645    };
646
647    Stat::If {
648        cond,
649        then_block,
650        elseif_clauses: Vec::new(),
651        else_block: None,
652    }
653}
654
655fn combine_or_terms(terms: &[Expr]) -> Expr {
656    terms
657        .iter()
658        .cloned()
659        .reduce(|lhs, rhs| simplify_expr(Expr::BinOp(BinOp::Or, Box::new(lhs), Box::new(rhs))))
660        .unwrap_or(Expr::Bool(true))
661}
662
663fn collect_and_terms<'a>(expr: &'a Expr, out: &mut Vec<Expr>) {
664    match expr {
665        Expr::BinOp(BinOp::And, lhs, rhs) => {
666            collect_and_terms(lhs, out);
667            collect_and_terms(rhs, out);
668        }
669        _ => out.push(expr.clone()),
670    }
671}
672
673fn combine_and_terms(terms: &[Expr]) -> Expr {
674    terms
675        .iter()
676        .cloned()
677        .reduce(|lhs, rhs| simplify_expr(Expr::BinOp(BinOp::And, Box::new(lhs), Box::new(rhs))))
678        .unwrap_or(Expr::Bool(true))
679}
680
681fn is_cacheable_getter_call(call: &CallExpr) -> bool {
682    match &call.func {
683        Expr::Field(_, method) => matches!(method.as_str(), "GetBuffByOwner" | "GetBuff"),
684        _ => false,
685    }
686}
687
688fn suggest_cached_call_name(expr: &Expr) -> String {
689    let Expr::FuncCall(call) = expr else {
690        return "cached_value".to_string();
691    };
692    let Expr::Field(_, method) = &call.func else {
693        return "cached_value".to_string();
694    };
695
696    if matches!(method.as_str(), "GetBuffByOwner" | "GetBuff") {
697        if let Some(expr) = call.args.first() {
698            match expr {
699                Expr::Number(NumLit::Int(buff_id)) => {
700                    return format!("buff_{}", buff_id);
701                }
702                Expr::Number(NumLit::Float(buff_id)) if buff_id.fract() == 0.0 => {
703                    return format!("buff_{}", *buff_id as i64);
704                }
705                _ => {}
706            }
707        }
708    }
709
710    camel_to_snake(method)
711}
712
713fn camel_to_snake(name: &str) -> String {
714    let mut out = String::new();
715    for (idx, ch) in name.chars().enumerate() {
716        if ch.is_ascii_uppercase() {
717            if idx != 0 {
718                out.push('_');
719            }
720            out.push(ch.to_ascii_lowercase());
721        } else {
722            out.push(ch);
723        }
724    }
725    out
726}
727
728fn uniquify_cache_name(base: String, cond: &Expr, block: &Block) -> String {
729    if !expr_contains_name(cond, &base) && !block_contains_name(block, &base) {
730        return base;
731    }
732
733    let mut suffix = 1;
734    loop {
735        let candidate = format!("{}_{}", base, suffix);
736        if !expr_contains_name(cond, &candidate) && !block_contains_name(block, &candidate) {
737            return candidate;
738        }
739        suffix += 1;
740    }
741}
742
743fn block_contains_name(block: &Block, name: &str) -> bool {
744    block.iter().any(|stat| stat_contains_name(stat, name))
745}
746
747fn stat_contains_name(stat: &Stat, name: &str) -> bool {
748    match stat {
749        Stat::LocalAssign { names, exprs } => {
750            names.iter().any(|existing| existing == name)
751                || exprs.iter().any(|expr| expr_contains_name(expr, name))
752        }
753        Stat::Assign { targets, values } => {
754            targets.iter().any(|expr| expr_contains_name(expr, name))
755                || values.iter().any(|expr| expr_contains_name(expr, name))
756        }
757        Stat::Call(call) => call_contains_name(call, name),
758        Stat::DoBlock(body) => block_contains_name(body, name),
759        Stat::While { cond, body } => expr_contains_name(cond, name) || block_contains_name(body, name),
760        Stat::Repeat { body, cond } => block_contains_name(body, name) || expr_contains_name(cond, name),
761        Stat::If { cond, then_block, elseif_clauses, else_block } => {
762            expr_contains_name(cond, name)
763                || block_contains_name(then_block, name)
764                || elseif_clauses.iter().any(|(expr, block)| expr_contains_name(expr, name) || block_contains_name(block, name))
765                || else_block.as_ref().is_some_and(|block| block_contains_name(block, name))
766        }
767        Stat::NumericFor { name: loop_name, start, limit, step, body } => {
768            loop_name == name
769                || expr_contains_name(start, name)
770                || expr_contains_name(limit, name)
771                || step.as_ref().is_some_and(|expr| expr_contains_name(expr, name))
772                || block_contains_name(body, name)
773        }
774        Stat::GenericFor { names, iterators, body } => {
775            names.iter().any(|existing| existing == name)
776                || iterators.iter().any(|expr| expr_contains_name(expr, name))
777                || block_contains_name(body, name)
778        }
779        Stat::Return(exprs) => exprs.iter().any(|expr| expr_contains_name(expr, name)),
780        Stat::Break | Stat::Comment(_) => false,
781    }
782}
783
784fn expr_contains_name(expr: &Expr, name: &str) -> bool {
785    match expr {
786        Expr::Name(existing) => existing == name,
787        Expr::Index(table, key) => expr_contains_name(table, name) || expr_contains_name(key, name),
788        Expr::Field(table, _) => expr_contains_name(table, name),
789        Expr::MethodCall(call) | Expr::FuncCall(call) => call_contains_name(call, name),
790        Expr::BinOp(_, lhs, rhs) => expr_contains_name(lhs, name) || expr_contains_name(rhs, name),
791        Expr::UnOp(_, inner) => expr_contains_name(inner, name),
792        Expr::FunctionDef(func) => block_contains_name(&func.body, name),
793        Expr::Table(fields) => fields.iter().any(|field| match field {
794            TableField::IndexField(key, value) => expr_contains_name(key, name) || expr_contains_name(value, name),
795            TableField::NameField(_, value) | TableField::Value(value) => expr_contains_name(value, name),
796        }),
797        _ => false,
798    }
799}
800
801fn call_contains_name(call: &CallExpr, name: &str) -> bool {
802    expr_contains_name(&call.func, name) || call.args.iter().any(|arg| expr_contains_name(arg, name))
803}
804
805fn block_contains_expr(block: &Block, target: &Expr) -> bool {
806    block.iter().any(|stat| stat_contains_expr(stat, target))
807}
808
809fn stat_contains_expr(stat: &Stat, target: &Expr) -> bool {
810    match stat {
811        Stat::LocalAssign { exprs, .. } => exprs.iter().any(|expr| expr_contains_expr(expr, target)),
812        Stat::Assign { targets, values } => {
813            targets.iter().any(|expr| expr_contains_expr(expr, target))
814                || values.iter().any(|expr| expr_contains_expr(expr, target))
815        }
816        Stat::Call(call) => expr_contains_expr(&Expr::FuncCall(Box::new(call.clone())), target),
817        Stat::DoBlock(body) => block_contains_expr(body, target),
818        Stat::While { cond, body } => expr_contains_expr(cond, target) || block_contains_expr(body, target),
819        Stat::Repeat { body, cond } => block_contains_expr(body, target) || expr_contains_expr(cond, target),
820        Stat::If { cond, then_block, elseif_clauses, else_block } => {
821            expr_contains_expr(cond, target)
822                || block_contains_expr(then_block, target)
823                || elseif_clauses.iter().any(|(expr, block)| expr_contains_expr(expr, target) || block_contains_expr(block, target))
824                || else_block.as_ref().is_some_and(|block| block_contains_expr(block, target))
825        }
826        Stat::NumericFor { start, limit, step, body, .. } => {
827            expr_contains_expr(start, target)
828                || expr_contains_expr(limit, target)
829                || step.as_ref().is_some_and(|expr| expr_contains_expr(expr, target))
830                || block_contains_expr(body, target)
831        }
832        Stat::GenericFor { iterators, body, .. } => {
833            iterators.iter().any(|expr| expr_contains_expr(expr, target))
834                || block_contains_expr(body, target)
835        }
836        Stat::Return(exprs) => exprs.iter().any(|expr| expr_contains_expr(expr, target)),
837        Stat::Break | Stat::Comment(_) => false,
838    }
839}
840
841fn expr_contains_expr(expr: &Expr, target: &Expr) -> bool {
842    if expr == target {
843        return true;
844    }
845
846    match expr {
847        Expr::Index(table, key) => expr_contains_expr(table, target) || expr_contains_expr(key, target),
848        Expr::Field(table, _) => expr_contains_expr(table, target),
849        Expr::MethodCall(call) | Expr::FuncCall(call) => {
850            expr_contains_expr(&call.func, target)
851                || call.args.iter().any(|arg| expr_contains_expr(arg, target))
852        }
853        Expr::BinOp(_, lhs, rhs) => expr_contains_expr(lhs, target) || expr_contains_expr(rhs, target),
854        Expr::UnOp(_, inner) => expr_contains_expr(inner, target),
855        Expr::FunctionDef(func) => block_contains_expr(&func.body, target),
856        Expr::Table(fields) => fields.iter().any(|field| match field {
857            TableField::IndexField(key, value) => expr_contains_expr(key, target) || expr_contains_expr(value, target),
858            TableField::NameField(_, value) | TableField::Value(value) => expr_contains_expr(value, target),
859        }),
860        _ => false,
861    }
862}
863
864fn replace_exprs_in_block(block: &Block, target: &Expr, replacement: &Expr) -> Block {
865    block.iter().map(|stat| replace_exprs_in_stat(stat, target, replacement)).collect()
866}
867
868fn replace_exprs_in_stat(stat: &Stat, target: &Expr, replacement: &Expr) -> Stat {
869    match stat {
870        Stat::LocalAssign { names, exprs } => Stat::LocalAssign {
871            names: names.clone(),
872            exprs: exprs.iter().map(|expr| replace_expr(expr, target, replacement)).collect(),
873        },
874        Stat::Assign { targets, values } => Stat::Assign {
875            targets: targets.iter().map(|expr| replace_expr(expr, target, replacement)).collect(),
876            values: values.iter().map(|expr| replace_expr(expr, target, replacement)).collect(),
877        },
878        Stat::Call(call) => match replace_expr(&Expr::FuncCall(Box::new(call.clone())), target, replacement) {
879            Expr::FuncCall(updated) => Stat::Call(*updated),
880            expr => Stat::Call(CallExpr { func: expr, args: Vec::new() }),
881        },
882        Stat::DoBlock(body) => Stat::DoBlock(replace_exprs_in_block(body, target, replacement)),
883        Stat::While { cond, body } => Stat::While {
884            cond: replace_expr(cond, target, replacement),
885            body: replace_exprs_in_block(body, target, replacement),
886        },
887        Stat::Repeat { body, cond } => Stat::Repeat {
888            body: replace_exprs_in_block(body, target, replacement),
889            cond: replace_expr(cond, target, replacement),
890        },
891        Stat::If { cond, then_block, elseif_clauses, else_block } => Stat::If {
892            cond: replace_expr(cond, target, replacement),
893            then_block: replace_exprs_in_block(then_block, target, replacement),
894            elseif_clauses: elseif_clauses
895                .iter()
896                .map(|(expr, block)| {
897                    (
898                        replace_expr(expr, target, replacement),
899                        replace_exprs_in_block(block, target, replacement),
900                    )
901                })
902                .collect(),
903            else_block: else_block
904                .as_ref()
905                .map(|block| replace_exprs_in_block(block, target, replacement)),
906        },
907        Stat::NumericFor { name, start, limit, step, body } => Stat::NumericFor {
908            name: name.clone(),
909            start: replace_expr(start, target, replacement),
910            limit: replace_expr(limit, target, replacement),
911            step: step.as_ref().map(|expr| replace_expr(expr, target, replacement)),
912            body: replace_exprs_in_block(body, target, replacement),
913        },
914        Stat::GenericFor { names, iterators, body } => Stat::GenericFor {
915            names: names.clone(),
916            iterators: iterators.iter().map(|expr| replace_expr(expr, target, replacement)).collect(),
917            body: replace_exprs_in_block(body, target, replacement),
918        },
919        Stat::Return(exprs) => Stat::Return(
920            exprs.iter().map(|expr| replace_expr(expr, target, replacement)).collect(),
921        ),
922        Stat::Break => Stat::Break,
923        Stat::Comment(text) => Stat::Comment(text.clone()),
924    }
925}
926
927fn replace_expr(expr: &Expr, target: &Expr, replacement: &Expr) -> Expr {
928    if expr == target {
929        return replacement.clone();
930    }
931
932    match expr {
933        Expr::Index(table, key) => Expr::Index(
934            Box::new(replace_expr(table, target, replacement)),
935            Box::new(replace_expr(key, target, replacement)),
936        ),
937        Expr::Field(table, field) => Expr::Field(
938            Box::new(replace_expr(table, target, replacement)),
939            field.clone(),
940        ),
941        Expr::MethodCall(call) => Expr::MethodCall(Box::new(replace_call(call, target, replacement))),
942        Expr::FuncCall(call) => Expr::FuncCall(Box::new(replace_call(call, target, replacement))),
943        Expr::BinOp(op, lhs, rhs) => Expr::BinOp(
944            *op,
945            Box::new(replace_expr(lhs, target, replacement)),
946            Box::new(replace_expr(rhs, target, replacement)),
947        ),
948        Expr::UnOp(op, inner) => Expr::UnOp(*op, Box::new(replace_expr(inner, target, replacement))),
949        Expr::FunctionDef(func) => Expr::FunctionDef(Box::new(Function {
950            params: func.params.clone(),
951            is_vararg: func.is_vararg,
952            body: replace_exprs_in_block(&func.body, target, replacement),
953        })),
954        Expr::Table(fields) => Expr::Table(
955            fields.iter().map(|field| match field {
956                TableField::IndexField(key, value) => TableField::IndexField(
957                    replace_expr(key, target, replacement),
958                    replace_expr(value, target, replacement),
959                ),
960                TableField::NameField(name, value) => {
961                    TableField::NameField(name.clone(), replace_expr(value, target, replacement))
962                }
963                TableField::Value(value) => TableField::Value(replace_expr(value, target, replacement)),
964            }).collect(),
965        ),
966        other => other.clone(),
967    }
968}
969
970fn replace_call(call: &CallExpr, target: &Expr, replacement: &Expr) -> CallExpr {
971    CallExpr {
972        func: replace_expr(&call.func, target, replacement),
973        args: call.args.iter().map(|arg| replace_expr(arg, target, replacement)).collect(),
974    }
975}
976
977/// Fold nested if-else chains into elseif clauses.
978/// Transforms: `if A then B else if C then D else E end end`
979/// into:       `if A then B elseif C then D else E end`
980fn fold_elseif_chain(block: &Block) -> Block {
981    block.iter().map(|stat| fold_stat(stat)).collect()
982}
983
984fn fold_stat(stat: &Stat) -> Stat {
985    match stat {
986        Stat::If {
987            cond,
988            then_block,
989            elseif_clauses,
990            else_block,
991        } => {
992            let then_block = fold_elseif_chain(then_block);
993            let mut new_elseifs: Vec<(Expr, Block)> = elseif_clauses
994                .iter()
995                .map(|(c, b)| (c.clone(), fold_elseif_chain(b)))
996                .collect();
997
998            // Try to flatten: if else_block is a single `if` statement, merge it
999            let new_else = if let Some(eb) = else_block {
1000                let folded = fold_elseif_chain(eb);
1001                if folded.len() == 1 {
1002                    if let Stat::If {
1003                        cond: inner_cond,
1004                        then_block: inner_then,
1005                        elseif_clauses: inner_elseifs,
1006                        else_block: inner_else,
1007                    } = &folded[0]
1008                    {
1009                        // Merge into elseif
1010                        new_elseifs.push((inner_cond.clone(), inner_then.clone()));
1011                        new_elseifs.extend(inner_elseifs.iter().cloned());
1012                        inner_else.clone()
1013                    } else {
1014                        Some(folded)
1015                    }
1016                } else {
1017                    Some(folded)
1018                }
1019            } else {
1020                None
1021            };
1022
1023            Stat::If {
1024                cond: cond.clone(),
1025                then_block,
1026                elseif_clauses: new_elseifs,
1027                else_block: new_else,
1028            }
1029        }
1030        Stat::While { cond, body } => Stat::While {
1031            cond: cond.clone(),
1032            body: fold_elseif_chain(body),
1033        },
1034        Stat::Repeat { body, cond } => Stat::Repeat {
1035            body: fold_elseif_chain(body),
1036            cond: cond.clone(),
1037        },
1038        Stat::NumericFor { name, start, limit, step, body } => Stat::NumericFor {
1039            name: name.clone(),
1040            start: start.clone(),
1041            limit: limit.clone(),
1042            step: step.clone(),
1043            body: fold_elseif_chain(body),
1044        },
1045        Stat::GenericFor { names, iterators, body } => Stat::GenericFor {
1046            names: names.clone(),
1047            iterators: iterators.clone(),
1048            body: fold_elseif_chain(body),
1049        },
1050        Stat::DoBlock(body) => Stat::DoBlock(fold_elseif_chain(body)),
1051        other => other.clone(),
1052    }
1053}
1054
1055fn emit_block(block: &Block, out: &mut String, indent: usize) {
1056    for (i, stat) in block.iter().enumerate() {
1057        // Add blank lines between top-level logical groups
1058        if indent == 0 && i > 0 {
1059            let prev = &block[i - 1];
1060            if should_separate(prev, stat) {
1061                out.push('\n');
1062            }
1063        }
1064        emit_stat(stat, out, indent);
1065    }
1066}
1067
1068/// Decide whether to insert a blank line between two consecutive top-level statements.
1069fn should_separate(prev: &Stat, curr: &Stat) -> bool {
1070    // Always separate after a function definition
1071    if is_func_def(prev) {
1072        return true;
1073    }
1074    // Separate before a function definition
1075    if is_func_def(curr) {
1076        return true;
1077    }
1078    // Separate between different "kinds" of statements
1079    // (e.g., after a block of assignments before a call, or vice versa)
1080    false
1081}
1082
1083fn is_func_def(stat: &Stat) -> bool {
1084    match stat {
1085        Stat::Assign { values, .. } => {
1086            values.len() == 1 && matches!(&values[0], Expr::FunctionDef(_))
1087        }
1088        Stat::LocalAssign { exprs, .. } => {
1089            exprs.len() == 1 && matches!(&exprs[0], Expr::FunctionDef(_))
1090        }
1091        _ => false,
1092    }
1093}
1094
1095fn emit_stat(stat: &Stat, out: &mut String, indent: usize) {
1096    let pad = "  ".repeat(indent);
1097    match stat {
1098        Stat::LocalAssign { names, exprs } => {
1099            write!(out, "{}local {}", pad, names.join(", ")).unwrap();
1100            if !exprs.is_empty() {
1101                out.push_str(" = ");
1102                emit_expr_list(exprs, out);
1103            }
1104            out.push('\n');
1105        }
1106        Stat::Assign { targets, values } => {
1107            // Pretty-print `name = function(...) end` as `function name(...) end`
1108            if targets.len() == 1 && values.len() == 1 {
1109                if let Expr::FunctionDef(func) = &values[0] {
1110                    let name = match &targets[0] {
1111                        Expr::Global(n) => Some(n.clone()),
1112                        Expr::Name(n) => Some(n.clone()),
1113                        Expr::Field(table, field) => {
1114                            // t.method = function(...) -> function t.method(...)
1115                            let mut s = String::new();
1116                            emit_expr(table, &mut s, 10);
1117                            s.push('.');
1118                            s.push_str(field);
1119                            Some(s)
1120                        }
1121                        _ => None,
1122                    };
1123                    if let Some(fname) = name {
1124                        write!(out, "{}function {}(", pad, fname).unwrap();
1125                        let mut params = func.params.join(", ");
1126                        if func.is_vararg {
1127                            if !params.is_empty() {
1128                                params.push_str(", ");
1129                            }
1130                            params.push_str("...");
1131                        }
1132                        out.push_str(&params);
1133                        out.push_str(")\n");
1134                        emit_block(&func.body, out, indent + 1);
1135                        writeln!(out, "{}end", pad).unwrap();
1136                        return;
1137                    }
1138                }
1139            }
1140            write!(out, "{}", pad).unwrap();
1141            emit_expr_list(targets, out);
1142            out.push_str(" = ");
1143            emit_expr_list(values, out);
1144            out.push('\n');
1145        }
1146        Stat::Call(call) => {
1147            write!(out, "{}", pad).unwrap();
1148            emit_call(call, out);
1149            out.push('\n');
1150        }
1151        Stat::DoBlock(body) => {
1152            writeln!(out, "{}do", pad).unwrap();
1153            emit_block(body, out, indent + 1);
1154            writeln!(out, "{}end", pad).unwrap();
1155        }
1156        Stat::While { cond, body } => {
1157            write!(out, "{}while ", pad).unwrap();
1158            emit_expr(cond, out, 0);
1159            out.push_str(" do\n");
1160            emit_block(body, out, indent + 1);
1161            writeln!(out, "{}end", pad).unwrap();
1162        }
1163        Stat::Repeat { body, cond } => {
1164            writeln!(out, "{}repeat", pad).unwrap();
1165            emit_block(body, out, indent + 1);
1166            write!(out, "{}until ", pad).unwrap();
1167            emit_expr(cond, out, 0);
1168            out.push('\n');
1169        }
1170        Stat::If {
1171            cond,
1172            then_block,
1173            elseif_clauses,
1174            else_block,
1175        } => {
1176            write!(out, "{}if ", pad).unwrap();
1177            emit_expr(cond, out, 0);
1178            out.push_str(" then\n");
1179            emit_block(then_block, out, indent + 1);
1180            for (ec, eb) in elseif_clauses {
1181                write!(out, "{}elseif ", pad).unwrap();
1182                emit_expr(ec, out, 0);
1183                out.push_str(" then\n");
1184                emit_block(eb, out, indent + 1);
1185            }
1186            if let Some(eb) = else_block {
1187                writeln!(out, "{}else", pad).unwrap();
1188                emit_block(eb, out, indent + 1);
1189            }
1190            writeln!(out, "{}end", pad).unwrap();
1191        }
1192        Stat::NumericFor {
1193            name,
1194            start,
1195            limit,
1196            step,
1197            body,
1198        } => {
1199            write!(out, "{}for {} = ", pad, name).unwrap();
1200            emit_expr(start, out, 0);
1201            out.push_str(", ");
1202            emit_expr(limit, out, 0);
1203            if let Some(s) = step {
1204                out.push_str(", ");
1205                emit_expr(s, out, 0);
1206            }
1207            out.push_str(" do\n");
1208            emit_block(body, out, indent + 1);
1209            writeln!(out, "{}end", pad).unwrap();
1210        }
1211        Stat::GenericFor {
1212            names,
1213            iterators,
1214            body,
1215        } => {
1216            write!(out, "{}for {} in ", pad, names.join(", ")).unwrap();
1217            emit_expr_list(iterators, out);
1218            out.push_str(" do\n");
1219            emit_block(body, out, indent + 1);
1220            writeln!(out, "{}end", pad).unwrap();
1221        }
1222        Stat::Return(exprs) => {
1223            write!(out, "{}return", pad).unwrap();
1224            if !exprs.is_empty() {
1225                out.push(' ');
1226                emit_expr_list(exprs, out);
1227            }
1228            out.push('\n');
1229        }
1230        Stat::Break => {
1231            writeln!(out, "{}break", pad).unwrap();
1232        }
1233        Stat::Comment(text) => {
1234            writeln!(out, "{}-- {}", pad, text).unwrap();
1235        }
1236    }
1237}
1238
1239fn emit_expr_list(exprs: &[Expr], out: &mut String) {
1240    for (i, e) in exprs.iter().enumerate() {
1241        if i > 0 {
1242            out.push_str(", ");
1243        }
1244        emit_expr(e, out, 0);
1245    }
1246}
1247
1248/// Emit an expression, handling operator precedence and parenthesization.
1249/// `parent_prec` is the precedence of the enclosing operator (0 = no enclosing op).
1250fn emit_expr(expr: &Expr, out: &mut String, parent_prec: u8) {
1251    match expr {
1252        Expr::Nil => out.push_str("nil"),
1253        Expr::Bool(true) => out.push_str("true"),
1254        Expr::Bool(false) => out.push_str("false"),
1255        Expr::Number(n) => emit_number(n, out),
1256        Expr::StringLit(s) => emit_string(s, out),
1257        Expr::VarArg => out.push_str("..."),
1258        Expr::Name(n) => out.push_str(n),
1259        Expr::Global(n) => out.push_str(n),
1260        Expr::Register(r) => write!(out, "r{}", r).unwrap(),
1261        Expr::Upvalue(u) => write!(out, "upval{}", u).unwrap(),
1262        Expr::Index(table, key) => {
1263            emit_expr(table, out, 10);
1264            out.push('[');
1265            emit_expr(key, out, 0);
1266            out.push(']');
1267        }
1268        Expr::Field(table, field) => {
1269            emit_expr(table, out, 10);
1270            out.push('.');
1271            out.push_str(field);
1272        }
1273        Expr::BinOp(op, lhs, rhs) => {
1274            let prec = op.precedence();
1275            let needs_parens = prec < parent_prec;
1276            if needs_parens {
1277                out.push('(');
1278            }
1279            emit_expr(lhs, out, prec);
1280            write!(out, " {} ", op.symbol()).unwrap();
1281            // Right-associative: right child needs prec+1 to avoid unnecessary parens
1282            let rhs_prec = if op.is_right_assoc() { prec } else { prec + 1 };
1283            emit_expr(rhs, out, rhs_prec);
1284            if needs_parens {
1285                out.push(')');
1286            }
1287        }
1288        Expr::UnOp(op, operand) => {
1289            let prec = op.precedence();
1290            let needs_parens = prec < parent_prec;
1291            if needs_parens {
1292                out.push('(');
1293            }
1294            out.push_str(op.symbol());
1295            emit_expr(operand, out, prec);
1296            if needs_parens {
1297                out.push(')');
1298            }
1299        }
1300        Expr::FuncCall(call) => {
1301            emit_call(call, out);
1302        }
1303        Expr::MethodCall(call) => {
1304            emit_call(call, out);
1305        }
1306        Expr::FunctionDef(func) => {
1307            emit_function_def(func, out, false);
1308        }
1309        Expr::Table(fields) => {
1310            emit_table(fields, out);
1311        }
1312    }
1313}
1314
1315fn emit_call(call: &CallExpr, out: &mut String) {
1316    emit_expr(&call.func, out, 10);
1317    out.push('(');
1318    emit_expr_list(&call.args, out);
1319    out.push(')');
1320}
1321
1322fn emit_function_def(func: &Function, out: &mut String, _as_stat: bool) {
1323    out.push_str("function(");
1324    let mut params = func.params.join(", ");
1325    if func.is_vararg {
1326        if !params.is_empty() {
1327            params.push_str(", ");
1328        }
1329        params.push_str("...");
1330    }
1331    out.push_str(&params);
1332    out.push_str(")\n");
1333
1334    // Estimate current indent from trailing whitespace
1335    let current_indent = count_trailing_indent(out);
1336    emit_block(&func.body, out, current_indent + 1);
1337
1338    let pad = "  ".repeat(current_indent);
1339    write!(out, "{}end", pad).unwrap();
1340}
1341
1342fn emit_table(fields: &[TableField], out: &mut String) {
1343    if fields.is_empty() {
1344        out.push_str("{}");
1345        return;
1346    }
1347
1348    // Estimate inline length to decide single-line vs multi-line
1349    let current_indent = count_trailing_indent(out);
1350    let inline = emit_table_inline(fields);
1351    // Use multi-line if: inline is too long, or has nested tables, or many fields
1352    let use_multiline = inline.len() > 80 || fields.len() > 4 && inline.len() > 60;
1353
1354    if !use_multiline {
1355        out.push_str(&inline);
1356        return;
1357    }
1358
1359    // Multi-line format
1360    let inner_pad = "  ".repeat(current_indent + 1);
1361    let outer_pad = "  ".repeat(current_indent);
1362    out.push_str("{\n");
1363    for (i, field) in fields.iter().enumerate() {
1364        out.push_str(&inner_pad);
1365        match field {
1366            TableField::IndexField(key, val) => {
1367                out.push('[');
1368                emit_expr(key, out, 0);
1369                out.push_str("] = ");
1370                emit_expr(val, out, 0);
1371            }
1372            TableField::NameField(name, val) => {
1373                out.push_str(name);
1374                out.push_str(" = ");
1375                emit_expr(val, out, 0);
1376            }
1377            TableField::Value(val) => {
1378                emit_expr(val, out, 0);
1379            }
1380        }
1381        if i + 1 < fields.len() {
1382            out.push(',');
1383        }
1384        out.push('\n');
1385    }
1386    write!(out, "{}}}", outer_pad).unwrap();
1387}
1388
1389/// Emit a table as a single-line string (for length estimation).
1390fn emit_table_inline(fields: &[TableField]) -> String {
1391    let mut s = String::new();
1392    s.push('{');
1393    for (i, field) in fields.iter().enumerate() {
1394        if i > 0 {
1395            s.push_str(", ");
1396        }
1397        match field {
1398            TableField::IndexField(key, val) => {
1399                s.push('[');
1400                emit_expr(key, &mut s, 0);
1401                s.push_str("] = ");
1402                emit_expr(val, &mut s, 0);
1403            }
1404            TableField::NameField(name, val) => {
1405                s.push_str(name);
1406                s.push_str(" = ");
1407                emit_expr(val, &mut s, 0);
1408            }
1409            TableField::Value(val) => {
1410                emit_expr(val, &mut s, 0);
1411            }
1412        }
1413    }
1414    s.push('}');
1415    s
1416}
1417
1418fn emit_number(n: &NumLit, out: &mut String) {
1419    match n {
1420        NumLit::Int(v) => write!(out, "{}", v).unwrap(),
1421        NumLit::Float(v) => {
1422            if v.fract() == 0.0 && v.abs() < 1e15 {
1423                // Emit as integer-looking float if it has no fractional part
1424                write!(out, "{}", *v as i64).unwrap();
1425            } else {
1426                write!(out, "{}", v).unwrap();
1427            }
1428        }
1429    }
1430}
1431
1432/// Emit a string literal with proper escaping.
1433/// Supports multi-line string detection, GBK decoding, and non-UTF-8 byte escapes.
1434fn emit_string(bytes: &[u8], out: &mut String) {
1435    // Try UTF-8 first
1436    if let Ok(s) = std::str::from_utf8(bytes) {
1437        emit_string_content(s, bytes, out);
1438        return;
1439    }
1440
1441    // Try GBK decoding (common in JX3 Lua scripts)
1442    let (decoded, _, had_errors) = encoding_rs::GBK.decode(bytes);
1443    if !had_errors {
1444        emit_string_content(&decoded, bytes, out);
1445        return;
1446    }
1447
1448    // Fallback: emit with byte escapes for non-ASCII
1449    out.push('"');
1450    for &b in bytes {
1451        emit_byte_escaped(b, out);
1452    }
1453    out.push('"');
1454}
1455
1456/// Emit a string that has been successfully decoded to text.
1457fn emit_string_content(text: &str, _raw: &[u8], out: &mut String) {
1458    let has_newlines = text.contains('\n');
1459    let has_long_bracket_close = text.contains("]]");
1460    let is_printable = text.chars().all(|c| !c.is_control() || c == '\n' || c == '\r' || c == '\t');
1461
1462    if has_newlines && !has_long_bracket_close && is_printable {
1463        // Use [[...]] long string
1464        out.push_str("[[");
1465        if text.starts_with('\n') {
1466            out.push('\n');
1467        }
1468        out.push_str(text);
1469        out.push_str("]]");
1470        return;
1471    }
1472
1473    // Use quoted string with escapes
1474    out.push('"');
1475    for ch in text.chars() {
1476        match ch {
1477            '\\' => out.push_str("\\\\"),
1478            '"' => out.push_str("\\\""),
1479            '\n' => out.push_str("\\n"),
1480            '\r' => out.push_str("\\r"),
1481            '\t' => out.push_str("\\t"),
1482            '\0' => out.push_str("\\0"),
1483            '\x07' => out.push_str("\\a"),
1484            '\x08' => out.push_str("\\b"),
1485            '\x0C' => out.push_str("\\f"),
1486            '\x0B' => out.push_str("\\v"),
1487            c if c >= ' ' && c <= '~' => out.push(c),
1488            c if !c.is_control() => out.push(c), // printable Unicode (incl. CJK)
1489            c => {
1490                // Control character: emit as byte escapes
1491                let mut buf = [0u8; 4];
1492                let s = c.encode_utf8(&mut buf);
1493                for &b in s.as_bytes() {
1494                    write!(out, "\\{}", b).unwrap();
1495                }
1496            }
1497        }
1498    }
1499    out.push('"');
1500}
1501
1502fn emit_byte_escaped(b: u8, out: &mut String) {
1503    match b {
1504        b'\\' => out.push_str("\\\\"),
1505        b'"' => out.push_str("\\\""),
1506        b'\n' => out.push_str("\\n"),
1507        b'\r' => out.push_str("\\r"),
1508        b'\t' => out.push_str("\\t"),
1509        b'\0' => out.push_str("\\0"),
1510        0x07 => out.push_str("\\a"),
1511        0x08 => out.push_str("\\b"),
1512        0x0C => out.push_str("\\f"),
1513        0x0B => out.push_str("\\v"),
1514        0x20..=0x7E => out.push(b as char),
1515        _ => {
1516            write!(out, "\\{}", b).unwrap();
1517        }
1518    }
1519}
1520
1521fn count_trailing_indent(s: &str) -> usize {
1522    // Count indent level from last newline
1523    if let Some(last_nl) = s.rfind('\n') {
1524        let after = &s[last_nl + 1..];
1525        let spaces = after.len() - after.trim_start().len();
1526        spaces / 2
1527    } else {
1528        0
1529    }
1530}
1531
1532#[cfg(test)]
1533mod tests {
1534    use super::*;
1535
1536    fn emit_single(stat: Stat) -> String {
1537        emit_chunk(&Function {
1538            params: Vec::new(),
1539            is_vararg: false,
1540            body: vec![stat],
1541        })
1542    }
1543
1544    #[test]
1545    fn flattens_linear_nested_ifs() {
1546        let source = emit_single(Stat::If {
1547            cond: Expr::Name("a".to_string()),
1548            then_block: vec![Stat::If {
1549                cond: Expr::Name("b".to_string()),
1550                then_block: vec![Stat::Return(vec![])],
1551                elseif_clauses: Vec::new(),
1552                else_block: None,
1553            }],
1554            elseif_clauses: Vec::new(),
1555            else_block: None,
1556        });
1557
1558        assert!(source.contains("if a and b then"));
1559        assert!(!source.contains("if b then"));
1560    }
1561
1562    #[test]
1563    fn inverts_empty_then_branch() {
1564        let source = emit_single(Stat::If {
1565            cond: Expr::Name("a".to_string()),
1566            then_block: Vec::new(),
1567            elseif_clauses: Vec::new(),
1568            else_block: Some(vec![Stat::Return(vec![])]),
1569        });
1570
1571        assert!(source.contains("if not a then"));
1572        assert!(!source.contains("else"));
1573    }
1574
1575    #[test]
1576    fn simplifies_true_and_condition() {
1577        let source = emit_single(Stat::If {
1578            cond: Expr::BinOp(
1579                BinOp::And,
1580                Box::new(Expr::Bool(true)),
1581                Box::new(Expr::Name("ready".to_string())),
1582            ),
1583            then_block: vec![Stat::Return(vec![])],
1584            elseif_clauses: Vec::new(),
1585            else_block: None,
1586        });
1587
1588        assert!(source.contains("if ready then"));
1589        assert!(!source.contains("true and"));
1590    }
1591
1592    #[test]
1593    fn hoists_guard_tail_out_of_outer_if() {
1594        let source = emit_chunk(&Function {
1595            params: Vec::new(),
1596            is_vararg: false,
1597            body: vec![Stat::If {
1598                cond: Expr::Name("a".to_string()),
1599                then_block: vec![
1600                    Stat::If {
1601                        cond: Expr::Name("b".to_string()),
1602                        then_block: vec![Stat::Return(vec![])],
1603                        elseif_clauses: Vec::new(),
1604                        else_block: None,
1605                    },
1606                    Stat::Call(CallExpr {
1607                        func: Expr::Name("next_step".to_string()),
1608                        args: Vec::new(),
1609                    }),
1610                ],
1611                elseif_clauses: Vec::new(),
1612                else_block: None,
1613            }],
1614        });
1615
1616        assert!(source.contains("if a and b then"));
1617        assert!(source.contains("end\nnext_step()"));
1618    }
1619
1620    #[test]
1621    fn folds_else_if_to_elseif() {
1622        let source = emit_single(Stat::If {
1623            cond: Expr::Name("a".to_string()),
1624            then_block: vec![Stat::Call(CallExpr {
1625                func: Expr::Name("left".to_string()),
1626                args: Vec::new(),
1627            })],
1628            elseif_clauses: Vec::new(),
1629            else_block: Some(vec![Stat::If {
1630                cond: Expr::Name("b".to_string()),
1631                then_block: vec![Stat::Call(CallExpr {
1632                    func: Expr::Name("right".to_string()),
1633                    args: Vec::new(),
1634                })],
1635                elseif_clauses: Vec::new(),
1636                else_block: None,
1637            }]),
1638        });
1639
1640        assert!(source.contains("elseif b then"));
1641        assert!(!source.contains("else\n  if b then"));
1642    }
1643
1644    #[test]
1645    fn merges_identical_elseif_bodies() {
1646        let body = vec![Stat::Call(CallExpr {
1647            func: Expr::Name("apply".to_string()),
1648            args: Vec::new(),
1649        })];
1650        let source = emit_single(Stat::If {
1651            cond: Expr::Name("left".to_string()),
1652            then_block: body.clone(),
1653            elseif_clauses: vec![
1654                (Expr::Name("right".to_string()), body.clone()),
1655                (Expr::Name("extra".to_string()), body),
1656            ],
1657            else_block: None,
1658        });
1659
1660        assert!(source.contains("if left or right or extra then"));
1661        assert!(!source.contains("elseif"));
1662    }
1663
1664    #[test]
1665    fn caches_repeated_getter_call_inside_if() {
1666        let buff_call = Expr::FuncCall(Box::new(CallExpr {
1667            func: Expr::Field(Box::new(Expr::Name("a1".to_string())), "GetBuffByOwner".to_string()),
1668            args: vec![
1669                Expr::Number(NumLit::Int(30349)),
1670                Expr::Number(NumLit::Int(0)),
1671                Expr::Field(Box::new(Expr::Name("a0".to_string())), "dwID".to_string()),
1672            ],
1673        }));
1674        let source = emit_single(Stat::If {
1675            cond: Expr::BinOp(
1676                BinOp::And,
1677                Box::new(Expr::Name("ready".to_string())),
1678                Box::new(buff_call.clone()),
1679            ),
1680            then_block: vec![Stat::Assign {
1681                targets: vec![Expr::Name("damage".to_string())],
1682                values: vec![Expr::Field(Box::new(buff_call), "nStackNum".to_string())],
1683            }],
1684            elseif_clauses: Vec::new(),
1685            else_block: None,
1686        });
1687
1688        assert!(source.contains("if ready then"));
1689        assert!(source.contains("local buff_30349 = a1.GetBuffByOwner(30349, 0, a0.dwID)"));
1690        assert!(source.contains("if buff_30349 then"));
1691        assert!(source.contains("damage = buff_30349.nStackNum"));
1692    }
1693
1694    #[test]
1695    fn caches_shared_negated_getter_across_sibling_ifs() {
1696        let guard_call = Expr::FuncCall(Box::new(CallExpr {
1697            func: Expr::Field(Box::new(Expr::Name("a1".to_string())), "GetBuffByOwner".to_string()),
1698            args: vec![
1699                Expr::Number(NumLit::Int(4706)),
1700                Expr::Number(NumLit::Int(1)),
1701                Expr::Field(Box::new(Expr::Name("a0".to_string())), "dwID".to_string()),
1702            ],
1703        }));
1704        let source = emit_chunk(&Function {
1705            params: Vec::new(),
1706            is_vararg: false,
1707            body: vec![
1708                Stat::If {
1709                    cond: Expr::BinOp(
1710                        BinOp::And,
1711                        Box::new(Expr::Name("left".to_string())),
1712                        Box::new(Expr::UnOp(UnOp::Not, Box::new(guard_call.clone()))),
1713                    ),
1714                    then_block: vec![Stat::Call(CallExpr {
1715                        func: Expr::Name("first".to_string()),
1716                        args: Vec::new(),
1717                    })],
1718                    elseif_clauses: Vec::new(),
1719                    else_block: None,
1720                },
1721                Stat::If {
1722                    cond: Expr::BinOp(
1723                        BinOp::And,
1724                        Box::new(Expr::Name("right".to_string())),
1725                        Box::new(Expr::UnOp(UnOp::Not, Box::new(guard_call))),
1726                    ),
1727                    then_block: vec![Stat::Call(CallExpr {
1728                        func: Expr::Name("second".to_string()),
1729                        args: Vec::new(),
1730                    })],
1731                    elseif_clauses: Vec::new(),
1732                    else_block: None,
1733                },
1734            ],
1735        });
1736
1737        assert!(source.contains("if left or right then"));
1738        assert!(source.contains("local buff_4706 = a1.GetBuffByOwner(4706, 1, a0.dwID)"));
1739        assert!(source.contains("if left and not buff_4706 then"));
1740        assert!(source.contains("if right and not buff_4706 then"));
1741    }
1742}