tjs2dec\decompile/
ssa.rs

1use std::collections::{BTreeSet, HashMap, HashSet};
2
3use anyhow::Result;
4
5use crate::vmcodes::vm;
6
7use super::cfg::{BasicBlock, Cfg};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
10pub enum Var {
11    Reg(i32),
12    Flag,
13    Exception,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct VarId {
18    pub var: Var,
19    pub ver: u32,
20}
21
22#[derive(Debug, Clone)]
23pub struct Phi {
24    pub result: VarId,
25    pub var: Var,
26    pub args: Vec<(usize, VarId)>, // (pred_block_id, value)
27}
28
29#[derive(Debug, Clone)]
30pub struct SsaInsn {
31    pub pc: usize,
32    pub op: i32,
33    pub mnemonic: &'static str,
34    pub raw_ops: Vec<i32>,
35    pub uses: Vec<VarId>,
36    pub defs: Vec<VarId>,
37}
38
39#[derive(Debug, Clone)]
40pub struct SsaBlock {
41    pub id: usize,
42    pub start_pc: usize,
43    pub pred: Vec<usize>,
44    pub succ: Vec<usize>,
45    pub phi: Vec<Phi>,
46    pub insns: Vec<SsaInsn>,
47}
48
49#[derive(Debug, Clone)]
50pub struct SsaProgram {
51    pub obj_index: usize,
52    pub blocks: Vec<SsaBlock>,
53    pub entry_block: usize,
54}
55
56impl SsaProgram {
57    pub fn from_cfg(cfg: &Cfg) -> Result<Self> {
58        let dom = DomInfo::compute(&cfg.blocks, cfg.entry_block);
59        let mut phi_sites = place_phi(&cfg.blocks, &dom);
60        let mut ssa_blocks = cfg
61            .blocks
62            .iter()
63            .map(|b| SsaBlock {
64                id: b.id,
65                start_pc: b.start_pc,
66                pred: b.pred.clone(),
67                succ: b.succ.clone(),
68                phi: Vec::new(),
69                insns: Vec::new(),
70            })
71            .collect::<Vec<_>>();
72
73        // Materialize phi nodes.
74        for (bid, vars) in phi_sites.drain() {
75            for v in vars {
76                ssa_blocks[bid].phi.push(Phi {
77                    result: VarId { var: v, ver: 0 }, // filled during renaming
78                    var: v,
79                    args: Vec::new(),
80                });
81            }
82            ssa_blocks[bid].phi.sort_by_key(|p| p.var);
83        }
84
85        rename(cfg, &dom, &mut ssa_blocks);
86        Ok(Self {
87            obj_index: cfg.obj_index,
88            blocks: ssa_blocks,
89            entry_block: cfg.entry_block,
90        })
91    }
92
93    pub fn dump(&self) -> String {
94        let mut out = String::new();
95        for b in &self.blocks {
96            out.push_str(&format!(
97                "-- bb{} @{} (pred={:?}, succ={:?})\n",
98                b.id, b.start_pc, b.pred, b.succ
99            ));
100            for p in &b.phi {
101                out.push_str(&format!(
102                    "  {} = phi {}",
103                    fmt_varid(p.result),
104                    fmt_var(p.var)
105                ));
106                if !p.args.is_empty() {
107                    out.push_str(" [");
108                    for (i, (pred, v)) in p.args.iter().enumerate() {
109                        if i != 0 {
110                            out.push_str(", ");
111                        }
112                        out.push_str(&format!("bb{}: {}", pred, fmt_varid(*v)));
113                    }
114                    out.push(']');
115                }
116                out.push('\n');
117            }
118            for insn in &b.insns {
119                out.push_str(&format!("  {:04} {}", insn.pc, insn.mnemonic));
120                if !insn.raw_ops.is_empty() {
121                    out.push(' ');
122                    for (i, w) in insn.raw_ops.iter().enumerate() {
123                        if i != 0 {
124                            out.push_str(", ");
125                        }
126                        out.push_str(&format!("{}", w));
127                    }
128                }
129                if !insn.defs.is_empty() {
130                    out.push_str("\t; def=");
131                    for (i, d) in insn.defs.iter().enumerate() {
132                        if i != 0 {
133                            out.push_str(",");
134                        }
135                        out.push_str(&fmt_varid(*d));
136                    }
137                }
138                if !insn.uses.is_empty() {
139                    out.push_str("\t; use=");
140                    for (i, u) in insn.uses.iter().enumerate() {
141                        if i != 0 {
142                            out.push_str(",");
143                        }
144                        out.push_str(&fmt_varid(*u));
145                    }
146                }
147                out.push('\n');
148            }
149        }
150        out
151    }
152}
153
154fn fmt_var(v: Var) -> &'static str {
155    match v {
156        Var::Reg(_) => "reg",
157        Var::Flag => "flag",
158        Var::Exception => "exception",
159    }
160}
161
162fn fmt_varid(id: VarId) -> String {
163    match id.var {
164        Var::Reg(r) => format!("r{}#{}", r, id.ver),
165        Var::Flag => format!("flag#{}", id.ver),
166        Var::Exception => format!("exc#{}", id.ver),
167    }
168}
169
170#[derive(Debug, Clone)]
171struct DomInfo {
172    idom: Vec<usize>,
173    children: Vec<Vec<usize>>,
174    df: Vec<HashSet<usize>>,
175}
176
177impl DomInfo {
178    fn compute(blocks: &[BasicBlock], entry: usize) -> Self {
179        let n = blocks.len();
180        let mut dom: Vec<HashSet<usize>> = vec![HashSet::new(); n];
181        for i in 0..n {
182            if i == entry {
183                dom[i].insert(i);
184            } else {
185                dom[i] = (0..n).collect();
186            }
187        }
188
189        let mut changed = true;
190        while changed {
191            changed = false;
192            for b in 0..n {
193                if b == entry {
194                    continue;
195                }
196                let preds = &blocks[b].pred;
197                if preds.is_empty() {
198                    continue;
199                }
200                let mut new_dom = dom[preds[0]].clone();
201                for &p in &preds[1..] {
202                    new_dom = new_dom
203                        .intersection(&dom[p])
204                        .copied()
205                        .collect::<HashSet<_>>();
206                }
207                new_dom.insert(b);
208                if new_dom != dom[b] {
209                    dom[b] = new_dom;
210                    changed = true;
211                }
212            }
213        }
214
215        // idom: pick the strict dominator with the largest dominator set (closest to b).
216        let mut idom = vec![entry; n];
217        for b in 0..n {
218            if b == entry {
219                idom[b] = entry;
220                continue;
221            }
222            let mut best = entry;
223            let mut best_len = 0usize;
224            for &c in dom[b].iter() {
225                if c == b {
226                    continue;
227                }
228                let l = dom[c].len();
229                if l > best_len {
230                    best = c;
231                    best_len = l;
232                }
233            }
234            idom[b] = best;
235        }
236
237        // children
238        let mut children = vec![Vec::new(); n];
239        for b in 0..n {
240            if b == entry {
241                continue;
242            }
243            children[idom[b]].push(b);
244        }
245
246        // dominance frontier
247        let mut df: Vec<HashSet<usize>> = vec![HashSet::new(); n];
248        for b in 0..n {
249            for &s in &blocks[b].succ {
250                if idom[s] != b {
251                    df[b].insert(s);
252                }
253            }
254        }
255        // postorder traversal over dom tree
256        let mut order = Vec::new();
257        fn post(u: usize, ch: &[Vec<usize>], out: &mut Vec<usize>) {
258            for &v in &ch[u] {
259                post(v, ch, out);
260            }
261            out.push(u);
262        }
263        post(entry, &children, &mut order);
264        for &b in &order {
265            for &c in &children[b] {
266                let cdf: Vec<usize> = df[c].iter().copied().collect();
267                for w in cdf {
268                    if idom[w] != b {
269                        df[b].insert(w);
270                    }
271                }
272            }
273        }
274
275        Self { idom, children, df }
276    }
277}
278
279fn collect_vars(blocks: &[BasicBlock]) -> BTreeSet<Var> {
280    let mut vars = BTreeSet::new();
281    vars.insert(Var::Flag);
282
283    for b in blocks {
284        for insn in &b.insns {
285            let (defs, uses) = effects_of(insn.op, insn.words.as_slice());
286            for r in defs {
287                vars.insert(r);
288            }
289            for r in uses {
290                vars.insert(r);
291            }
292        }
293    }
294    vars
295}
296
297fn place_phi(blocks: &[BasicBlock], dom: &DomInfo) -> HashMap<usize, BTreeSet<Var>> {
298    let vars = collect_vars(blocks);
299    let mut def_sites: HashMap<Var, HashSet<usize>> = HashMap::new();
300    for v in vars.iter().copied() {
301        def_sites.insert(v, HashSet::new());
302    }
303    for b in blocks {
304        for insn in &b.insns {
305            let (defs, _uses) = effects_of(insn.op, insn.words.as_slice());
306            for v in defs {
307                def_sites.entry(v).or_default().insert(b.id);
308            }
309        }
310    }
311
312    let mut phi: HashMap<usize, BTreeSet<Var>> = HashMap::new();
313    for (v, defs) in def_sites {
314        let mut work: Vec<usize> = defs.iter().copied().collect();
315        let mut has_phi: HashSet<usize> = HashSet::new();
316        while let Some(n) = work.pop() {
317            for &y in &dom.df[n] {
318                if !has_phi.contains(&y) {
319                    has_phi.insert(y);
320                    phi.entry(y).or_default().insert(v);
321                    if !defs.contains(&y) {
322                        work.push(y);
323                    }
324                }
325            }
326        }
327    }
328    phi
329}
330
331fn rename(cfg: &Cfg, dom: &DomInfo, blocks: &mut [SsaBlock]) {
332    let mut counters: HashMap<Var, u32> = HashMap::new();
333    let mut stacks: HashMap<Var, Vec<VarId>> = HashMap::new();
334
335    // Initialize with version 0 for each observed variable; this avoids special-casing
336    // "use before def" (it becomes rX#0).
337    for b in &cfg.blocks {
338        for insn in &b.insns {
339            let (defs, uses) = effects_of(insn.op, insn.words.as_slice());
340            for v in defs.into_iter().chain(uses.into_iter()) {
341                counters.entry(v).or_insert(0);
342                stacks.entry(v).or_default();
343            }
344        }
345    }
346    counters.entry(Var::Flag).or_insert(0);
347    stacks.entry(Var::Flag).or_default();
348    // Exception is a pseudo source; version 0 is fine.
349    counters.entry(Var::Exception).or_insert(0);
350    stacks.entry(Var::Exception).or_default();
351
352    for (&v, st) in stacks.iter_mut() {
353        st.push(VarId { var: v, ver: 0 });
354    }
355
356    fn new_ver(
357        v: Var,
358        counters: &mut HashMap<Var, u32>,
359        stacks: &mut HashMap<Var, Vec<VarId>>,
360    ) -> VarId {
361        let c = counters.entry(v).or_insert(0);
362        *c += 1;
363        let id = VarId { var: v, ver: *c };
364        stacks.entry(v).or_default().push(id);
365        id
366    }
367
368    fn cur(v: Var, stacks: &HashMap<Var, Vec<VarId>>) -> VarId {
369        stacks
370            .get(&v)
371            .and_then(|s| s.last().copied())
372            .unwrap_or(VarId { var: v, ver: 0 })
373    }
374
375    fn dfs(
376        bid: usize,
377        cfg: &Cfg,
378        dom: &DomInfo,
379        blocks: &mut [SsaBlock],
380        counters: &mut HashMap<Var, u32>,
381        stacks: &mut HashMap<Var, Vec<VarId>>,
382    ) {
383        let mut pushed: Vec<Var> = Vec::new();
384
385        // Rename phi results.
386        for p in blocks[bid].phi.iter_mut() {
387            let id = new_ver(p.var, counters, stacks);
388            p.result = id;
389            pushed.push(p.var);
390        }
391
392        // Insert a pseudo "exception store" at catch entries: rEx = exc.
393        if let Some(&ex_reg) = cfg.catch_sites.get(&blocks[bid].start_pc) {
394            let dst = Var::Reg(ex_reg);
395            let src = Var::Exception;
396            let dst_id = new_ver(dst, counters, stacks);
397            pushed.push(dst);
398            let src_id = cur(src, stacks);
399            blocks[bid].insns.push(SsaInsn {
400                pc: blocks[bid].start_pc,
401                op: -1,
402                mnemonic: "EXCDEF",
403                raw_ops: vec![ex_reg],
404                uses: vec![src_id],
405                defs: vec![dst_id],
406            });
407        }
408
409        // Rename normal instructions.
410        for insn in &cfg.blocks[bid].insns {
411            let (defs, uses) = effects_of(insn.op, insn.words.as_slice());
412            let mut use_ids = Vec::new();
413            for v in uses {
414                use_ids.push(cur(v, stacks));
415            }
416
417            let mut def_ids = Vec::new();
418            for v in defs {
419                let id = new_ver(v, counters, stacks);
420                def_ids.push(id);
421                pushed.push(v);
422            }
423
424            blocks[bid].insns.push(SsaInsn {
425                pc: insn.pc,
426                op: insn.op,
427                mnemonic: insn.mnemonic(),
428                raw_ops: insn.operands().to_vec(),
429                uses: use_ids,
430                defs: def_ids,
431            });
432        }
433
434        // Populate phi arguments for successors.
435        let end_state: HashMap<Var, VarId> = stacks
436            .iter()
437            .map(|(&v, s)| (v, *s.last().unwrap()))
438            .collect();
439        for &succ in blocks[bid].succ.iter() {
440            for p in blocks[succ].phi.iter_mut() {
441                let val = *end_state
442                    .get(&p.var)
443                    .unwrap_or(&VarId { var: p.var, ver: 0 });
444                p.args.push((bid, val));
445            }
446        }
447
448        // Recurse.
449        for &c in &dom.children[bid] {
450            dfs(c, cfg, dom, blocks, counters, stacks);
451        }
452
453        // Pop everything we pushed in this block.
454        for v in pushed.into_iter().rev() {
455            if let Some(st) = stacks.get_mut(&v) {
456                let _ = st.pop();
457            }
458        }
459    }
460
461    dfs(
462        cfg.entry_block,
463        cfg,
464        dom,
465        blocks,
466        &mut counters,
467        &mut stacks,
468    );
469}
470
471fn effects_of(op: i32, words: &[i32]) -> (Vec<Var>, Vec<Var>) {
472    // Return (defs, uses). Conservatively treats most arithmetic as read-modify-write.
473    // This function is the single place to refine semantics as we progress.
474    if words.is_empty() {
475        return (Vec::new(), Vec::new());
476    }
477    let ops = &words[1..];
478
479    // Helpers
480    let r = |i: usize| -> Var { Var::Reg(ops.get(i).copied().unwrap_or(0)) };
481    let def_r0 = |reg: i32| -> Vec<Var> {
482        if reg == 0 {
483            Vec::new()
484        } else {
485            vec![Var::Reg(reg)]
486        }
487    };
488
489    // Opcodes that define/consume the flag.
490    // Note: SETF/SETNF are constant-set-to-register, not flag operations.
491    if matches!(
492        op,
493        vm::VM_CEQ
494            | vm::VM_CDEQ
495            | vm::VM_CLT
496            | vm::VM_CGT
497            | vm::VM_CHKINS
498            | vm::VM_TT
499            | vm::VM_TF
500            | vm::VM_NF
501    ) {
502        let mut uses = Vec::new();
503        match op {
504            x if x == vm::VM_TT || x == vm::VM_TF => {
505                uses.push(r(0));
506            }
507            x if x == vm::VM_CEQ
508                || x == vm::VM_CDEQ
509                || x == vm::VM_CLT
510                || x == vm::VM_CGT
511                || x == vm::VM_CHKINS =>
512            {
513                uses.push(r(0));
514                uses.push(r(1));
515            }
516            x if x == vm::VM_NF => {
517                uses.push(Var::Flag);
518            }
519            _ => {}
520        }
521        let defs = vec![Var::Flag];
522        return (defs, uses);
523    }
524
525    // Branch uses flag.
526    if op == vm::VM_JF || op == vm::VM_JNF {
527        return (Vec::new(), vec![Var::Flag]);
528    }
529
530    // SETF/SETNF: dst := true/false (does NOT define Flag)
531    if op == vm::VM_SETF || op == vm::VM_SETNF {
532        let dst = ops.get(0).copied().unwrap_or(0);
533        return (vec![Var::Reg(dst)], Vec::new());
534    }
535
536    // CONST: dst := data[*]
537    if op == vm::VM_CONST {
538        let dst = ops.get(0).copied().unwrap_or(0);
539        return (def_r0(dst), Vec::new());
540    }
541
542    // CP: dst := src
543    if op == vm::VM_CP {
544        let dst = ops.get(0).copied().unwrap_or(0);
545        let src = ops.get(1).copied().unwrap_or(0);
546        return (def_r0(dst), vec![Var::Reg(src)]);
547    }
548
549    // CL: dst := void
550    if op == vm::VM_CL {
551        let dst = ops.get(0).copied().unwrap_or(0);
552        return (def_r0(dst), Vec::new());
553    }
554
555    // CCL: cl %r0-%rN
556    if op == vm::VM_CCL {
557        let r0 = ops.get(0).copied().unwrap_or(0);
558        let cnt = ops.get(1).copied().unwrap_or(0);
559        let mut defs = Vec::new();
560        for k in 0..cnt {
561            let rr = r0 + k;
562            if rr != 0 {
563                defs.push(Var::Reg(rr));
564            }
565        }
566        return (defs, Vec::new());
567    }
568
569    // Global: dst := GlobalObject
570    if op == vm::VM_GLOBAL {
571        let dst = ops.get(0).copied().unwrap_or(0);
572        return (def_r0(dst), Vec::new());
573    }
574
575    // SRV: set return value (uses r)
576    if op == vm::VM_SRV {
577        return (Vec::new(), vec![r(0)]);
578    }
579
580    // RET / JMP / NOP / REGMEMBER / DEBUGGER / EXTRY / ENTRY: no register effects here.
581    // VM_JMP carries a relative PC offset as its single operand, NOT a register.
582    if matches!(
583        op,
584        vm::VM_RET
585            | vm::VM_JMP
586            | vm::VM_NOP
587            | vm::VM_REGMEMBER
588            | vm::VM_DEBUGGER
589            | vm::VM_EXTRY
590            | vm::VM_ENTRY
591    ) {
592        return (Vec::new(), Vec::new());
593    }
594
595    // THROW reads operand.
596    if op == vm::VM_THROW {
597        return (Vec::new(), vec![r(0)]);
598    }
599
600    // SETP/GETP (property accessor register binding) uses regs.
601    if op == vm::VM_SETP {
602        // setp %obj, %prop
603        return (Vec::new(), vec![r(0), r(1)]);
604    }
605    if op == vm::VM_GETP {
606        // getp %dst, %prop
607        let dst = ops.get(0).copied().unwrap_or(0);
608        return (def_r0(dst), vec![r(1)]);
609    }
610
611    // GPD/GPDS: dst := obj[member_data]
612    if op == vm::VM_GPD || op == vm::VM_GPDS {
613        let dst = ops.get(0).copied().unwrap_or(0);
614        let obj = ops.get(1).copied().unwrap_or(0);
615        return (def_r0(dst), vec![Var::Reg(obj)]);
616    }
617    // GPI/GPIS: dst := obj[member_reg]
618    if op == vm::VM_GPI || op == vm::VM_GPIS {
619        let dst = ops.get(0).copied().unwrap_or(0);
620        let obj = ops.get(1).copied().unwrap_or(0);
621        let key = ops.get(2).copied().unwrap_or(0);
622        return (def_r0(dst), vec![Var::Reg(obj), Var::Reg(key)]);
623    }
624    // SPD/SPDE/SPDEH/SPDS: obj[member_data] := value
625    if matches!(op, vm::VM_SPD | vm::VM_SPDE | vm::VM_SPDEH | vm::VM_SPDS) {
626        let obj = ops.get(0).copied().unwrap_or(0);
627        let val = ops.get(2).copied().unwrap_or(0);
628        return (Vec::new(), vec![Var::Reg(obj), Var::Reg(val)]);
629    }
630    // SPI/SPIE/SPIS: obj[member_reg] := value
631    if op == vm::VM_SPI || op == vm::VM_SPIE || op == vm::VM_SPIS {
632        let obj = ops.get(0).copied().unwrap_or(0);
633        let key = ops.get(1).copied().unwrap_or(0);
634        let val = ops.get(2).copied().unwrap_or(0);
635        return (
636            Vec::new(),
637            vec![Var::Reg(obj), Var::Reg(key), Var::Reg(val)],
638        );
639    }
640
641    // DELD/DELI, TYPEOFD/TYPEOFI: affect object + key + dst (for typeof*)
642    // DELD: dst := delete obj[member_data]
643    if op == vm::VM_DELD {
644        let dst = ops.get(0).copied().unwrap_or(0);
645        let obj = ops.get(1).copied().unwrap_or(0);
646        return (vec![Var::Reg(dst)], vec![Var::Reg(obj)]);
647    }
648
649    // DELI: dst := delete obj[key_reg]
650    if op == vm::VM_DELI {
651        let dst = ops.get(0).copied().unwrap_or(0);
652        let obj = ops.get(1).copied().unwrap_or(0);
653        let key = ops.get(2).copied().unwrap_or(0);
654        return (vec![Var::Reg(dst)], vec![Var::Reg(obj), Var::Reg(key)]);
655    }
656
657    if op == vm::VM_TYPEOFD {
658        let dst = ops.get(0).copied().unwrap_or(0);
659        let base = ops.get(1).copied().unwrap_or(0);
660        return (def_r0(dst), vec![Var::Reg(base)]);
661    }
662    if op == vm::VM_TYPEOFI {
663        let dst = ops.get(0).copied().unwrap_or(0);
664        let base = ops.get(1).copied().unwrap_or(0);
665        let key = ops.get(2).copied().unwrap_or(0);
666        return (def_r0(dst), vec![Var::Reg(base), Var::Reg(key)]);
667    }
668
669    // INC/DEC families
670    if (vm::VM_INC..=vm::VM_INCP).contains(&op) {
671        return effects_op1_prop(ops, vm::VM_INC, op, true);
672    }
673    if (vm::VM_DEC..=vm::VM_DECP).contains(&op) {
674        return effects_op1_prop(ops, vm::VM_DEC, op, true);
675    }
676
677    // Binary families with property variants.
678    if is_op2_prop(op) {
679        return effects_op2_prop(ops, op);
680    }
681
682    // CALL/NEW families: define dst, use callee + args.
683    if op == vm::VM_CALL || op == vm::VM_NEW {
684        let dst = ops.get(0).copied().unwrap_or(0);
685        let callee = ops.get(1).copied().unwrap_or(0);
686        let argc = ops.get(2).copied().unwrap_or(0);
687        let mut uses = vec![Var::Reg(callee)];
688        uses.extend(call_arg_regs(argc, ops, 3));
689        return (def_r0(dst), uses);
690    }
691    // CALLD: dst := obj.*member(args)
692    // Format (ops): [dst, obj, member_data_idx, argc, arg0, arg1, ...]
693    if op == vm::VM_CALLD {
694        let dst = ops.get(0).copied().unwrap_or(0);
695        let obj = ops.get(1).copied().unwrap_or(0);
696        // ops[2] = member_data_idx (not a register)
697        let argc = ops.get(3).copied().unwrap_or(0);
698        let mut uses = vec![Var::Reg(obj)];
699        uses.extend(call_arg_regs(argc, ops, 4));
700        return (def_r0(dst), uses);
701    }
702    // CALLI: dst := obj.%member(args)
703    // Format (ops): [dst, obj, member_reg, argc, arg0, arg1, ...]
704    if op == vm::VM_CALLI {
705        let dst = ops.get(0).copied().unwrap_or(0);
706        let obj = ops.get(1).copied().unwrap_or(0);
707        let member = ops.get(2).copied().unwrap_or(0);
708        let argc = ops.get(3).copied().unwrap_or(0);
709        let mut uses = vec![Var::Reg(obj), Var::Reg(member)];
710        uses.extend(call_arg_regs(argc, ops, 4));
711        return (def_r0(dst), uses);
712    }
713
714    // Unary RMW operations (conservative default): op %r => r := f(r)
715    if ops.len() == 1 {
716        let dst = ops.get(0).copied().unwrap_or(0);
717        if dst == 0 {
718            return (Vec::new(), Vec::new());
719        }
720        return (vec![Var::Reg(dst)], vec![Var::Reg(dst)]);
721    }
722
723    // Binary RMW operations (conservative default): op %a,%b => a := f(a,b)
724    if ops.len() == 2 {
725        let a = ops.get(0).copied().unwrap_or(0);
726        let b = ops.get(1).copied().unwrap_or(0);
727        let defs = def_r0(a);
728        let mut uses = Vec::new();
729        uses.push(Var::Reg(a));
730        uses.push(Var::Reg(b));
731        return (defs, uses);
732    }
733
734    // Default: no register effects.
735    (Vec::new(), Vec::new())
736}
737
738fn call_arg_regs(argc: i32, ops: &[i32], start: usize) -> Vec<Var> {
739    // Mirror disasm formatting: argc >=0 => that many registers;
740    // -1 => omitted args; -2 => FAT list.
741    let mut uses = Vec::new();
742    if argc == -1 {
743        return uses;
744    }
745    if argc == -2 {
746        let num = ops.get(start).copied().unwrap_or(0) as usize;
747        // pairs: (ty, val)
748        for j in 0..num {
749            let ty = ops.get(start + 1 + j * 2).copied().unwrap_or(0);
750            let val = ops.get(start + 1 + j * 2 + 1).copied().unwrap_or(0);
751            // Only FAT_NORMAL / FAT_EXPAND consume a register.
752            if ty == 0 || ty == 1 {
753                uses.push(Var::Reg(val));
754            }
755        }
756        return uses;
757    }
758
759    let n = argc.max(0) as usize;
760    for j in 0..n {
761        if let Some(&r) = ops.get(start + j) {
762            uses.push(Var::Reg(r));
763        }
764    }
765    uses
766}
767
768fn is_op2_prop(op: i32) -> bool {
769    matches!(
770        op,
771        _ if (vm::VM_LOR..=vm::VM_LOR + 3).contains(&op)
772            || (vm::VM_LAND..=vm::VM_LAND + 3).contains(&op)
773            || (vm::VM_BOR..=vm::VM_BOR + 3).contains(&op)
774            || (vm::VM_BXOR..=vm::VM_BXOR + 3).contains(&op)
775            || (vm::VM_BAND..=vm::VM_BAND + 3).contains(&op)
776            || (vm::VM_SAR..=vm::VM_SAR + 3).contains(&op)
777            || (vm::VM_SAL..=vm::VM_SAL + 3).contains(&op)
778            || (vm::VM_SR..=vm::VM_SR + 3).contains(&op)
779            || (vm::VM_ADD..=vm::VM_ADD + 3).contains(&op)
780            || (vm::VM_SUB..=vm::VM_SUB + 3).contains(&op)
781            || (vm::VM_MOD..=vm::VM_MOD + 3).contains(&op)
782            || (vm::VM_DIV..=vm::VM_DIV + 3).contains(&op)
783            || (vm::VM_IDIV..=vm::VM_IDIV + 3).contains(&op)
784            || (vm::VM_MUL..=vm::VM_MUL + 3).contains(&op)
785    )
786}
787
788fn op2_base(op: i32) -> i32 {
789    for base in [
790        vm::VM_LOR,
791        vm::VM_LAND,
792        vm::VM_BOR,
793        vm::VM_BXOR,
794        vm::VM_BAND,
795        vm::VM_SAR,
796        vm::VM_SAL,
797        vm::VM_SR,
798        vm::VM_ADD,
799        vm::VM_SUB,
800        vm::VM_MOD,
801        vm::VM_DIV,
802        vm::VM_IDIV,
803        vm::VM_MUL,
804    ] {
805        if (base..=base + 3).contains(&op) {
806            return base;
807        }
808    }
809    op
810}
811
812fn effects_op2_prop(ops: &[i32], op: i32) -> (Vec<Var>, Vec<Var>) {
813    let base = op2_base(op);
814    match op - base {
815        0 => {
816            // op %a, %b  => a := f(a,b)
817            let a = ops.get(0).copied().unwrap_or(0);
818            let b = ops.get(1).copied().unwrap_or(0);
819            let defs = if a == 0 {
820                Vec::new()
821            } else {
822                vec![Var::Reg(a)]
823            };
824            (defs, vec![Var::Reg(a), Var::Reg(b)])
825        }
826        1 => {
827            // oppd %res, %obj.*data, %rhs
828            let res = ops.get(0).copied().unwrap_or(0);
829            let obj = ops.get(1).copied().unwrap_or(0);
830            let rhs = ops.get(3).copied().unwrap_or(0);
831            (
832                if res == 0 {
833                    Vec::new()
834                } else {
835                    vec![Var::Reg(res)]
836                },
837                vec![Var::Reg(obj), Var::Reg(rhs)],
838            )
839        }
840        2 => {
841            // oppi %res, %obj.%key, %rhs
842            let res = ops.get(0).copied().unwrap_or(0);
843            let obj = ops.get(1).copied().unwrap_or(0);
844            let key = ops.get(2).copied().unwrap_or(0);
845            let rhs = ops.get(3).copied().unwrap_or(0);
846            (
847                if res == 0 {
848                    Vec::new()
849                } else {
850                    vec![Var::Reg(res)]
851                },
852                vec![Var::Reg(obj), Var::Reg(key), Var::Reg(rhs)],
853            )
854        }
855        3 => {
856            // opp %res, %obj, %rhs
857            let res = ops.get(0).copied().unwrap_or(0);
858            let obj = ops.get(1).copied().unwrap_or(0);
859            let rhs = ops.get(2).copied().unwrap_or(0);
860            (
861                if res == 0 {
862                    Vec::new()
863                } else {
864                    vec![Var::Reg(res)]
865                },
866                vec![Var::Reg(obj), Var::Reg(rhs)],
867            )
868        }
869        _ => (Vec::new(), Vec::new()),
870    }
871}
872
873fn effects_op1_prop(ops: &[i32], base: i32, op: i32, rmw: bool) -> (Vec<Var>, Vec<Var>) {
874    match op - base {
875        0 => {
876            // inc/dec %r
877            let r0 = ops.get(0).copied().unwrap_or(0);
878            if r0 == 0 {
879                return (Vec::new(), Vec::new());
880            }
881            if rmw {
882                (vec![Var::Reg(r0)], vec![Var::Reg(r0)])
883            } else {
884                (vec![Var::Reg(r0)], Vec::new())
885            }
886        }
887        1 => {
888            // incpd %res, %obj.*data
889            let res = ops.get(0).copied().unwrap_or(0);
890            let obj = ops.get(1).copied().unwrap_or(0);
891            (
892                if res == 0 {
893                    Vec::new()
894                } else {
895                    vec![Var::Reg(res)]
896                },
897                vec![Var::Reg(obj)],
898            )
899        }
900        2 => {
901            // incpi %res, %obj.%key
902            let res = ops.get(0).copied().unwrap_or(0);
903            let obj = ops.get(1).copied().unwrap_or(0);
904            let key = ops.get(2).copied().unwrap_or(0);
905            (
906                if res == 0 {
907                    Vec::new()
908                } else {
909                    vec![Var::Reg(res)]
910                },
911                vec![Var::Reg(obj), Var::Reg(key)],
912            )
913        }
914        3 => {
915            // incp %res, %obj
916            let res = ops.get(0).copied().unwrap_or(0);
917            let obj = ops.get(1).copied().unwrap_or(0);
918            (
919                if res == 0 {
920                    Vec::new()
921                } else {
922                    vec![Var::Reg(res)]
923                },
924                vec![Var::Reg(obj)],
925            )
926        }
927        _ => (Vec::new(), Vec::new()),
928    }
929}