tjs2dec\decompile/
srcgen_low.rs

1use anyhow::Result;
2use std::collections::{HashMap, HashSet};
3use std::fmt::Write as _;
4
5use crate::model::{Tjs2File, Tjs2Object};
6
7use super::cfg::Cfg;
8use super::expr::Expr;
9use super::expr_build::{ExprBlock, ExprProgram, Stmt, Terminator};
10use super::ssa::{Var, VarId};
11
12pub struct SrcgenOptions {
13    pub inline: bool,
14}
15
16impl Default for SrcgenOptions {
17    fn default() -> Self {
18        SrcgenOptions { inline: true }
19    }
20}
21
22pub fn dump_src_file(file: &Tjs2File, _opt: SrcgenOptions) -> Result<String> {
23    let mut out = String::new();
24
25    writeln!(
26        out,
27        "// Decompiled from TJS2 bytecode (decompile_low)\n// objects: {}\n",
28        file.objects.len()
29    )?;
30    writeln!(out, "var __tjs2dec_objs = [];\n")?;
31
32    for obj in &file.objects {
33        if obj.code.is_empty() {
34            continue;
35        }
36
37        writeln!(
38            out,
39            "// == object {}: {} ==",
40            obj.index,
41            obj.name.as_deref().unwrap_or("<anonymous>")
42        )?;
43
44        let lhs = obj_lhs(obj.index, obj.name.as_deref());
45        writeln!(out, "{} = function() {{", lhs)?;
46
47        let cfg = Cfg::build(obj)?;
48        let ssa = super::ssa::SsaProgram::from_cfg(&cfg)?;
49        let prog = ExprProgram::from_ssa(file, obj, &ssa)?;
50
51        emit_function(&mut out, &prog, &cfg, obj)?;
52
53        writeln!(out, "}};\n")?;
54    }
55
56    Ok(out)
57}
58
59fn obj_lhs(idx: usize, name: Option<&str>) -> String {
60    if let Some(n) = name {
61        let n = n.trim();
62        if !n.is_empty() {
63            return n.to_string();
64        }
65    }
66    format!("__tjs2dec_objs[{}]", idx)
67}
68
69fn fmt_vid(vid: VarId) -> String {
70    match vid.var {
71        Var::Reg(r) => format!("__r{}_{}", r, vid.ver),
72        Var::Flag => format!("__flag_{}", vid.ver),
73        Var::Exception => format!("__exc_{}", vid.ver),
74    }
75}
76
77fn emit_function(out: &mut String, prog: &ExprProgram, cfg: &Cfg, obj: &Tjs2Object) -> Result<()> {
78    // helper to keep opaque ops executable
79    writeln!(out, "  function __tjs2dec_opaque(op) {{ return void; }}")?;
80
81    let fmt_var = |v: VarId| fmt_vid(v);
82
83    // collect + declare SSA vars
84    let vars = collect_all_vars(prog);
85    if !vars.is_empty() {
86        for name in vars {
87            writeln!(out, "  var {} = void;", name)?;
88        }
89    }
90
91    writeln!(out, "  var __rv = void;")?;
92    writeln!(out, "  var __bb = {};", prog.entry_block)?;
93    writeln!(out, "  var __exobj = void;")?;
94    writeln!(out, "  while(true) {{")?;
95    writeln!(out, "    switch(__bb) {{")?;
96
97    // precompute phi edge moves: (pred, succ) -> [(dst, src)...]
98    let phi_moves = build_phi_edge_moves(prog);
99
100    for b in &prog.blocks {
101        writeln!(out, "      case {}: {{", b.id)?;
102
103        // optional: if this block is a catch entry (by pc), you may want to materialize __exobj.
104        // We do NOT guess register bindings here; SSA should already encode catch semantics.
105
106        let exc_succ = exceptional_succ(b);
107
108        if let Some(ex) = exc_succ {
109            writeln!(out, "        try {{")?;
110            emit_block_body(out, b, &fmt_var, &phi_moves, obj, ex)?;
111            writeln!(out, "        }} catch(__e) {{")?;
112            writeln!(out, "          __exobj = __e;")?;
113            emit_phi_parallel_copies(out, &fmt_var, &phi_moves, b.id, ex, "          ")?;
114            writeln!(out, "          __bb = {};", ex)?;
115            writeln!(out, "          continue;")?;
116            writeln!(out, "        }}")?;
117        } else {
118            emit_block_body(out, b, &fmt_var, &phi_moves, obj, usize::MAX)?;
119        }
120
121        writeln!(out, "      }}")?;
122    }
123
124    // default escape hatch
125    writeln!(out, "      default: return __rv;")?;
126    writeln!(out, "    }}")?;
127    writeln!(out, "  }}")?;
128
129    Ok(())
130}
131
132fn emit_block_body(
133    out: &mut String,
134    b: &ExprBlock,
135    fmt_var: &dyn Fn(VarId) -> String,
136    phi_moves: &HashMap<(usize, usize), Vec<(VarId, VarId)>>,
137    _obj: &Tjs2Object,
138    _exc_succ: usize,
139) -> Result<()> {
140    for st in &b.stmts {
141        emit_stmt(out, st, fmt_var)?;
142    }
143
144    emit_terminator(out, b, fmt_var, phi_moves)?;
145
146    Ok(())
147}
148
149fn emit_stmt(out: &mut String, st: &Stmt, fmt_var: &dyn Fn(VarId) -> String) -> Result<()> {
150    match st {
151        Stmt::Assign { dst, expr } => {
152            writeln!(
153                out,
154                "        {} = {};",
155                fmt_var(*dst),
156                expr.to_tjs_with(fmt_var)
157            )?;
158        }
159        Stmt::Store { target, value } => {
160            writeln!(
161                out,
162                "        {} = {};",
163                target.to_tjs_with(fmt_var),
164                value.to_tjs_with(fmt_var)
165            )?;
166        }
167        Stmt::Update {
168            dst,
169            target,
170            op,
171            rhs,
172        } => {
173            // Evaluate once to avoid re-evaluating target if it's not trivial.
174            // This is still a static lowering; if you want stricter "no temp" policy, tell me.
175            let tmp = "__tjs2dec_u";
176            writeln!(
177                out,
178                "        var {} = ({} {} {});",
179                tmp,
180                target.to_tjs_with(fmt_var),
181                op.op_str(),
182                rhs.to_tjs_with(fmt_var)
183            )?;
184            writeln!(out, "        {} = {};", target.to_tjs_with(fmt_var), tmp)?;
185            if let Some(d) = dst {
186                writeln!(out, "        {} = {};", fmt_var(*d), tmp)?;
187            }
188        }
189        Stmt::Expr(e) => {
190            writeln!(out, "        {};", e.to_tjs_with(fmt_var))?;
191        }
192        Stmt::Opaque { op, args, defs } => {
193            let mut call = String::new();
194            write!(&mut call, "__tjs2dec_opaque(\"{}\"", op)?;
195            for a in args {
196                write!(&mut call, ", {}", a.to_tjs_with(fmt_var))?;
197            }
198            call.push(')');
199
200            if defs.is_empty() {
201                writeln!(out, "        {};", call)?;
202            } else if defs.len() == 1 {
203                writeln!(out, "        {} = {};", fmt_var(defs[0]), call)?;
204            } else {
205                // multi-def: we do one call, copy result to all defs
206                writeln!(out, "        var __tjs2dec_o = {};", call)?;
207                for d in defs {
208                    writeln!(out, "        {} = __tjs2dec_o;", fmt_var(*d))?;
209                }
210            }
211        }
212    }
213    Ok(())
214}
215
216fn emit_terminator(
217    out: &mut String,
218    b: &ExprBlock,
219    fmt_var: &dyn Fn(VarId) -> String,
220    phi_moves: &HashMap<(usize, usize), Vec<(VarId, VarId)>>,
221) -> Result<()> {
222    match &b.term {
223        Terminator::Jmp(t) => {
224            emit_phi_parallel_copies(out, fmt_var, phi_moves, b.id, *t, "        ")?;
225            writeln!(out, "        __bb = {};", t)?;
226            writeln!(out, "        continue;")?;
227        }
228        Terminator::Br {
229            cond,
230            if_true,
231            if_false,
232        } => {
233            writeln!(out, "        if ({}) {{", cond.to_tjs_with(fmt_var))?;
234            emit_phi_parallel_copies(out, fmt_var, phi_moves, b.id, *if_true, "          ")?;
235            writeln!(out, "          __bb = {};", if_true)?;
236            writeln!(out, "          continue;")?;
237            writeln!(out, "        }} else {{")?;
238            emit_phi_parallel_copies(out, fmt_var, phi_moves, b.id, *if_false, "          ")?;
239            writeln!(out, "          __bb = {};", if_false)?;
240            writeln!(out, "          continue;")?;
241            writeln!(out, "        }}")?;
242        }
243        Terminator::Ret => {
244            writeln!(out, "        return __rv;")?;
245        }
246        Terminator::Throw(e) => {
247            writeln!(out, "        throw {};", e.to_tjs_with(fmt_var))?;
248        }
249        Terminator::Exit => {
250            writeln!(out, "        return __rv;")?;
251        }
252        Terminator::Fallthrough => {
253            if let Some(t) = b.succ.first().copied() {
254                emit_phi_parallel_copies(out, fmt_var, phi_moves, b.id, t, "        ")?;
255                writeln!(out, "        __bb = {};", t)?;
256                writeln!(out, "        continue;")?;
257            } else {
258                writeln!(out, "        return __rv;")?;
259            }
260        }
261    }
262    Ok(())
263}
264
265fn exceptional_succ(b: &ExprBlock) -> Option<usize> {
266    let normal = match b.term {
267        Terminator::Jmp(_) => 1,
268        Terminator::Br { .. } => 2,
269        Terminator::Fallthrough => {
270            if b.succ.is_empty() {
271                0
272            } else {
273                1
274            }
275        }
276        Terminator::Ret | Terminator::Throw(_) | Terminator::Exit => 0,
277    };
278    if b.succ.len() > normal {
279        // CFG builder appends exceptional succ conservatively at the end.
280        b.succ.last().copied()
281    } else {
282        None
283    }
284}
285
286fn build_phi_edge_moves(prog: &ExprProgram) -> HashMap<(usize, usize), Vec<(VarId, VarId)>> {
287    let mut m: HashMap<(usize, usize), Vec<(VarId, VarId)>> = HashMap::new();
288    for blk in &prog.blocks {
289        let succ = blk.id;
290        for p in &blk.phi {
291            let dst = p.result;
292            for (pred, src) in &p.args {
293                m.entry((*pred, succ)).or_default().push((dst, *src));
294            }
295        }
296    }
297    m
298}
299
300fn emit_phi_parallel_copies(
301    out: &mut String,
302    fmt_var: &dyn Fn(VarId) -> String,
303    phi_moves: &HashMap<(usize, usize), Vec<(VarId, VarId)>>,
304    pred: usize,
305    succ: usize,
306    indent: &str,
307) -> Result<()> {
308    let Some(moves) = phi_moves.get(&(pred, succ)) else {
309        return Ok(());
310    };
311
312    // Filter identity moves
313    let mut mv: Vec<(VarId, VarId)> = moves.iter().copied().filter(|(d, s)| d != s).collect();
314    if mv.is_empty() {
315        return Ok(());
316    }
317
318    // Simple parallel-copy: pre-save any src that is also a dst.
319    let dst_set: HashSet<VarId> = mv.iter().map(|(d, _)| *d).collect();
320    let mut saved: HashMap<VarId, String> = HashMap::new();
321    let mut tmp_i = 0usize;
322
323    for (_, s) in &mv {
324        if dst_set.contains(s) && !saved.contains_key(s) {
325            let tname = format!("__tjs2dec_phi{}", tmp_i);
326            tmp_i += 1;
327            writeln!(out, "{}var {} = {};", indent, tname, fmt_var(*s))?;
328            saved.insert(*s, tname);
329        }
330    }
331
332    for (d, s) in mv.drain(..) {
333        if let Some(tn) = saved.get(&s) {
334            writeln!(out, "{}{} = {};", indent, fmt_var(d), tn)?;
335        } else {
336            writeln!(out, "{}{} = {};", indent, fmt_var(d), fmt_var(s))?;
337        }
338    }
339
340    Ok(())
341}
342
343fn collect_all_vars(prog: &ExprProgram) -> Vec<String> {
344    let mut set: HashSet<VarId> = HashSet::new();
345
346    for b in &prog.blocks {
347        for p in &b.phi {
348            set.insert(p.result);
349            for (_, v) in &p.args {
350                set.insert(*v);
351            }
352        }
353
354        for st in &b.stmts {
355            match st {
356                Stmt::Assign { dst, expr } => {
357                    set.insert(*dst);
358                    collect_expr_vars(expr, &mut set);
359                }
360                Stmt::Store { target, value } => {
361                    collect_expr_vars(target, &mut set);
362                    collect_expr_vars(value, &mut set);
363                }
364                Stmt::Update {
365                    dst, target, rhs, ..
366                } => {
367                    if let Some(d) = dst {
368                        set.insert(*d);
369                    }
370                    collect_expr_vars(target, &mut set);
371                    collect_expr_vars(rhs, &mut set);
372                }
373                Stmt::Expr(e) => collect_expr_vars(e, &mut set),
374                Stmt::Opaque { args, defs, .. } => {
375                    for d in defs {
376                        set.insert(*d);
377                    }
378                    for a in args {
379                        collect_expr_vars(a, &mut set);
380                    }
381                }
382            }
383        }
384
385        match &b.term {
386            Terminator::Br { cond, .. } => collect_expr_vars(cond, &mut set),
387            Terminator::Throw(e) => collect_expr_vars(e, &mut set),
388            _ => {}
389        }
390    }
391
392    let mut names: Vec<String> = set.into_iter().map(fmt_vid).collect();
393    names.sort();
394    names.dedup();
395    names
396}
397
398fn collect_expr_vars(e: &Expr, out: &mut HashSet<VarId>) {
399    match e {
400        Expr::SsaVar(v) => {
401            out.insert(*v);
402        }
403        Expr::Unary(_, x) | Expr::Deref(x) => {
404            collect_expr_vars(x, out);
405        }
406        Expr::Binary(_, a, b) => {
407            collect_expr_vars(a, out);
408            collect_expr_vars(b, out);
409        }
410        Expr::Call(f, args) | Expr::New(f, args) => {
411            collect_expr_vars(f, out);
412            for a in args {
413                collect_expr_vars(a, out);
414            }
415        }
416        Expr::Index(a, b) => {
417            collect_expr_vars(a, out);
418            collect_expr_vars(b, out);
419        }
420        Expr::Member(base, _) => {
421            collect_expr_vars(base, out);
422        }
423        Expr::MethodCall { base, args, .. } => {
424            collect_expr_vars(base, out);
425            for a in args {
426                collect_expr_vars(a, out);
427            }
428        }
429        Expr::Opaque(_, args) => {
430            for a in args {
431                collect_expr_vars(a, out);
432            }
433        }
434        _ => {}
435    }
436}