tjs2dec\decompile/
srcgen_high.rs

1use anyhow::Result;
2use std::collections::{HashMap, HashSet};
3use std::fmt::Write as _;
4
5use crate::{Tjs2File, Tjs2Object};
6
7use super::cfg::Cfg;
8use super::expr::{BinOp, Expr, UnOp};
9use super::expr_build::{ExprProgram, Stmt, Terminator};
10use super::ssa::{SsaProgram, Var, VarId};
11
12fn vm_binop(op: &str) -> Option<BinOp> {
13    match op {
14        "VM_ADD" | "ADD" => Some(BinOp::Add),
15        "VM_SUB" | "SUB" => Some(BinOp::Sub),
16        "VM_MUL" | "MUL" => Some(BinOp::Mul),
17        "VM_DIV" | "DIV" => Some(BinOp::Div),
18        "VM_MOD" | "MOD" => Some(BinOp::Mod),
19
20        "VM_SAL" | "SHL" => Some(BinOp::Shl),
21        "VM_SAR" | "SHR" => Some(BinOp::Shr),
22        "VM_SR" | "USHR" => Some(BinOp::UShr),
23
24        "VM_BAND" | "BAND" => Some(BinOp::BitAnd),
25        "VM_BXOR" | "BXOR" => Some(BinOp::BitXor),
26        "VM_BOR" | "BOR" => Some(BinOp::BitOr),
27
28        "VM_LAND" | "LAND" => Some(BinOp::LogAnd),
29        "VM_LOR" | "LOR" => Some(BinOp::LogOr),
30
31        "VM_EQ" | "EQ" => Some(BinOp::Eq),
32        "VM_NE" | "NE" => Some(BinOp::Ne),
33        "VM_DEQ" | "DEQ" => Some(BinOp::StrictEq),
34        "VM_DNE" | "DNE" => Some(BinOp::StrictNe),
35
36        "VM_LT" | "LT" => Some(BinOp::Lt),
37        "VM_LE" | "LE" => Some(BinOp::Le),
38        "VM_GT" | "GT" => Some(BinOp::Gt),
39        "VM_GE" | "GE" => Some(BinOp::Ge),
40
41        "VM_IN" | "CHKINS" => Some(BinOp::In),
42
43        _ => None,
44    }
45}
46
47fn vm_unop(op: &str) -> Option<UnOp> {
48    match op {
49        "VM_CHS" | "CHS" => Some(UnOp::Neg),
50        "VM_LNOT" | "LNOT" => Some(UnOp::Not),
51        "VM_BNOT" | "BNOT" => Some(UnOp::BitNot),
52        "VM_TYPEOF" | "TYPEOF" => Some(UnOp::Typeof),
53        "VM_DELETE" | "DELETE" => Some(UnOp::Delete),
54        _ => None,
55    }
56}
57
58fn fmt_octet_literal(bytes: &[u8]) -> String {
59    // B: official usage style
60    let mut s = String::new();
61    s.push_str("octet([");
62    for (k, b) in bytes.iter().enumerate() {
63        if k != 0 {
64            s.push_str(", ");
65        }
66        s.push_str(&format!("0x{:02X}", b));
67    }
68    s.push_str("])");
69    s
70}
71
72fn escape_tjs_string_min(s: &str) -> String {
73    let mut out = String::new();
74    for ch in s.chars() {
75        match ch {
76            '\\' => out.push_str("\\\\"),
77            '"' => out.push_str("\\\""),
78            '\n' => out.push_str("\\n"),
79            '\r' => out.push_str("\\r"),
80            '\t' => out.push_str("\\t"),
81            '\0' => out.push_str("\\0"),
82            _ => out.push(ch),
83        }
84    }
85    out
86}
87
88/// Options controlling how code is emitted.
89pub struct SrcgenOptions {
90    pub inline: bool,
91}
92
93impl Default for SrcgenOptions {
94    fn default() -> Self {
95        SrcgenOptions { inline: true }
96    }
97}
98
99/// Returns true if obj or any of its ancestors (via parent chain) is a class (context_type=6),
100/// stopping at context_type=0 (global). Used to decide if `%-2` (scope) maps to `this`.
101fn scope_is_class(file: &Tjs2File, obj: &Tjs2Object) -> bool {
102    let mut cur = obj.parent;
103    loop {
104        if cur < 0 || cur as usize >= file.objects.len() {
105            return false;
106        }
107        let p = &file.objects[cur as usize];
108        match p.context_type {
109            0 => return false,
110            6 => return true,
111            _ => cur = p.parent,
112        }
113    }
114}
115
116/// Build a fmt_var closure appropriate for the given object's context.
117/// - r >= 0: r{r}_{ver}
118/// - r == -1: "this"
119/// - r == -2: "this" if in a class scope, else "global"
120/// - r <= -3 and frame_idx < arg_count: "a{frame_idx}" (declared parameter)
121/// - r <= -3 and frame_idx >= arg_count: "_fr{frame_idx - arg_count}" (local frame slot)
122fn make_fmt_var(in_class: bool, arg_count: usize) -> impl Fn(VarId) -> String {
123    move |vid: VarId| -> String {
124        match vid.var {
125            Var::Reg(r) if r >= 0 => format!("r{}_{}", r, vid.ver),
126            Var::Reg(-1) => "this".to_string(),
127            Var::Reg(-2) => {
128                if in_class {
129                    "this".to_string()
130                } else {
131                    "global".to_string()
132                }
133            }
134            Var::Reg(r) => {
135                let frame_idx = (-3 - r) as usize;
136                if frame_idx < arg_count {
137                    format!("a{}", frame_idx)
138                } else {
139                    format!("_fr{}", frame_idx - arg_count)
140                }
141            }
142            Var::Flag => format!("flag_{}", vid.ver),
143            Var::Exception => format!("exc_{}", vid.ver),
144        }
145    }
146}
147
148// pub fn dump_src_file(file: &Tjs2File, _opt: SrcgenOptions) -> Result<String> {
149//     let mut out = String::new();
150//     writeln!(
151//         out,
152//         "// Decompiled from TJS2 bytecode\n// objects: {}\n",
153//         file.objects.len()
154//     )?;
155
156//     for obj in &file.objects {
157//         if obj.code.is_empty() {
158//             continue;
159//         }
160//         writeln!(
161//             out,
162//             "// == object {}: {} ==",
163//             obj.index,
164//             obj.name.as_deref().unwrap_or("<anonymous>")
165//         )?;
166
167//         let lhs = obj_lhs(obj.index, obj.name.as_deref());
168//         writeln!(out, "{} = function() {{", lhs)?;
169
170//         let cfg = Cfg::build(obj)?;
171//         let ssa = SsaProgram::from_cfg(&cfg)?;
172//         let prog = ExprProgram::from_ssa(file, obj, &ssa)?;
173
174//         let fmt_var = |vid: VarId| -> String { fmt_vid_tjs(vid) };
175//         emit_var_decls(&mut out, &prog, &fmt_var)?;
176
177//         // Recover return expressions from SSA (expr_build's Terminator::Ret has no expr).
178//         let mut ret_expr: Vec<Option<Expr>> = vec![None; prog.blocks.len()];
179//         for b in &ssa.blocks {
180//             if let Some(last) = b.insns.last() {
181//                 // VM_RET has the return value in uses[0] (static, no guessing).
182//                 if last.mnemonic.eq_ignore_ascii_case("RET") || last.mnemonic.eq_ignore_ascii_case("VM_RET") {
183//                     if let Some(v) = last.uses.get(0).copied() {
184//                         ret_expr[b.id] = Some(Expr::SsaVar(v));
185//                     }
186//                 }
187//             }
188//         }
189
190//         let mut s = Structurer::new(&cfg, &prog, &fmt_var, ret_expr);
191//         let lines = s.emit_function_body(prog.entry_block, 2);
192
193//         for l in lines {
194//             writeln!(out, "{}", l)?;
195//         }
196//         writeln!(out, "}};\n")?;
197//     }
198
199//     Ok(out)
200// }
201
202fn const_propagate_intrablock(prog: &mut ExprProgram) {
203    // In TJS2, the initial value (ver=0) of all non-negative local registers is void.
204    // Register 0 (result register) is most commonly seen as "void" when used as a store value.
205    let initial_void: HashMap<VarId, Expr> = {
206        let mut m = HashMap::new();
207        m.insert(
208            VarId {
209                var: Var::Reg(0),
210                ver: 0,
211            },
212            Expr::Void,
213        );
214        m
215    };
216
217    for b in &mut prog.blocks {
218        let mut env: HashMap<VarId, Expr> = initial_void.clone();
219
220        for st in &mut b.stmts {
221            // 1) rewrite uses in this statement
222            rewrite_stmt(st, &env);
223
224            // 2) record defs if RHS is safe to propagate
225            match st {
226                Stmt::Assign { dst, expr } => {
227                    if let Some(v) = prop_value(expr, &env) {
228                        env.insert(*dst, v);
229                    }
230                }
231                // Opaque: only propagate if you have explicit “pure” ones (optional)
232                Stmt::Opaque { defs, op, args } => {
233                    // keep simple: do not record (safe default)
234                    let _ = (defs, op, args);
235                }
236                _ => {}
237            }
238        }
239    }
240}
241
242fn rewrite_stmt(st: &mut Stmt, env: &HashMap<VarId, Expr>) {
243    match st {
244        Stmt::Assign { expr, .. } => rewrite_expr(expr, env),
245        Stmt::Store { target, value } => {
246            rewrite_expr(target, env);
247            rewrite_expr(value, env);
248        }
249        Stmt::Update { target, rhs, .. } => {
250            rewrite_expr(target, env);
251            rewrite_expr(rhs, env);
252        }
253        Stmt::Expr(e) => rewrite_expr(e, env),
254        Stmt::Opaque { args, .. } => {
255            for a in args {
256                rewrite_expr(a, env);
257            }
258        }
259    }
260}
261
262fn rewrite_expr(e: &mut Expr, env: &HashMap<VarId, Expr>) {
263    match e {
264        Expr::SsaVar(v) => {
265            if let Some(rep) = env.get(v).cloned() {
266                *e = rep;
267            }
268        }
269        Expr::Unary(_, expr) => rewrite_expr(expr, env),
270        Expr::Binary(_, lhs, rhs) => {
271            rewrite_expr(lhs, env);
272            rewrite_expr(rhs, env);
273        }
274        Expr::Member(base, _) | Expr::Deref(base) => rewrite_expr(base, env),
275        Expr::Index(base, index) => {
276            rewrite_expr(base, env);
277            rewrite_expr(index, env);
278        }
279        Expr::Call(callee, args) => {
280            rewrite_expr(callee, env);
281            for a in args {
282                rewrite_expr(a, env);
283            }
284        }
285        Expr::New(ctor, args) => {
286            rewrite_expr(ctor, env);
287            for a in args {
288                rewrite_expr(a, env);
289            }
290        }
291        Expr::MethodCall { base, args, .. } => {
292            rewrite_expr(base, env);
293            for a in args {
294                rewrite_expr(a, env);
295            }
296        }
297        Expr::Opaque(_, args) => {
298            for a in args {
299                rewrite_expr(a, env);
300            }
301        }
302        _ => {}
303    }
304}
305
306/// Decide whether `rhs` is safe to propagate as a value.
307/// Keep it conservative: literals, variable aliases, and global/thisproxy member refs.
308fn prop_value(rhs: &Expr, env: &HashMap<VarId, Expr>) -> Option<Expr> {
309    let mut v = rhs.clone();
310    rewrite_expr(&mut v, env);
311
312    match &v {
313        // literals
314        Expr::Void
315        | Expr::Null
316        | Expr::Bool(_)
317        | Expr::Int(_)
318        | Expr::Real(_)
319        | Expr::Str(_)
320        | Expr::Octet(_) => Some(v),
321
322        // alias
323        Expr::SsaVar(_) => Some(v),
324
325        // member ref: allow base to be global/thisproxy/this or an already-propagated alias
326        Expr::Member(base, name) => {
327            let ok = matches!(
328                **base,
329                Expr::SsaVar(VarId {
330                    var: Var::Reg(-2),
331                    ..
332                }) | Expr::SsaVar(VarId {
333                    var: Var::Reg(-1),
334                    ..
335                }) // this
336            ) || is_identifier(name);
337            if ok { Some(v) } else { None }
338        }
339
340        _ => None,
341    }
342}
343
344fn emit_object_body(obj: &Tjs2Object, file: &Tjs2File, indent: usize) -> Result<(String, String)> {
345    let mut out = String::new();
346
347    let cfg = Cfg::build(obj)?;
348    let ssa = SsaProgram::from_cfg(&cfg)?;
349    let mut prog = ExprProgram::from_ssa(file, obj, &ssa)?;
350    const_propagate_intrablock(&mut prog);
351
352    let params = build_params(obj);
353    let arg_count = obj.func_decl_arg_count.max(0) as usize;
354
355    let in_class = scope_is_class(file, obj);
356    let fmt_var = make_fmt_var(in_class, arg_count);
357
358    // Recover return value from SSA: find the SRV source (it writes to r0 conceptually).
359    // We special-case: for SRV opaque stmts, propagate the arg into a synthetic ret_expr.
360    let mut ret_expr: Vec<Option<Expr>> = vec![None; prog.blocks.len()];
361    for b in &ssa.blocks {
362        // Find the last SRV instruction in this block and use its source as ret_expr.
363        let srv = b.insns.iter().rev().find(|i| {
364            i.mnemonic.eq_ignore_ascii_case("SRV") || i.mnemonic.eq_ignore_ascii_case("VM_SRV")
365        });
366        if let Some(si) = srv {
367            if let Some(v) = si.uses.get(0).copied() {
368                ret_expr[b.id] = Some(Expr::SsaVar(v));
369            }
370        }
371    }
372    propagate_into_ret_expr(&mut ret_expr, &prog);
373    remove_dead_phis(&mut prog, &ret_expr);
374    remove_dead_assigns(&mut prog, &ret_expr);
375
376    emit_var_decls(&mut out, &prog, &fmt_var, arg_count, indent)?;
377
378    let mut s = Structurer::new(&cfg, &prog, &fmt_var, ret_expr);
379    let lines = s.emit_function_body(prog.entry_block, indent);
380
381    for l in lines {
382        writeln!(out, "{}", l)?;
383    }
384    Ok((out, params))
385}
386
387/// Build param string from func_decl_arg_count (authoritative — always trust it).
388fn build_params(obj: &Tjs2Object) -> String {
389    let n = obj.func_decl_arg_count.max(0) as usize;
390    (0..n)
391        .map(|i| format!("a{}", i))
392        .collect::<Vec<_>>()
393        .join(", ")
394}
395
396/// Propagate constant assignments into ret_expr for each block.
397fn propagate_into_ret_expr(ret_expr: &mut Vec<Option<Expr>>, prog: &ExprProgram) {
398    for b in &prog.blocks {
399        let Some(re) = ret_expr.get_mut(b.id) else {
400            continue;
401        };
402        let Some(e) = re else {
403            continue;
404        };
405        let mut env: HashMap<VarId, Expr> = HashMap::new();
406        for st in &b.stmts {
407            match st {
408                Stmt::Assign { dst, expr } => {
409                    let mut v = expr.clone();
410                    rewrite_expr(&mut v, &env);
411                    if let Some(pv) = prop_value(&v, &env) {
412                        env.insert(*dst, pv);
413                    }
414                }
415                Stmt::Opaque { op, args, defs }
416                    if (op.eq_ignore_ascii_case("VM_SRV") || op.eq_ignore_ascii_case("SRV"))
417                        && !args.is_empty()
418                        && !defs.is_empty() =>
419                {
420                    let mut v = args[0].clone();
421                    rewrite_expr(&mut v, &env);
422                    if let Some(pv) = prop_value(&v, &env) {
423                        env.insert(defs[0], pv);
424                    }
425                }
426                _ => {}
427            }
428        }
429        rewrite_expr(e, &env);
430    }
431}
432
433/// Eliminate phi nodes whose results are never truly used (i.e. only consumed by other dead
434/// phis or by edge copies that feed dead phis).  After this pass, `build_edge_copies` will
435/// not emit the now-removed phi copies, and a subsequent `remove_dead_assigns` will clean up
436/// any statements that only computed values for those dead phi args.
437fn remove_dead_phis(prog: &mut ExprProgram, ret_expr: &[Option<Expr>]) {
438    // Phase 1 – seed: vars used in stmts / terminators / ret_expr (NOT phi args yet).
439    let mut live: HashSet<VarId> = HashSet::new();
440    for b in &prog.blocks {
441        for st in &b.stmts {
442            collect_uses_stmt(st, &mut live);
443        }
444        collect_vars_term(&b.term, &mut live);
445    }
446    for re in ret_expr {
447        if let Some(e) = re {
448            collect_vars_expr(e, &mut live);
449        }
450    }
451
452    // Phase 2 – propagate: if a phi result is live, its args become live.
453    let mut changed = true;
454    while changed {
455        changed = false;
456        for b in &prog.blocks {
457            for phi in &b.phi {
458                if live.contains(&phi.result) {
459                    for (_, v) in &phi.args {
460                        if live.insert(*v) {
461                            changed = true;
462                        }
463                    }
464                }
465            }
466        }
467    }
468
469    // Phase 3 – prune: drop every phi whose result is not in the live set.
470    for b in &mut prog.blocks {
471        b.phi.retain(|phi| live.contains(&phi.result));
472    }
473}
474
475/// Remove Assign statements whose dst is never referenced anywhere after constant propagation.
476fn remove_dead_assigns(prog: &mut ExprProgram, ret_expr: &[Option<Expr>]) {
477    let mut used: HashSet<VarId> = HashSet::new();
478    for b in &prog.blocks {
479        for phi in &b.phi {
480            for (_, v) in &phi.args {
481                used.insert(*v);
482            }
483        }
484        for st in &b.stmts {
485            collect_uses_stmt(st, &mut used); // only RHS / side-effect reads, not dsts
486        }
487        collect_vars_term(&b.term, &mut used);
488    }
489    for re in ret_expr {
490        if let Some(e) = re {
491            collect_vars_expr(e, &mut used);
492        }
493    }
494
495    let mut dead_dsts: HashSet<VarId> = HashSet::new();
496    for b in &prog.blocks {
497        for st in &b.stmts {
498            if let Stmt::Assign { dst, .. } = st {
499                if !used.contains(dst) {
500                    dead_dsts.insert(*dst);
501                }
502            }
503        }
504    }
505    for b in &mut prog.blocks {
506        b.stmts
507            .retain(|st| !matches!(st, Stmt::Assign { dst, .. } if dead_dsts.contains(dst)));
508    }
509}
510
511/// Collect only the "use" side of a statement (not defs of Assign).
512fn collect_uses_stmt(st: &Stmt, s: &mut HashSet<VarId>) {
513    match st {
514        Stmt::Assign { expr, .. } => collect_vars_expr(expr, s), // dst is a def, skip it
515        Stmt::Store { target, value } => {
516            collect_vars_expr(target, s);
517            collect_vars_expr(value, s);
518        }
519        Stmt::Update {
520            dst, target, rhs, ..
521        } => {
522            if let Some(d) = dst {
523                s.insert(*d); // the update result is a use-def; still mark it used
524            }
525            collect_vars_expr(target, s);
526            collect_vars_expr(rhs, s);
527        }
528        Stmt::Expr(e) => collect_vars_expr(e, s),
529        Stmt::Opaque { args, defs, .. } => {
530            for d in defs {
531                s.insert(*d);
532            }
533            for a in args {
534                collect_vars_expr(a, s);
535            }
536        }
537    }
538}
539
540fn infer_single_return_expr(file: &Tjs2File, getter_obj: &Tjs2Object) -> Option<String> {
541    let cfg = Cfg::build(getter_obj).ok()?;
542    let ssa = SsaProgram::from_cfg(&cfg).ok()?;
543    let mut prog = ExprProgram::from_ssa(file, getter_obj, &ssa).ok()?;
544    const_propagate_intrablock(&mut prog);
545
546    let in_class = scope_is_class(file, getter_obj);
547    let arg_count = getter_obj.func_decl_arg_count.max(0) as usize;
548    let fmt_var = make_fmt_var(in_class, arg_count);
549
550    // Collect SRV return expressions
551    let mut ret_expr: Vec<Option<Expr>> = vec![None; prog.blocks.len()];
552    for b in &ssa.blocks {
553        let srv = b.insns.iter().rev().find(|i| {
554            i.mnemonic.eq_ignore_ascii_case("SRV") || i.mnemonic.eq_ignore_ascii_case("VM_SRV")
555        });
556        if let Some(si) = srv {
557            if let Some(v) = si.uses.get(0).copied() {
558                ret_expr[b.id] = Some(Expr::SsaVar(v));
559            }
560        }
561    }
562    propagate_into_ret_expr(&mut ret_expr, &prog);
563
564    let mut unique_ret: Option<String> = None;
565    for re in &ret_expr {
566        if let Some(e) = re {
567            let s = e.to_tjs_with(&fmt_var);
568            if let Some(prev) = &unique_ret {
569                if *prev != s {
570                    return None;
571                }
572            } else {
573                unique_ret = Some(s);
574            }
575        }
576    }
577    unique_ret
578}
579
580pub fn dump_src_file(file: &Tjs2File) -> Result<String> {
581    let mut out = String::new();
582    writeln!(out, "// Decompiled by tjs2Decompiler (high)")?;
583    writeln!(
584        out,
585        "// objects={}, toplevel={}",
586        file.objects.len(),
587        file.toplevel
588    )?;
589    writeln!(out)?;
590
591    let toplevel = file.toplevel.max(0) as usize;
592
593    // Build parent -> children index (ordered by object index)
594    let mut children_of: HashMap<usize, Vec<usize>> = HashMap::new();
595    for obj in &file.objects {
596        if obj.parent >= 0 {
597            children_of
598                .entry(obj.parent as usize)
599                .or_default()
600                .push(obj.index);
601        }
602    }
603
604    // Track emitted objects to avoid duplication
605    let mut emitted: HashSet<usize> = HashSet::new();
606    emitted.insert(toplevel);
607
608    // === Emit classes: context_type==6 with parent==toplevel ===
609    let classes: Vec<usize> = file
610        .objects
611        .iter()
612        .filter(|o| o.context_type == 6 && o.parent == toplevel as i32)
613        .map(|o| o.index)
614        .collect();
615
616    for cls_idx in classes {
617        let cls_obj = &file.objects[cls_idx];
618        let cls_name = match cls_obj.name.as_deref() {
619            Some(n) if is_identifier(n) => n.to_string(),
620            _ => format!("__class_{}", cls_idx),
621        };
622
623        emitted.insert(cls_idx);
624
625        // extends: use super_class_getter field, or find context_type==7 child
626        let extends = if cls_obj.super_class_getter >= 0 {
627            let sg = cls_obj.super_class_getter as usize;
628            if sg < file.objects.len() {
629                emitted.insert(sg);
630                infer_single_return_expr(file, &file.objects[sg])
631            } else {
632                None
633            }
634        } else {
635            // fallback: look for context_type==7 child
636            children_of
637                .get(&cls_idx)
638                .and_then(|ch| ch.iter().find(|&&ci| file.objects[ci].context_type == 7))
639                .and_then(|&ci| {
640                    emitted.insert(ci);
641                    infer_single_return_expr(file, &file.objects[ci])
642                })
643        };
644
645        match &extends {
646            Some(e) => writeln!(out, "class {} extends {} {{", cls_name, e)?,
647            None => writeln!(out, "class {} {{", cls_name)?,
648        }
649
650        let empty_children: Vec<usize> = Vec::new();
651        let children = children_of.get(&cls_idx).unwrap_or(&empty_children).clone();
652
653        let mut first_member = true;
654        for ci in &children {
655            if emitted.contains(ci) {
656                continue;
657            }
658            let mobj = &file.objects[*ci];
659            match mobj.context_type {
660                7 => {
661                    emitted.insert(*ci);
662                } // superclass getter already handled
663                2 => {
664                    emitted.insert(*ci);
665                } // anonymous closure – skip top-level
666                3 => {
667                    // Property object
668                    emitted.insert(*ci);
669                    let prop_name = match mobj.name.as_deref() {
670                        Some(n) if n != "(anonymous)" && is_identifier(n) => n.to_string(),
671                        _ => format!("__prop_{}", ci),
672                    };
673                    if !first_member {
674                        writeln!(out)?;
675                    }
676                    first_member = false;
677                    writeln!(out, "  property {} {{", prop_name)?;
678
679                    if mobj.prop_getter >= 0 {
680                        let gi = mobj.prop_getter as usize;
681                        if gi < file.objects.len() && !emitted.contains(&gi) {
682                            // body indent=6: class(2) + property(2) + getter(2)
683                            let (body, _params) = emit_object_body(&file.objects[gi], file, 6)?;
684                            writeln!(out, "    getter() {{")?;
685                            write!(out, "{body}")?;
686                            writeln!(out, "    }}")?;
687                            emitted.insert(gi);
688                        }
689                    }
690                    if mobj.prop_setter >= 0 {
691                        let si = mobj.prop_setter as usize;
692                        if si < file.objects.len() && !emitted.contains(&si) {
693                            // body indent=6: class(2) + property(2) + setter(2)
694                            let (body, params) = emit_object_body(&file.objects[si], file, 6)?;
695                            writeln!(out, "    setter({}) {{", params)?;
696                            write!(out, "{body}")?;
697                            writeln!(out, "    }}")?;
698                            emitted.insert(si);
699                        }
700                    }
701
702                    // Mark any context_type==5 (getter) children of the property object
703                    if let Some(prop_children) = children_of.get(ci) {
704                        for &pci in prop_children {
705                            emitted.insert(pci);
706                        }
707                    }
708
709                    writeln!(out, "  }}")?;
710                }
711                1 => {
712                    // Method
713                    let mname = match mobj.name.as_deref() {
714                        Some(n) if n != "(anonymous)" && is_identifier(n) => n.to_string(),
715                        _ => format!("__method_{}", ci),
716                    };
717                    emitted.insert(*ci);
718                    // Mark anonymous children (closures inside this method)
719                    if let Some(mch) = children_of.get(ci) {
720                        for &mci in mch {
721                            if file.objects[mci].context_type == 2 {
722                                emitted.insert(mci);
723                            }
724                        }
725                    }
726
727                    if !first_member {
728                        writeln!(out)?;
729                    }
730                    first_member = false;
731
732                    if mobj.code.is_empty() {
733                        writeln!(out, "  function {}() {{}}", mname)?;
734                    } else {
735                        // body indent=4: class(2) + method(2)
736                        let (body, params) = emit_object_body(mobj, file, 4)?;
737                        writeln!(out, "  function {}({}) {{", mname, params)?;
738                        write!(out, "{body}")?;
739                        writeln!(out, "  }}")?;
740                    }
741                }
742                _ => {
743                    emitted.insert(*ci);
744                }
745            }
746        }
747
748        writeln!(out, "}}")?;
749        writeln!(out)?;
750    }
751
752    // === Emit top-level functions: context_type==1 with parent==toplevel ===
753    for obj in &file.objects {
754        if emitted.contains(&obj.index) {
755            continue;
756        }
757        if obj.parent != toplevel as i32 {
758            continue;
759        }
760        if obj.context_type != 1 {
761            continue;
762        }
763        if obj.code.is_empty() {
764            continue;
765        }
766
767        let name = match obj.name.as_deref() {
768            Some(n) if n != "(anonymous)" && is_identifier(n) => n.to_string(),
769            _ => format!("__func_{}", obj.index),
770        };
771        emitted.insert(obj.index);
772
773        // Mark anonymous closure children
774        if let Some(ch) = children_of.get(&obj.index) {
775            for &ci in ch {
776                if file.objects[ci].context_type == 2 {
777                    emitted.insert(ci);
778                }
779            }
780        }
781
782        // body indent=2: top-level function body is one level deep
783        let (body, params) = emit_object_body(obj, file, 2)?;
784        writeln!(out, "function {}({}) {{", name, params)?;
785        write!(out, "{body}")?;
786        writeln!(out, "}}")?;
787        writeln!(out)?;
788    }
789
790    Ok(out)
791}
792
793/* ------------------------- structuring ------------------------- */
794
795#[derive(Clone)]
796struct LoopCtx {
797    header: usize,
798    exit: Option<usize>,
799}
800
801#[derive(Clone, Copy)]
802struct RegionOutcome {
803    falls_through: bool,
804}
805
806struct Structurer<'a> {
807    cfg: &'a Cfg,
808    prog: &'a ExprProgram,
809    fmt_var: &'a dyn Fn(VarId) -> String,
810
811    // (pred, succ) -> list of (phi_result, incoming_value)
812    edge_copies: HashMap<(usize, usize), Vec<(VarId, VarId)>>,
813
814    // dominators / postdominators on reachable blocks
815    dom: Vec<HashSet<usize>>,
816    pdom: Vec<HashSet<usize>>,
817    ipdom: Vec<Option<usize>>,
818
819    // loop header -> natural loop node set
820    loops: HashMap<usize, HashSet<usize>>,
821
822    emitted: HashSet<usize>,
823
824    // return expression per block (from SSA)
825    ret_expr: Vec<Option<Expr>>,
826    uses_rv: bool,
827}
828
829impl<'a> Structurer<'a> {
830    fn new(
831        cfg: &'a Cfg,
832        prog: &'a ExprProgram,
833        fmt_var: &'a dyn Fn(VarId) -> String,
834        ret_expr: Vec<Option<Expr>>,
835    ) -> Self {
836        let edge_copies = build_edge_copies(prog);
837        let reachable = compute_reachable(prog, prog.entry_block);
838
839        let dom = compute_dominators(prog, prog.entry_block, &reachable);
840        let pdom = compute_postdominators(prog, &reachable);
841        let ipdom = compute_ipdom(&pdom);
842
843        let loops = compute_natural_loops(prog, &dom, &reachable);
844
845        let uses_rv = prog.blocks.iter().any(|b| {
846            b.stmts.iter().any(|st| {
847                matches!(
848                    st,
849                    Stmt::Opaque { op, .. }
850                        if op.eq_ignore_ascii_case("SRV") || op.eq_ignore_ascii_case("VM_SRV")
851                )
852            })
853        });
854
855        Self {
856            cfg,
857            prog,
858            fmt_var,
859            edge_copies,
860            dom,
861            pdom,
862            ipdom,
863            loops,
864            emitted: HashSet::new(),
865            ret_expr,
866            uses_rv,
867        }
868    }
869
870    fn emit_function_body(&mut self, entry: usize, indent: usize) -> Vec<String> {
871        let mut lines = Vec::new();
872        let _ = self.emit_seq(entry, None, indent, None, &mut lines);
873        // Unreachable blocks are silently omitted — no goto/state-machine fallback.
874        simplify_empty_if_then(&mut lines);
875        lines
876    }
877
878    fn emit_seq(
879        &mut self,
880        mut cur: usize,
881        stop: Option<usize>,
882        indent: usize,
883        loop_ctx: Option<LoopCtx>,
884        out: &mut Vec<String>,
885    ) -> RegionOutcome {
886        while Some(cur) != stop {
887            let loop_ctx = loop_ctx.clone();
888            if self.emitted.contains(&cur) {
889                return RegionOutcome {
890                    falls_through: true,
891                };
892            }
893
894            if self.is_loop_header(cur) && stop != Some(cur) {
895                let oc = self.emit_loop(cur, indent, out);
896                if let Some(n) = self.loop_exit(cur) {
897                    cur = n;
898                    continue;
899                }
900                return oc;
901            }
902
903            self.emitted.insert(cur);
904
905            self.emit_block_stmts(cur, indent, out);
906
907            let blk = &self.prog.blocks[cur];
908            match blk.term.clone() {
909                Terminator::Ret => {
910                    if let Some(e) = self.ret_expr.get(cur).and_then(|x| x.clone()) {
911                        let s = self.expr_to_tjs(&e);
912                        if s == "void" || s == "r0_0" {
913                            out.push(format!("{}return;", " ".repeat(indent)));
914                        } else {
915                            out.push(format!("{}return {};", " ".repeat(indent), s));
916                        }
917                    } else {
918                        out.push(format!("{}return;", " ".repeat(indent)));
919                    }
920                    return RegionOutcome {
921                        falls_through: false,
922                    };
923                }
924                Terminator::Throw(e) => {
925                    out.push(format!(
926                        "{}throw {};",
927                        " ".repeat(indent),
928                        self.expr_to_tjs(&e)
929                    ));
930                    return RegionOutcome {
931                        falls_through: false,
932                    };
933                }
934                Terminator::Exit | Terminator::Fallthrough => {
935                    if let Some(n) = blk.succ.get(0).copied() {
936                        self.emit_edge_copies(cur, n, indent, out);
937                        cur = n;
938                        continue;
939                    }
940                    out.push(format!("{}return;", " ".repeat(indent)));
941                    return RegionOutcome {
942                        falls_through: false,
943                    };
944                }
945                Terminator::Jmp(t) => {
946                    if let Some(ctx) = loop_ctx.clone() {
947                        if t == ctx.header {
948                            self.emit_edge_copies(cur, t, indent, out);
949                            out.push(format!("{}continue;", " ".repeat(indent)));
950                            return RegionOutcome {
951                                falls_through: false,
952                            };
953                        }
954                        if ctx.exit == Some(t) {
955                            self.emit_edge_copies(cur, t, indent, out);
956                            out.push(format!("{}break;", " ".repeat(indent)));
957                            return RegionOutcome {
958                                falls_through: false,
959                            };
960                        }
961                    }
962                    if stop == Some(t) {
963                        self.emit_edge_copies(cur, t, indent, out);
964                        return RegionOutcome {
965                            falls_through: true,
966                        };
967                    }
968                    self.emit_edge_copies(cur, t, indent, out);
969                    cur = t;
970                    continue;
971                }
972                Terminator::Br {
973                    cond,
974                    if_true,
975                    if_false,
976                } => {
977                    // If this branch is a loop-exit/continue inside a loop body, prioritize break/continue patterns.
978                    if let Some(ctx) = loop_ctx.clone() {
979                        if if_true == ctx.header
980                            || if_false == ctx.header
981                            || ctx.exit == Some(if_true)
982                            || ctx.exit == Some(if_false)
983                        {
984                            let oc = self.emit_branch_in_loop(
985                                cur, &cond, if_true, if_false, indent, ctx, out,
986                            );
987                            return oc;
988                        }
989                    }
990
991                    let join = self.ipdom.get(cur).and_then(|x| *x).or(stop);
992
993                    // If the then-branch is trivially empty but else is not, negate and swap
994                    // to avoid emitting `if (cond) { } else { ... }`.
995                    let (cond_emitted, first_succ, second_succ) = if self
996                        .branch_is_trivially_empty(cur, if_true, join)
997                        && !self.branch_is_trivially_empty(cur, if_false, join)
998                    {
999                        (Expr::Unary(UnOp::Not, Box::new(cond)), if_false, if_true)
1000                    } else {
1001                        (cond, if_true, if_false)
1002                    };
1003
1004                    out.push(format!(
1005                        "{}if ({}) {{",
1006                        " ".repeat(indent),
1007                        self.expr_to_tjs(&cond_emitted)
1008                    ));
1009
1010                    // then (primary branch)
1011                    self.emit_edge_copies(cur, first_succ, indent + 2, out);
1012                    let then_oc =
1013                        self.emit_seq(first_succ, join, indent + 2, loop_ctx.clone(), out);
1014                    out.push(format!("{}}}", " ".repeat(indent)));
1015
1016                    // else (secondary branch) — omit the else block when trivially empty
1017                    let second_is_empty = self.branch_is_trivially_empty(cur, second_succ, join);
1018                    let else_oc = if second_is_empty {
1019                        self.mark_chain_emitted(second_succ, join);
1020                        RegionOutcome {
1021                            falls_through: true,
1022                        }
1023                    } else {
1024                        out.push(format!("{}else {{", " ".repeat(indent)));
1025                        self.emit_edge_copies(cur, second_succ, indent + 2, out);
1026                        let oc = self.emit_seq(second_succ, join, indent + 2, loop_ctx, out);
1027                        out.push(format!("{}}}", " ".repeat(indent)));
1028                        oc
1029                    };
1030
1031                    if let Some(j) = join {
1032                        if then_oc.falls_through || else_oc.falls_through {
1033                            cur = j;
1034                            continue;
1035                        }
1036                        return RegionOutcome {
1037                            falls_through: false,
1038                        };
1039                    }
1040                    return RegionOutcome {
1041                        falls_through: then_oc.falls_through || else_oc.falls_through,
1042                    };
1043                }
1044            }
1045        }
1046
1047        RegionOutcome {
1048            falls_through: true,
1049        }
1050    }
1051
1052    fn emit_branch_in_loop(
1053        &mut self,
1054        cur: usize,
1055        cond: &Expr,
1056        t: usize,
1057        f: usize,
1058        indent: usize,
1059        ctx: LoopCtx,
1060        out: &mut Vec<String>,
1061    ) -> RegionOutcome {
1062        // Pattern:
1063        // if (cond) { ... } else { ... }
1064        // but allow branches to be break/continue.
1065        out.push(format!(
1066            "{}if ({}) {{",
1067            " ".repeat(indent),
1068            self.expr_to_tjs(cond)
1069        ));
1070
1071        self.emit_edge_copies(cur, t, indent + 2, out);
1072        let then_oc = self.emit_seq(t, None, indent + 2, Some(ctx.clone()), out);
1073        out.push(format!("{}}}", " ".repeat(indent)));
1074
1075        out.push(format!("{}else {{", " ".repeat(indent)));
1076        self.emit_edge_copies(cur, f, indent + 2, out);
1077        let else_oc = self.emit_seq(f, None, indent + 2, Some(ctx), out);
1078        out.push(format!("{}}}", " ".repeat(indent)));
1079
1080        RegionOutcome {
1081            falls_through: then_oc.falls_through || else_oc.falls_through,
1082        }
1083    }
1084
1085    fn is_loop_header(&self, h: usize) -> bool {
1086        self.loops.contains_key(&h)
1087    }
1088
1089    fn loop_exit(&self, h: usize) -> Option<usize> {
1090        let body = self.loops.get(&h)?;
1091        let blk = &self.prog.blocks[h];
1092        for &s in &blk.succ {
1093            if !body.contains(&s) {
1094                return Some(s);
1095            }
1096        }
1097        None
1098    }
1099
1100    fn emit_loop(&mut self, header: usize, indent: usize, out: &mut Vec<String>) -> RegionOutcome {
1101        let body_nodes = match self.loops.get(&header) {
1102            Some(s) => s.clone(),
1103            None => {
1104                return RegionOutcome {
1105                    falls_through: true,
1106                };
1107            }
1108        };
1109
1110        // Choose loop exit as header successor not in loop set.
1111        let exit = self.loop_exit(header);
1112
1113        out.push(format!("{}while (true) {{", " ".repeat(indent)));
1114
1115        // Emit header statements inside loop.
1116        self.emit_block_stmts(header, indent + 2, out);
1117
1118        // Handle header terminator as loop guard / dispatch.
1119        let blk = &self.prog.blocks[header];
1120        match blk.term.clone() {
1121            Terminator::Br {
1122                cond,
1123                if_true,
1124                if_false,
1125            } => {
1126                // Decide which successor stays in loop.
1127                let t_in = body_nodes.contains(&if_true);
1128                let f_in = body_nodes.contains(&if_false);
1129
1130                if exit.is_some() && (t_in ^ f_in) {
1131                    let (body_succ, exit_succ, break_on_true) = if t_in {
1132                        (if_true, if_false, false)
1133                    } else {
1134                        (if_false, if_true, true)
1135                    };
1136
1137                    if break_on_true {
1138                        // if (cond) { copies; break; }
1139                        out.push(format!(
1140                            "{}if ({}) {{",
1141                            " ".repeat(indent + 2),
1142                            self.expr_to_tjs(&cond)
1143                        ));
1144                        self.emit_edge_copies(header, exit_succ, indent + 4, out);
1145                        out.push(format!("{}break;", " ".repeat(indent + 4)));
1146                        out.push(format!("{}}}", " ".repeat(indent + 2)));
1147                    } else {
1148                        // if (!cond) { copies; break; }
1149                        let ncond = Expr::Unary(UnOp::Not, Box::new(cond));
1150                        out.push(format!(
1151                            "{}if ({}) {{",
1152                            " ".repeat(indent + 2),
1153                            self.expr_to_tjs(&ncond)
1154                        ));
1155                        self.emit_edge_copies(header, exit_succ, indent + 4, out);
1156                        out.push(format!("{}break;", " ".repeat(indent + 4)));
1157                        out.push(format!("{}}}", " ".repeat(indent + 2)));
1158                    }
1159
1160                    // fall into body
1161                    self.emit_edge_copies(header, body_succ, indent + 2, out);
1162                    let _ = self.emit_seq(
1163                        body_succ,
1164                        Some(header),
1165                        indent + 2,
1166                        Some(LoopCtx { header, exit }),
1167                        out,
1168                    );
1169                } else {
1170                    // Fallback: still emit both arms inside loop (no goto/state machine).
1171                    out.push(format!(
1172                        "{}if ({}) {{",
1173                        " ".repeat(indent + 2),
1174                        self.expr_to_tjs(&cond)
1175                    ));
1176                    self.emit_edge_copies(header, if_true, indent + 4, out);
1177                    let _ = self.emit_seq(
1178                        if_true,
1179                        Some(header),
1180                        indent + 4,
1181                        Some(LoopCtx { header, exit }),
1182                        out,
1183                    );
1184                    out.push(format!("{}}}", " ".repeat(indent + 2)));
1185                    out.push(format!("{}else {{", " ".repeat(indent + 2)));
1186                    self.emit_edge_copies(header, if_false, indent + 4, out);
1187                    let _ = self.emit_seq(
1188                        if_false,
1189                        Some(header),
1190                        indent + 4,
1191                        Some(LoopCtx { header, exit }),
1192                        out,
1193                    );
1194                    out.push(format!("{}}}", " ".repeat(indent + 2)));
1195                }
1196            }
1197            Terminator::Jmp(t) => {
1198                if t == header {
1199                    out.push(format!("{}continue;", " ".repeat(indent + 2)));
1200                } else {
1201                    self.emit_edge_copies(header, t, indent + 2, out);
1202                    let _ = self.emit_seq(
1203                        t,
1204                        Some(header),
1205                        indent + 2,
1206                        Some(LoopCtx { header, exit }),
1207                        out,
1208                    );
1209                }
1210            }
1211            Terminator::Ret => {
1212                if let Some(e) = self.ret_expr.get(header).and_then(|x| x.clone()) {
1213                    let s = self.expr_to_tjs(&e);
1214                    if s == "void" || s == "r0_0" {
1215                        out.push(format!("{}return;", " ".repeat(indent + 2)));
1216                    } else {
1217                        out.push(format!("{}return {};", " ".repeat(indent + 2), s));
1218                    }
1219                } else {
1220                    out.push(format!("{}return;", " ".repeat(indent + 2)));
1221                }
1222            }
1223            Terminator::Throw(e) => {
1224                out.push(format!(
1225                    "{}throw {};",
1226                    " ".repeat(indent + 2),
1227                    self.expr_to_tjs(&e)
1228                ));
1229            }
1230            Terminator::Exit | Terminator::Fallthrough => {
1231                if let Some(n) = blk.succ.get(0).copied() {
1232                    self.emit_edge_copies(header, n, indent + 2, out);
1233                    let _ = self.emit_seq(
1234                        n,
1235                        Some(header),
1236                        indent + 2,
1237                        Some(LoopCtx { header, exit }),
1238                        out,
1239                    );
1240                } else {
1241                    out.push(format!("{}return;", " ".repeat(indent + 2)));
1242                }
1243            }
1244        }
1245
1246        out.push(format!("{}}}", " ".repeat(indent)));
1247
1248        // Mark all nodes in this loop as emitted (except those already).
1249        for n in body_nodes {
1250            self.emitted.insert(n);
1251        }
1252        self.emitted.insert(header);
1253
1254        RegionOutcome {
1255            falls_through: exit.is_some(),
1256        }
1257    }
1258
1259    /// Returns true when branching from `pred` to `succ` (with `stop` as the region limit)
1260    /// would emit zero lines: no non-trivial edge copies, no block statements, and every block
1261    /// in the single-successor chain eventually falls to `stop` (follows Jmp/Fallthrough/Exit
1262    /// only, up to `depth` hops, with cycle detection).
1263    fn branch_is_trivially_empty(&self, pred: usize, succ: usize, stop: Option<usize>) -> bool {
1264        let mut visited = HashSet::new();
1265        self.chain_is_empty(pred, succ, stop, &mut visited, 16)
1266    }
1267
1268    fn chain_is_empty(
1269        &self,
1270        pred: usize,
1271        succ: usize,
1272        stop: Option<usize>,
1273        visited: &mut HashSet<usize>,
1274        depth: usize,
1275    ) -> bool {
1276        // Always check edge copies from pred→succ first (including when succ==stop),
1277        // so that live phi edge copies on the final hop are not silently skipped.
1278        if let Some(xs) = self.edge_copies.get(&(pred, succ)) {
1279            for (d, s) in xs {
1280                if (self.fmt_var)(*d) != (self.fmt_var)(*s) {
1281                    return false;
1282                }
1283            }
1284        }
1285        if Some(succ) == stop {
1286            return true;
1287        }
1288        if depth == 0 || !visited.insert(succ) {
1289            return false;
1290        }
1291        let blk = &self.prog.blocks[succ];
1292        // Any non-control stmt → not empty.
1293        for st in &blk.stmts {
1294            if !matches!(st, Stmt::Opaque { op, .. } if is_control_op(op)) {
1295                return false;
1296            }
1297        }
1298        // Follow single-successor terminators only.
1299        match &blk.term {
1300            Terminator::Jmp(t) => self.chain_is_empty(succ, *t, stop, visited, depth - 1),
1301            Terminator::Fallthrough | Terminator::Exit => match blk.succ.get(0).copied() {
1302                Some(t) => self.chain_is_empty(succ, t, stop, visited, depth - 1),
1303                None => stop.is_none(),
1304            },
1305            _ => false,
1306        }
1307    }
1308
1309    /// Mark all blocks in the single-successor chain from `succ` up to (but not including)
1310    /// `stop` as emitted.  Called when we skip an empty branch entirely.
1311    fn mark_chain_emitted(&mut self, succ: usize, stop: Option<usize>) {
1312        let mut cur = succ;
1313        loop {
1314            if Some(cur) == stop || !self.emitted.insert(cur) {
1315                break;
1316            }
1317            let blk = &self.prog.blocks[cur];
1318            match &blk.term {
1319                Terminator::Jmp(t) => cur = *t,
1320                Terminator::Fallthrough | Terminator::Exit => match blk.succ.get(0).copied() {
1321                    Some(t) => cur = t,
1322                    None => break,
1323                },
1324                _ => break,
1325            }
1326        }
1327    }
1328
1329    fn emit_block_stmts(&self, bid: usize, indent: usize, out: &mut Vec<String>) {
1330        let blk = &self.prog.blocks[bid];
1331        for st in &blk.stmts {
1332            if let Stmt::Opaque { op, .. } = st {
1333                if is_control_op(op) {
1334                    continue;
1335                }
1336            }
1337            let s = self.stmt_to_tjs(st);
1338            if s.is_empty() || s == "// (control op omitted)" {
1339                continue;
1340            }
1341            out.push(format!("{}{}", " ".repeat(indent), s));
1342        }
1343    }
1344
1345    fn emit_edge_copies(&self, pred: usize, succ: usize, indent: usize, out: &mut Vec<String>) {
1346        if let Some(xs) = self.edge_copies.get(&(pred, succ)) {
1347            for (dst, src) in xs {
1348                let d = (self.fmt_var)(*dst);
1349                // r0_0 (initial void of result register) renders as "void".
1350                let s = if src.var == Var::Reg(0) && src.ver == 0 {
1351                    "void".to_string()
1352                } else {
1353                    (self.fmt_var)(*src)
1354                };
1355                if d == s {
1356                    continue; // skip self-assignments
1357                }
1358                out.push(format!("{}{} = {};", " ".repeat(indent), d, s));
1359            }
1360        }
1361    }
1362
1363    fn stmt_to_tjs(&self, st: &Stmt) -> String {
1364        match st {
1365            Stmt::Assign { dst, expr } => {
1366                format!("{} = {};", (self.fmt_var)(*dst), self.expr_to_tjs(expr))
1367            }
1368            Stmt::Store { target, value } => {
1369                format!(
1370                    "{} = {};",
1371                    self.expr_to_tjs(target),
1372                    self.expr_to_tjs(value)
1373                )
1374            }
1375            Stmt::Update {
1376                dst,
1377                target,
1378                op,
1379                rhs,
1380            } => {
1381                if let Some(comp) = to_compound_assign(*op) {
1382                    if let Some(d) = dst {
1383                        format!(
1384                            "{} = ({} {} {});",
1385                            (self.fmt_var)(*d),
1386                            self.expr_to_tjs(target),
1387                            comp.op_str(),
1388                            self.expr_to_tjs(rhs)
1389                        )
1390                    } else {
1391                        format!(
1392                            "{} {} {};",
1393                            self.expr_to_tjs(target),
1394                            comp.op_str(),
1395                            self.expr_to_tjs(rhs)
1396                        )
1397                    }
1398                } else {
1399                    if let Some(d) = dst {
1400                        format!(
1401                            "{} = ({} = ({} {} {}));",
1402                            (self.fmt_var)(*d),
1403                            self.expr_to_tjs(target),
1404                            self.expr_to_tjs(target),
1405                            op.op_str(),
1406                            self.expr_to_tjs(rhs)
1407                        )
1408                    } else {
1409                        format!(
1410                            "{} = ({} {} {});",
1411                            self.expr_to_tjs(target),
1412                            self.expr_to_tjs(target),
1413                            op.op_str(),
1414                            self.expr_to_tjs(rhs)
1415                        )
1416                    }
1417                }
1418            }
1419            Stmt::Expr(e) => format!("{};", self.expr_to_tjs(e)),
1420            Stmt::Opaque { op, args, defs } => {
1421                match op.to_string().as_str() {
1422                    "JF" | "JNF" | "JMP" | "RET" | "THROW" | "ENTRY" | "EXTRY" | "VM_JF"
1423                    | "VM_JNF" | "VM_JMP" | "VM_RET" | "VM_THROW" | "VM_ENTRY" | "VM_EXTRY" => {
1424                        return "// (control op omitted)".to_string();
1425                    }
1426                    _ => {}
1427                }
1428                let op_name = op.to_string();
1429                if op_name == "VM_CHGTHIS" || op_name == "CHGTHIS" {
1430                    return "// (this-change op omitted)".to_string();
1431                }
1432
1433                if (op_name == "VM_TYPEOFD"
1434                    || op_name == "TYPEOFD"
1435                    || op_name == "VM_TYPEOF"
1436                    || op_name == "TYPEOF")
1437                    && args.len() == 1
1438                {
1439                    let x = args[0].to_tjs_with(self.fmt_var);
1440                    let expr = format!("(typeof {})", x);
1441
1442                    if defs.is_empty() {
1443                        return format!("{};", expr);
1444                    } else if defs.len() == 1 {
1445                        return format!("{} = {};", (self.fmt_var)(defs[0]), expr);
1446                    } else {
1447                        let mut s = String::new();
1448                        let _ = write!(&mut s, "{{ var __t = {}; ", expr);
1449                        for (i, d) in defs.iter().enumerate() {
1450                            let _ = write!(&mut s, "{} = __t[{}]; ", (self.fmt_var)(*d), i);
1451                        }
1452                        let _ = write!(&mut s, "}}");
1453                        return s;
1454                    }
1455                }
1456
1457                if op_name == "VM_SRV" || op_name == "SRV" {
1458                    // SRV is represented by ret_expr in the Structurer; suppress inline output.
1459                    return String::new();
1460                }
1461
1462                if (op_name == "VM_NUM" || op_name == "NUM") && args.len() == 1 {
1463                    let x = args[0].to_tjs_with(self.fmt_var);
1464
1465                    let expr = format!("real({})", x);
1466
1467                    if defs.len() == 1 {
1468                        return format!("{} = {};", (self.fmt_var)(defs[0]), expr);
1469                    } else {
1470                        return format!("{};", expr);
1471                    }
1472                }
1473
1474                if (op_name.starts_with("VM_STR") || op_name == "STR") && args.len() == 1 {
1475                    let x = args[0].to_tjs_with(self.fmt_var);
1476                    let expr = format!("string({})", x);
1477
1478                    if defs.len() == 1 {
1479                        return format!("{} = {};", (self.fmt_var)(defs[0]), expr);
1480                    } else {
1481                        return format!("{};", expr);
1482                    }
1483                }
1484
1485                if op_name == "VM_CHGTHIS" || op_name == "CHGTHIS" {
1486                    if args.len() == 2 {
1487                        return format!(
1488                            "chgthis({}, {});",
1489                            args[0].to_tjs_with(self.fmt_var),
1490                            args[1].to_tjs_with(self.fmt_var),
1491                        );
1492                    }
1493                    return "// chgthis();".to_string();
1494                }
1495
1496                if op_name.starts_with("VM_REGMEMBER") && args.len() == 3 {
1497                    return format!(
1498                        "{}.{} = {};",
1499                        args[0].to_tjs_with(self.fmt_var),
1500                        args[1].to_tjs_with(self.fmt_var),
1501                        args[2].to_tjs_with(self.fmt_var)
1502                    );
1503                }
1504
1505                if op_name.starts_with("VM_INV") && args.len() >= 2 {
1506                    let recv = args[0].to_tjs_with(self.fmt_var);
1507                    let method = args[1].to_tjs_with(self.fmt_var);
1508                    let call_args = args
1509                        .iter()
1510                        .skip(2)
1511                        .map(|x| x.to_tjs_with(self.fmt_var))
1512                        .collect::<Vec<_>>()
1513                        .join(", ");
1514                    let call = format!("{}.{}({})", recv, method, call_args);
1515                    if defs.len() == 1 {
1516                        return format!("{} = {};", (self.fmt_var)(defs[0]), call);
1517                    } else {
1518                        return format!("{};", call);
1519                    }
1520                }
1521
1522                let call = if args.is_empty() {
1523                    format!("{}()", op)
1524                } else {
1525                    // let mut s = String::new();
1526                    // s.push_str(op);
1527                    // s.push('(');
1528                    // for (i, a) in args.iter().enumerate() {
1529                    //     if i != 0 {
1530                    //         s.push_str(", ");
1531                    //     }
1532                    //     s.push_str(&self.expr_to_tjs(a));
1533                    // }
1534                    // s.push(')');
1535
1536                    let a0 = args.get(0).map(|x| x.to_tjs_with(self.fmt_var));
1537                    let a1 = args.get(1).map(|x| x.to_tjs_with(self.fmt_var));
1538
1539                    let opname = op;
1540
1541                    let call = if let (Some(x), Some(y)) = (a0.as_deref(), a1.as_deref()) {
1542                        // binary families (cover D/I/P variants by starts_with)
1543                        if opname.starts_with("VM_ADD") {
1544                            format!("({} + {})", x, y)
1545                        } else if opname.starts_with("VM_SUB") {
1546                            format!("({} - {})", x, y)
1547                        } else if opname.starts_with("VM_MUL") {
1548                            format!("({} * {})", x, y)
1549                        } else if opname.starts_with("VM_DIV") {
1550                            format!("({} / {})", x, y)
1551                        } else if opname.starts_with("VM_IDIV") {
1552                            format!("({} \\ {})", x, y)
1553                        } else if opname.starts_with("VM_MOD") {
1554                            format!("({} % {})", x, y)
1555                        } else if opname.starts_with("VM_SAL") {
1556                            format!("({} << {})", x, y)
1557                        } else if opname.starts_with("VM_SAR") {
1558                            format!("({} >> {})", x, y)
1559                        } else if opname.starts_with("VM_SR") {
1560                            format!("({} >>> {})", x, y)
1561                        } else if opname.starts_with("VM_BAND") {
1562                            format!("({} & {})", x, y)
1563                        } else if opname.starts_with("VM_BXOR") {
1564                            format!("({} ^ {})", x, y)
1565                        } else if opname.starts_with("VM_BOR") {
1566                            format!("({} | {})", x, y)
1567                        } else if opname.starts_with("VM_LAND") {
1568                            format!("({} && {})", x, y)
1569                        } else if opname.starts_with("VM_LOR") {
1570                            format!("({} || {})", x, y)
1571                        } else if opname.starts_with("VM_EQ") {
1572                            format!("({} == {})", x, y)
1573                        } else if opname.starts_with("VM_NE") {
1574                            format!("({} != {})", x, y)
1575                        } else if opname.starts_with("VM_DEQ") {
1576                            format!("({} === {})", x, y)
1577                        } else if opname.starts_with("VM_DNE") {
1578                            format!("({} !== {})", x, y)
1579                        } else if opname.starts_with("VM_LT") {
1580                            format!("({} < {})", x, y)
1581                        } else if opname.starts_with("VM_LE") {
1582                            format!("({} <= {})", x, y)
1583                        } else if opname.starts_with("VM_GT") {
1584                            format!("({} > {})", x, y)
1585                        } else if opname.starts_with("VM_GE") {
1586                            format!("({} >= {})", x, y)
1587                        } else if opname.to_string() == "CHKINS" || opname.starts_with("VM_IN") {
1588                            format!("({} in {})", x, y)
1589                        } else {
1590                            // fallback to original call form
1591                            let mut s = String::new();
1592                            s.push_str(op);
1593                            s.push('(');
1594                            for (i, a) in args.iter().enumerate() {
1595                                if i != 0 {
1596                                    s.push_str(", ");
1597                                }
1598                                s.push_str(&a.to_tjs_with(self.fmt_var));
1599                            }
1600                            s.push(')');
1601                            s
1602                        }
1603                    } else if let Some(x) = a0.as_deref() {
1604                        // unary families (also cover variants)
1605                        if opname.starts_with("VM_CHS") {
1606                            format!("(-{})", x)
1607                        } else if opname.starts_with("VM_LNOT") {
1608                            format!("(!{})", x)
1609                        } else if opname.starts_with("VM_BNOT") {
1610                            format!("(~{})", x)
1611                        } else if opname.starts_with("VM_TYPEOF") {
1612                            format!("(typeof {})", x)
1613                        } else if opname.starts_with("VM_DELETE") {
1614                            format!("(delete {})", x)
1615                        } else if opname.starts_with("VM_INC") {
1616                            format!("({} + 1)", x)
1617                        } else if opname.starts_with("VM_DEC") {
1618                            format!("({} - 1)", x)
1619                        } else {
1620                            // fallback
1621                            let mut s = String::new();
1622                            s.push_str(op);
1623                            s.push('(');
1624                            for (i, a) in args.iter().enumerate() {
1625                                if i != 0 {
1626                                    s.push_str(", ");
1627                                }
1628                                s.push_str(&a.to_tjs_with(self.fmt_var));
1629                            }
1630                            s.push(')');
1631                            s
1632                        }
1633                    } else {
1634                        format!("{}()", op)
1635                    };
1636
1637                    call
1638                };
1639
1640                if defs.is_empty() {
1641                    format!("{};", call)
1642                } else if defs.len() == 1 {
1643                    format!("{} = {};", (self.fmt_var)(defs[0]), call)
1644                } else {
1645                    // Multiple defs: use a temp array-like value.
1646                    // Still no helper functions; just structured, explicit assignments.
1647                    let mut s = String::new();
1648                    s.push_str("{ ");
1649                    s.push_str("var __t = ");
1650                    s.push_str(&call);
1651                    s.push_str("; ");
1652                    for (i, d) in defs.iter().enumerate() {
1653                        let _ = write!(&mut s, "{} = __t[{}]; ", (self.fmt_var)(*d), i);
1654                    }
1655                    s.push_str("}");
1656                    s
1657                }
1658            }
1659        }
1660    }
1661
1662    fn expr_to_tjs(&self, e: &Expr) -> String {
1663        e.to_tjs_with(self.fmt_var)
1664    }
1665}
1666
1667/* ------------------------- utilities ------------------------- */
1668
1669fn build_edge_copies(prog: &ExprProgram) -> HashMap<(usize, usize), Vec<(VarId, VarId)>> {
1670    let mut m: HashMap<(usize, usize), Vec<(VarId, VarId)>> = HashMap::new();
1671    for b in &prog.blocks {
1672        for phi in &b.phi {
1673            for (pred, v) in &phi.args {
1674                m.entry((*pred, b.id)).or_default().push((phi.result, *v));
1675            }
1676        }
1677    }
1678    m
1679}
1680
1681fn compute_reachable(prog: &ExprProgram, entry: usize) -> HashSet<usize> {
1682    let mut seen = HashSet::new();
1683    let mut stack = vec![entry];
1684    while let Some(n) = stack.pop() {
1685        if !seen.insert(n) {
1686            continue;
1687        }
1688        for &s in &prog.blocks[n].succ {
1689            stack.push(s);
1690        }
1691    }
1692    seen
1693}
1694
1695fn compute_dominators(
1696    prog: &ExprProgram,
1697    entry: usize,
1698    reachable: &HashSet<usize>,
1699) -> Vec<HashSet<usize>> {
1700    let n = prog.blocks.len();
1701    let all: HashSet<usize> = (0..n).filter(|x| reachable.contains(x)).collect();
1702
1703    let mut dom = vec![HashSet::new(); n];
1704    for b in 0..n {
1705        if !reachable.contains(&b) {
1706            continue;
1707        }
1708        if b == entry {
1709            dom[b].insert(entry);
1710        } else {
1711            dom[b] = all.clone();
1712        }
1713    }
1714
1715    let mut changed = true;
1716    while changed {
1717        changed = false;
1718        for b in 0..n {
1719            if !reachable.contains(&b) || b == entry {
1720                continue;
1721            }
1722            let preds = &prog.blocks[b].pred;
1723            if preds.is_empty() {
1724                continue;
1725            }
1726            let mut nd = all.clone();
1727            for &p in preds {
1728                if !reachable.contains(&p) {
1729                    continue;
1730                }
1731                nd = nd
1732                    .intersection(&dom[p])
1733                    .copied()
1734                    .collect::<HashSet<usize>>();
1735            }
1736            nd.insert(b);
1737            if nd != dom[b] {
1738                dom[b] = nd;
1739                changed = true;
1740            }
1741        }
1742    }
1743    dom
1744}
1745
1746fn compute_postdominators(prog: &ExprProgram, reachable: &HashSet<usize>) -> Vec<HashSet<usize>> {
1747    let n = prog.blocks.len();
1748    let all: HashSet<usize> = (0..n).filter(|x| reachable.contains(x)).collect();
1749
1750    let exits: HashSet<usize> = (0..n)
1751        .filter(|b| {
1752            if !reachable.contains(b) {
1753                return false;
1754            }
1755            matches!(
1756                prog.blocks[*b].term,
1757                Terminator::Ret | Terminator::Throw(_) // Exit/Fallthrough with no succ also treated later
1758            ) || prog.blocks[*b].succ.is_empty()
1759        })
1760        .collect();
1761
1762    let mut pdom = vec![HashSet::new(); n];
1763    for b in 0..n {
1764        if !reachable.contains(&b) {
1765            continue;
1766        }
1767        if exits.contains(&b) {
1768            pdom[b].insert(b);
1769        } else {
1770            pdom[b] = all.clone();
1771        }
1772    }
1773
1774    let mut changed = true;
1775    while changed {
1776        changed = false;
1777        for b in 0..n {
1778            if !reachable.contains(&b) || exits.contains(&b) {
1779                continue;
1780            }
1781            let succs = &prog.blocks[b].succ;
1782            if succs.is_empty() {
1783                continue;
1784            }
1785            let mut nd = all.clone();
1786            for &s in succs {
1787                if !reachable.contains(&s) {
1788                    continue;
1789                }
1790                nd = nd
1791                    .intersection(&pdom[s])
1792                    .copied()
1793                    .collect::<HashSet<usize>>();
1794            }
1795            nd.insert(b);
1796            if nd != pdom[b] {
1797                pdom[b] = nd;
1798                changed = true;
1799            }
1800        }
1801    }
1802    pdom
1803}
1804
1805fn compute_ipdom(pdom: &[HashSet<usize>]) -> Vec<Option<usize>> {
1806    let n = pdom.len();
1807    let mut ip = vec![None; n];
1808    for b in 0..n {
1809        let mut cand: Vec<usize> = pdom[b].iter().copied().collect();
1810        cand.retain(|x| *x != b);
1811        if cand.is_empty() {
1812            continue;
1813        }
1814        // pick c such that no other candidate post-dominates c
1815        let mut picked = None;
1816        'outer: for &c in &cand {
1817            for &d in &cand {
1818                if d == c {
1819                    continue;
1820                }
1821                if pdom[d].contains(&c) {
1822                    continue 'outer;
1823                }
1824            }
1825            picked = Some(c);
1826            break;
1827        }
1828        ip[b] = picked;
1829    }
1830    ip
1831}
1832
1833fn compute_natural_loops(
1834    prog: &ExprProgram,
1835    dom: &[HashSet<usize>],
1836    reachable: &HashSet<usize>,
1837) -> HashMap<usize, HashSet<usize>> {
1838    let mut loops: HashMap<usize, HashSet<usize>> = HashMap::new();
1839    for u in 0..prog.blocks.len() {
1840        if !reachable.contains(&u) {
1841            continue;
1842        }
1843        for &v in &prog.blocks[u].succ {
1844            if !reachable.contains(&v) {
1845                continue;
1846            }
1847            // back edge u -> v if v dominates u
1848            if dom[u].contains(&v) {
1849                let mut set = HashSet::new();
1850                set.insert(v);
1851                set.insert(u);
1852                let mut stack = vec![u];
1853                while let Some(x) = stack.pop() {
1854                    for &p in &prog.blocks[x].pred {
1855                        if !reachable.contains(&p) {
1856                            continue;
1857                        }
1858                        if set.insert(p) {
1859                            stack.push(p);
1860                        }
1861                    }
1862                }
1863                loops
1864                    .entry(v)
1865                    .and_modify(|s| {
1866                        for n in &set {
1867                            s.insert(*n);
1868                        }
1869                    })
1870                    .or_insert(set);
1871            }
1872        }
1873    }
1874    loops
1875}
1876
1877fn to_compound_assign(op: BinOp) -> Option<BinOp> {
1878    Some(match op {
1879        BinOp::Add => BinOp::AddAssign,
1880        BinOp::Sub => BinOp::SubAssign,
1881        BinOp::Mul => BinOp::MulAssign,
1882        BinOp::Div => BinOp::DivAssign,
1883        BinOp::Mod => BinOp::ModAssign,
1884        BinOp::Shl => BinOp::ShlAssign,
1885        BinOp::Shr => BinOp::ShrAssign,
1886        BinOp::UShr => BinOp::UShrAssign,
1887        BinOp::BitAnd => BinOp::AndAssign,
1888        BinOp::BitOr => BinOp::OrAssign,
1889        BinOp::BitXor => BinOp::XorAssign,
1890        _ => return None,
1891    })
1892}
1893
1894fn is_control_op(op: &str) -> bool {
1895    let bare = op.strip_prefix("VM_").unwrap_or(op);
1896    bare.eq_ignore_ascii_case("JMP")
1897        || bare.eq_ignore_ascii_case("JF")
1898        || bare.eq_ignore_ascii_case("JNF")
1899        || bare.eq_ignore_ascii_case("RET")
1900        || bare.eq_ignore_ascii_case("THROW")
1901        || bare.eq_ignore_ascii_case("ENTRY")
1902        || bare.eq_ignore_ascii_case("EXTRY")
1903}
1904
1905fn emit_var_decls(
1906    out: &mut String,
1907    prog: &ExprProgram,
1908    fmt_var: &dyn Fn(VarId) -> String,
1909    arg_count: usize,
1910    indent: usize,
1911) -> Result<()> {
1912    let mut vars: Vec<VarId> = collect_vars(prog);
1913    vars.sort_by_key(|v| (var_key(v), v.ver));
1914    // Declare positive registers, frame locals (_fr*), and special vars.
1915    // Do NOT declare declared params (a0..a{n-1}) or special regs (-1=this, -2=global/this).
1916    // Also skip r0_0 (ver=0 of reg 0) — it's always void and never declared.
1917    vars.retain(|v| match v.var {
1918        Var::Reg(r) if r >= 0 => !(r == 0 && v.ver == 0), // skip r0_0
1919        Var::Reg(r) if r <= -3 => (-3 - r) as usize >= arg_count, // frame locals only, not args
1920        Var::Flag | Var::Exception => true,
1921        _ => false,
1922    });
1923    vars.dedup_by_key(|v| fmt_var(*v));
1924    if vars.is_empty() {
1925        return Ok(());
1926    }
1927
1928    let pad = " ".repeat(indent);
1929    let mut i = 0usize;
1930    while i < vars.len() {
1931        let end = (i + 12).min(vars.len());
1932        write!(out, "{}var ", pad)?;
1933        for j in i..end {
1934            if j != i {
1935                write!(out, ", ")?;
1936            }
1937            write!(out, "{}", fmt_var(vars[j]))?;
1938        }
1939        writeln!(out, ";")?;
1940        i = end;
1941    }
1942    Ok(())
1943}
1944
1945fn collect_vars(prog: &ExprProgram) -> Vec<VarId> {
1946    let mut s: HashSet<VarId> = HashSet::new();
1947
1948    for b in &prog.blocks {
1949        for p in &b.phi {
1950            s.insert(p.result);
1951            for (_pred, v) in &p.args {
1952                s.insert(*v);
1953            }
1954        }
1955        for st in &b.stmts {
1956            collect_vars_stmt(st, &mut s);
1957        }
1958        collect_vars_term(&b.term, &mut s);
1959    }
1960
1961    s.into_iter().collect()
1962}
1963
1964fn collect_vars_stmt(st: &Stmt, s: &mut HashSet<VarId>) {
1965    match st {
1966        Stmt::Assign { dst, expr } => {
1967            s.insert(*dst);
1968            collect_vars_expr(expr, s);
1969        }
1970        Stmt::Store { target, value } => {
1971            collect_vars_expr(target, s);
1972            collect_vars_expr(value, s);
1973        }
1974        Stmt::Update {
1975            dst, target, rhs, ..
1976        } => {
1977            if let Some(d) = dst {
1978                s.insert(*d);
1979            }
1980            collect_vars_expr(target, s);
1981            collect_vars_expr(rhs, s);
1982        }
1983        Stmt::Expr(e) => collect_vars_expr(e, s),
1984        Stmt::Opaque { args, defs, .. } => {
1985            for d in defs {
1986                s.insert(*d);
1987            }
1988            for a in args {
1989                collect_vars_expr(a, s);
1990            }
1991        }
1992    }
1993}
1994
1995fn collect_vars_term(t: &Terminator, s: &mut HashSet<VarId>) {
1996    match t {
1997        Terminator::Br { cond, .. } => collect_vars_expr(cond, s),
1998        Terminator::Throw(e) => collect_vars_expr(e, s),
1999        _ => {}
2000    }
2001}
2002
2003fn collect_vars_expr(e: &Expr, s: &mut HashSet<VarId>) {
2004    match e {
2005        Expr::SsaVar(v) => {
2006            s.insert(*v);
2007        }
2008        Expr::Unary(_, a) => collect_vars_expr(a, s),
2009        Expr::Deref(a) => collect_vars_expr(a, s),
2010        Expr::Binary(_, a, b) => {
2011            collect_vars_expr(a, s);
2012            collect_vars_expr(b, s);
2013        }
2014        Expr::Call(f, args) | Expr::New(f, args) => {
2015            collect_vars_expr(f, s);
2016            for a in args {
2017                collect_vars_expr(a, s);
2018            }
2019        }
2020        Expr::Index(a, b) => {
2021            collect_vars_expr(a, s);
2022            collect_vars_expr(b, s);
2023        }
2024        Expr::Member(a, _) => collect_vars_expr(a, s),
2025        Expr::MethodCall { base, args, .. } => {
2026            collect_vars_expr(base, s);
2027            for a in args {
2028                collect_vars_expr(a, s);
2029            }
2030        }
2031        Expr::Opaque(_, args) => {
2032            for a in args {
2033                collect_vars_expr(a, s);
2034            }
2035        }
2036        _ => {}
2037    }
2038}
2039
2040fn var_key(v: &VarId) -> (u8, i32) {
2041    match v.var {
2042        Var::Reg(r) => (0, r),
2043        Var::Flag => (1, 0),
2044        Var::Exception => (2, 0),
2045    }
2046}
2047
2048fn fmt_vid_tjs(vid: VarId) -> String {
2049    match vid.var {
2050        Var::Reg(r) if r >= 0 => format!("r{}_{}", r, vid.ver),
2051        Var::Reg(-1) => "this".to_string(),
2052        Var::Reg(-2) => "global".to_string(),
2053        Var::Reg(r) => format!("a{}", (-3 - r) as usize),
2054        Var::Flag => format!("flag_{}", vid.ver),
2055        Var::Exception => format!("exc_{}", vid.ver),
2056    }
2057}
2058
2059fn obj_lhs(index: usize, name: Option<&str>) -> String {
2060    if let Some(n) = name {
2061        let parts: Vec<&str> = n.split('.').collect();
2062        if !parts.is_empty() && parts.iter().all(|p| is_identifier(p)) {
2063            return parts.join(".");
2064        }
2065    }
2066    format!("obj{}", index)
2067}
2068
2069/// Post-processing pass: collapse `if (cond) { } else { ... }` into `if (!cond) { ... }`.
2070/// Operates on a flat list of lines with consistent indentation.
2071fn simplify_empty_if_then(lines: &mut Vec<String>) {
2072    let mut i = 0;
2073    while i + 2 < lines.len() {
2074        let ind0 = leading_spaces(&lines[i]);
2075        let ind1 = leading_spaces(&lines[i + 1]);
2076        let ind2 = leading_spaces(&lines[i + 2]);
2077        let ln0 = lines[i][ind0..].trim_end();
2078        let ln1 = lines[i + 1][ind1..].trim_end();
2079        let ln2 = lines[i + 2][ind2..].trim_end();
2080
2081        if ind0 == ind1
2082            && ind0 == ind2
2083            && ln0.starts_with("if (")
2084            && ln0.ends_with(") {")
2085            && ln1 == "}"
2086            && ln2 == "else {"
2087        {
2088            let cond = &ln0["if (".len()..ln0.len() - ") {".len()];
2089            let ncond = negate_str_cond(cond);
2090            let spaces = " ".repeat(ind0);
2091            lines[i] = format!("{}if ({}) {{", spaces, ncond);
2092            lines.remove(i + 2); // "else {"
2093            lines.remove(i + 1); // "}"
2094        // Don't increment — recheck this line in case of further nesting.
2095        } else {
2096            i += 1;
2097        }
2098    }
2099}
2100
2101fn leading_spaces(s: &str) -> usize {
2102    s.len() - s.trim_start().len()
2103}
2104
2105/// Negate a condition string syntactically:
2106/// - `"!expr"` / `"!(inner)"` → strip outer negation
2107/// - simple identifier → `"!ident"`
2108/// - anything else → `"!(cond)"`
2109fn negate_str_cond(cond: &str) -> String {
2110    if let Some(rest) = cond.strip_prefix('!') {
2111        if rest.starts_with('(') && rest.ends_with(')') {
2112            rest[1..rest.len() - 1].to_string()
2113        } else {
2114            rest.to_string()
2115        }
2116    } else if cond.chars().all(|c| c.is_alphanumeric() || c == '_') {
2117        format!("!{}", cond)
2118    } else {
2119        format!("!({})", cond)
2120    }
2121}
2122
2123fn is_identifier(s: &str) -> bool {
2124    let mut it = s.chars();
2125    let Some(c0) = it.next() else {
2126        return false;
2127    };
2128    if !(c0 == '_' || c0.is_ascii_alphabetic()) {
2129        return false;
2130    }
2131    it.all(|c| c == '_' || c.is_ascii_alphanumeric())
2132}