Skip to main content

jdc_core/
convert.rs

1//! Region tree → Stmt tree conversion.
2//!
3//! Responsibilities:
4//! * weave in per-block statements and terminals (return/throw),
5//! * resolve `Goto{target}` nodes against the enclosing loop/switch scope
6//!   stack into `break` / `continue` / labeled goto,
7//! * classify loops into while / do-while / infinite forms,
8//! * assemble try/catch with exception parameter variables.
9
10use crate::fx::{FxHashMap as HashMap, FxHashSet as HashSet};
11
12use crate::cfg::Cfg;
13use crate::ir::build::{BlockResult, Term};
14use crate::ir::expr::{BinOp, ConstVal, Expr, UnOp};
15use crate::ir::stmt::{Catch, Stmt};
16use crate::structure::Region;
17
18/// A named Java statement for `break`/`continue` targets.
19#[derive(Debug)]
20pub enum Jump {
21    Break(Option<String>),
22    Continue(Option<String>),
23    /// Continue of the loop at the given depth-from-innermost, prefixed by
24    /// an emission of the target block's own statements (the jump re-runs
25    /// a shared statement-bearing tail that flows back into the header;
26    /// a bare Continue would skip those statements).
27    ContinueVia(usize, Option<String>),
28    /// Unstructured: emit `label:` + goto as comments (best effort).
29    RawGoto(usize),
30}
31
32#[derive(Debug)]
33struct LoopCtx {
34    header: usize,
35    exits: HashSet<usize>,
36    label: String,
37}
38
39#[derive(Debug)]
40struct SwitchCtx {
41    follow: Option<usize>,
42    label: String,
43    /// Loop-stack depth when the switch was entered: a `break` from a case
44    /// body only needs a label when the jump sits inside a loop that was
45    /// opened WITHIN the switch (an enclosing loop does not intercept it).
46    loops_depth: usize,
47}
48
49pub struct Converter<'a> {
50    pub cfg: &'a Cfg,
51    pub results: &'a Vec<BlockResult>,
52    pub groups: std::borrow::Cow<'a, [crate::structure::TryGroup]>,
53    /// Dominators over the full method graph (for goto fallback decisions).
54    pub dom: std::borrow::Cow<'a, crate::structure::DomInfo>,
55    /// Block whose statements are currently being converted (for postdom
56    /// checks in goto resolution).
57    cur_block: usize,
58    loops: Vec<LoopCtx>,
59    switches: Vec<SwitchCtx>,
60    /// Labels actually referenced by break/continue statements.
61    used_labels: HashSet<String>,
62    /// Follow block of each enclosing If region; a goto to the innermost
63    /// follow is natural structured flow and disappears.
64    if_follows: Vec<usize>,
65    label_counter: usize,
66    /// True while converting the last element of a Seq (a trailing Goto
67    /// there may be inlined as a copy instead of an unemittable jump).
68    goto_is_last: bool,
69    /// The block whose emission IMMEDIATELY follows the current position:
70    /// the innermost enclosing If's follow while converting its arms, or
71    /// the next Seq sibling's head. A RawGoto may elide to fallthrough
72    /// ONLY when its target is this block — the old stack-wide
73    /// `if_follows.contains` check elided jumps to OUTER follows whose
74    /// emission was separated by intervening follow/copy emissions
75    /// (jdk17 DatagramChannelImpl.receive: the b15 fall arm's Goto{SET}
76    /// elided into cur=14's follow emission, skipping
77    /// `sender = sourceSocketAddress()` on the n==0&&isOpen path;
78    /// sun.net.www.protocol.http HttpURLConnection.getInputStream0: the
79    /// clone arms' Goto{addToCache} fell into the post-loop tail — the
80    /// documented addToCache-skip residual).
81    expect_next: std::cell::Cell<Option<usize>>,
82    /// Set while converting a Seq element that DIRECTLY follows a
83    /// CopyStmts of that same block: the back-edge-stub pattern
84    /// [CopyStmts{b}, Goto{b}] already emitted b's statements, so the
85    /// Goto must not inline them again (jdk11 HttpURLConnection
86    /// getInputStream0: the clone tail rendered the clone/removeFromCache
87    /// block twice — the second removeFromCache removed the just-cloned
88    /// entry from the auth cache; a header-adjacent b would also make
89    /// ContinueVia re-emit b's statements after the copy).
90    prev_copy: std::cell::Cell<Option<usize>>,
91    /// Heads of copy-walked shared tails (from the structurer).
92    copied_tails: HashSet<usize>,
93    /// Collected label emissions: block target -> label name (for `Label` stmts).
94    pub pending_labels: HashMap<usize, String>,
95    /// Names of FINAL fields of the class being decompiled. A shared
96    /// terminator block whose statements write one of them must never be
97    /// inlined/copied at extra arrival sites: javac rejects a second
98    /// assignment to a final field (sun.security.util.Debug `hexDigits`).
99    final_fields: HashSet<String>,
100}
101
102/// Remove a trailing `Goto` whose target is the natural continuation.
103fn strip_trailing_goto(s: &mut Stmt, follow: Option<usize>) {
104    let Some(f) = follow else { return };
105    let items = match s {
106        Stmt::Block(v) => v,
107        Stmt::Goto(t) if *t as usize == f => {
108            *s = Stmt::Block(vec![]);
109            return;
110        }
111        _ => return,
112    };
113    // Single descent, no iteration: every arm either strips or stops
114    // (clippy::never_loop was right — this was a while-let in name
115    // only; `if let` states the actual control flow).
116    if let Some(last) = items.last_mut() {
117        match last {
118            Stmt::Goto(t) if *t as usize == f => {
119                items.pop();
120            }
121            Stmt::Block(inner) if !inner.is_empty() => {
122                // descend into the trailing block one level
123                let n = inner.len();
124                if matches!(inner[n - 1], Stmt::Goto(t) if t as usize == f) {
125                    inner.pop();
126                }
127            }
128            _ => {}
129        }
130    }
131}
132
133impl<'a> Converter<'a> {
134    pub fn with_copied_tails(mut self, tails: HashSet<usize>) -> Self {
135        self.copied_tails = tails;
136        self
137    }
138
139    pub fn with_final_fields(mut self, finals: HashSet<String>) -> Self {
140        self.final_fields = finals;
141        self
142    }
143
144    /// True when any statement assigns a final field of this class.
145    fn stmts_write_final(&self, v: &[Stmt]) -> bool {
146        v.iter().any(|s| match s {
147            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
148                matches!(&**target, Expr::Field { name, .. } if self.final_fields.contains(name.as_ref()))
149            }
150            _ => false,
151        })
152    }
153
154    /// Chain-aware twin of stmts_write_final for a tail HEAD: walks the
155    /// linear single-succ Fall/Goto chain (bounded) so a blank-final write
156    /// PAST the head still rejects the inline term-copy (URICertStore
157    /// clinit's two-field tail; mirrors structure's
158    /// terminator_writes_final chain probe).
159    fn tail_chain_writes_final(&self, t: usize) -> bool {
160        if self.final_fields.is_empty() {
161            return false;
162        }
163        let mut x = t;
164        let mut seen: HashSet<usize> = HashSet::default();
165        for _ in 0..8 {
166            if !seen.insert(x) {
167                return false;
168            }
169            if self.stmts_write_final(&self.results[x].stmts) {
170                return true;
171            }
172            if !matches!(self.results[x].term, Term::Fallthrough | Term::Goto) {
173                return false;
174            }
175            let succs = &self.cfg.blocks[x].succ;
176            if succs.len() != 1 {
177                return false;
178            }
179            x = succs[0];
180        }
181        false
182    }
183
184    pub fn new(cfg: &'a Cfg, results: &'a Vec<BlockResult>) -> Self {
185        let universe: HashSet<usize> = (0..cfg.blocks.len()).collect();
186        let dom = crate::structure::compute_dominators(cfg, &universe, cfg.entry);
187        let groups = crate::structure::group_exceptions_with(cfg, Some(results));
188        Self::from_parts(
189            cfg,
190            results,
191            std::borrow::Cow::Owned(groups),
192            std::borrow::Cow::Owned(dom),
193        )
194    }
195
196    /// `new` with the group set and dominator tree precomputed and shared
197    /// with the Structurer (see `Structurer::with_precomputed_groups`).
198    pub fn with_precomputed(
199        cfg: &'a Cfg,
200        results: &'a Vec<BlockResult>,
201        groups: Vec<crate::structure::TryGroup>,
202        dom: crate::structure::DomInfo,
203    ) -> Self {
204        Self::from_parts(
205            cfg,
206            results,
207            std::borrow::Cow::Owned(groups),
208            std::borrow::Cow::Owned(dom),
209        )
210    }
211
212    /// `with_precomputed` borrowing the shared groups/dominators instead
213    /// of taking clones: ddc computes both once per method and feeds the
214    /// SAME data to the Structurer and (per retry) the Converter — three
215    /// deep clones (groups twice, idom vec once) per method vanished.
216    pub fn with_precomputed_ref(
217        cfg: &'a Cfg,
218        results: &'a Vec<BlockResult>,
219        groups: &'a [crate::structure::TryGroup],
220        dom: &'a crate::structure::DomInfo,
221    ) -> Self {
222        Self::from_parts(
223            cfg,
224            results,
225            std::borrow::Cow::Borrowed(groups),
226            std::borrow::Cow::Borrowed(dom),
227        )
228    }
229
230    fn from_parts(
231        cfg: &'a Cfg,
232        results: &'a Vec<BlockResult>,
233        groups: std::borrow::Cow<'a, [crate::structure::TryGroup]>,
234        dom: std::borrow::Cow<'a, crate::structure::DomInfo>,
235    ) -> Self {
236        Converter {
237            cfg,
238            results,
239            groups,
240            dom,
241            cur_block: usize::MAX,
242            loops: Vec::new(),
243            switches: Vec::new(),
244            if_follows: Vec::new(),
245            used_labels: HashSet::default(),
246            label_counter: 0,
247            goto_is_last: false,
248            expect_next: std::cell::Cell::new(None),
249            prev_copy: std::cell::Cell::new(None),
250            copied_tails: HashSet::default(),
251            pending_labels: HashMap::default(),
252            final_fields: HashSet::default(),
253        }
254    }
255
256    fn next_label(&mut self) -> String {
257        self.label_counter += 1;
258        format!("L{}", self.label_counter)
259    }
260
261    fn groups_end(&self, gi: usize) -> Option<u32> {
262        self.groups.get(gi).map(|g| g.end)
263    }
264
265    pub fn convert(&mut self, r: Region) -> Stmt {
266        let mut st = self.conv(r);
267        prune_unused_labels(&mut st, &self.used_labels);
268        st
269    }
270
271    fn conv(&mut self, r: Region) -> Stmt {
272        match r {
273            Region::Empty => Stmt::Block(vec![]),
274            Region::Basic { block } => {
275                self.cur_block = block;
276                if crate::dbg_flag!("JCDC_DBG_BLOCKS") {
277                    eprintln!(
278                        "conv Basic {} ({} stmts)",
279                        block,
280                        self.results[block].stmts.len()
281                    );
282                }
283                self.block_stmts(block)
284            }
285            Region::CopyStmts { block } => {
286                // Copies include the block's terminator when it is a
287                // return/throw (shared terminator blocks are inlined at
288                // every arrival site).
289                let mut v = self.results[block].stmts.clone();
290                match &self.results[block].term {
291                    Term::Return(e) => v.push(Stmt::Return(e.clone())),
292                    Term::Throw(e) => v.push(Stmt::Throw(e.clone())),
293                    _ => {}
294                }
295                if v.len() == 1 {
296                    v.into_iter().next().unwrap()
297                } else {
298                    Stmt::Block(v)
299                }
300            }
301            Region::Seq(v) => {
302                let mut out = Vec::new();
303                let n = v.len();
304                let heads: Vec<usize> = v.iter().map(crate::structure::region_head_block).collect();
305                let prev_copies: Vec<Option<usize>> = (0..n)
306                    .map(|k| {
307                        if k > 0 {
308                            match &v[k - 1] {
309                                Region::CopyStmts { block } => Some(*block),
310                                _ => None,
311                            }
312                        } else {
313                            None
314                        }
315                    })
316                    .collect();
317                for (k, x) in v.into_iter().enumerate() {
318                    let save = self.goto_is_last;
319                    self.goto_is_last = k + 1 == n;
320                    let save_pc = self.prev_copy.get();
321                    self.prev_copy.set(prev_copies[k]);
322                    let save_exp = self.expect_next.get();
323                    // Only override when the next emission is KNOWN: a
324                    // headless tail (Goto/Empty — a jump out) must keep
325                    // the inherited expectation, or a nested arm's Goto
326                    // would elide against a phantom (jdk17 dci: Basic17's
327                    // MAX head erased the follow expectation and Goto{16}
328                    // elided past the SET).
329                    if k + 1 < n && heads[k + 1] != usize::MAX {
330                        self.expect_next.set(Some(heads[k + 1]));
331                    }
332                    out.push(self.conv(x));
333                    self.expect_next.set(save_exp);
334                    self.prev_copy.set(save_pc);
335                    self.goto_is_last = save;
336                }
337                Stmt::Block(out)
338            }
339            Region::If {
340                block,
341                cond,
342                then_r,
343                else_r,
344                follow,
345                ternary,
346            } => {
347                self.cur_block = block;
348                let mut head = self.block_stmts_no_term(block);
349                if let Some((_tv, _fv)) = ternary {
350                    // Pure value diamond: the folded conditional flows into
351                    // the merge block's rebuilt input stack; nothing to emit
352                    // here (branch blocks are statement-free by definition).
353                    let _ = cond;
354                } else {
355                    if let Some(f) = follow {
356                        self.if_follows.push(f);
357                    }
358                    let save_exp = self.expect_next.get();
359                    // follow=None keeps the inherited expectation: the
360                    // flow leaving this If's arms continues to whatever
361                    // follows the If itself.
362                    self.expect_next.set(follow.or(save_exp));
363                    let then_stmt = self.conv(*then_r);
364                    let else_stmt = self.conv(*else_r);
365                    self.expect_next.set(save_exp);
366                    if follow.is_some() {
367                        self.if_follows.pop();
368                    }
369                    let if_stmt = Stmt::If {
370                        cond,
371                        then_stmt: Box::new(then_stmt),
372                        else_stmt: if else_stmt.is_empty_block() {
373                            None
374                        } else {
375                            Some(Box::new(else_stmt))
376                        },
377                    };
378                    head.push(if_stmt);
379                }
380                if head.len() == 1 {
381                    head.pop().unwrap()
382                } else {
383                    Stmt::Block(head)
384                }
385            }
386            Region::Loop {
387                header,
388                body,
389                members,
390                exits,
391            } => {
392                let label = self.next_label();
393                self.loops.push(LoopCtx {
394                    header,
395                    exits: exits.iter().copied().collect(),
396                    label: label.clone(),
397                });
398                let body_stmt = self.conv(*body);
399                let ctx = self.loops.pop().unwrap();
400                let _ = members;
401                let mut st = self.classify_loop(header, body_stmt, ctx);
402                if crate::dbg_flag!("JCDC_DBG_LOOP") {
403                    let kind = match &st {
404                        Stmt::While { .. } => "While".to_string(),
405                        Stmt::DoWhile { .. } => "DoWhile".to_string(),
406                        Stmt::For { .. } => "For".to_string(),
407                        Stmt::Block(b) => format!(
408                            "Block{}[{}]",
409                            b.len(),
410                            b.iter()
411                                .map(|x| match x {
412                                    Stmt::DoWhile { .. } => "DoWhile",
413                                    Stmt::While { .. } => "While",
414                                    Stmt::Break(_) => "Break",
415                                    Stmt::Return(_) => "Return",
416                                    _ => "_",
417                                })
418                                .collect::<Vec<_>>()
419                                .join(",")
420                        ),
421                        Stmt::Labeled { .. } => "Labeled".to_string(),
422                        _ => "Other".to_string(),
423                    };
424                    eprintln!("CLASSIFY-OUT header={} -> {}", header, kind);
425                }
426                // ALWAYS wrap; prune_unused_labels (post-convert) strips
427                // the wrapper when no break/continue ended up using it.
428                // Deciding at region-close missed breaks born in copied
429                // regions AFTER the loop popped (results[..].stmts
430                // clones / tail copies) — orphan `break L5` with no `L5:`
431                // (weixin 未定义的标签 ×687).
432                st = Stmt::Labeled {
433                    label,
434                    body: Box::new(st),
435                };
436                st
437            }
438            Region::Switch {
439                block,
440                selector,
441                cases,
442                default,
443                follow,
444            } => {
445                self.cur_block = block;
446                let sw_label = self.next_label();
447                self.switches.push(SwitchCtx {
448                    follow,
449                    label: sw_label.clone(),
450                    loops_depth: self.loops.len(),
451                });
452                let mut case_groups = Vec::new();
453                for (vals, r) in cases {
454                    let s = self.conv(r);
455                    case_groups.push(crate::ir::stmt::CaseGroup {
456                        labels: vals,
457                        string_labels: vec![],
458                        enum_labels: vec![],
459                        raw_labels: vec![],
460                        guard: None,
461                        body: stmt_to_vec(s),
462                    });
463                }
464                let default_stmt = default.map(|d| Box::new(self.conv(*d)));
465                self.switches.pop();
466                let mut head = self.block_stmts_no_term(block);
467                let mut sw = Stmt::Switch {
468                    selector,
469                    cases: case_groups,
470                    default: default_stmt,
471                    on_string: false,
472                };
473                {
474                    sw = Stmt::Labeled {
475                        label: sw_label,
476                        body: Box::new(sw),
477                    };
478                }
479                head.push(sw);
480                if head.len() == 1 {
481                    head.pop().unwrap()
482                } else {
483                    Stmt::Block(head)
484                }
485            }
486            Region::Try {
487                group_idx,
488                body,
489                catches,
490            } => {
491                let mut body_stmt = self.conv(*body);
492                // Natural exits of the try body / handlers jump to the block
493                // right after the try span; that is structured fallthrough.
494                let gend = self.groups_end(group_idx);
495                let try_follow = gend.and_then(|e| self.cfg.block_at(e));
496                strip_trailing_goto(&mut body_stmt, try_follow);
497                let mut catch_stmts = Vec::new();
498                for (tys, _hblock, r) in catches {
499                    let mut s = self.conv(*r);
500                    strip_trailing_goto(&mut s, try_follow);
501                    catch_stmts.push(Catch {
502                        exc: tys,
503                        var: u32::MAX, // assigned by the method pipeline (handler store)
504                        var_name: None,
505                        body: Box::new(s),
506                    });
507                }
508                Stmt::Try {
509                    body: Box::new(body_stmt),
510                    catches: catch_stmts,
511                    finally: None,
512                }
513            }
514            Region::Goto { target } => {
515                if crate::dbg_flag!("JCDC_DBG_GOTO") {
516                    eprintln!(
517                        "conv Goto target={} cur_block={} loops={:?} switches={} if_follows={:?}",
518                        target,
519                        self.cur_block,
520                        self.loops
521                            .iter()
522                            .map(|l| (l.header, l.exits.len()))
523                            .collect::<Vec<_>>(),
524                        self.switches.len(),
525                        self.if_follows
526                    );
527                }
528                // A jump to an enclosing if's follow that TERMINATES
529                // (return/throw) can be inlined: the copy ends this path
530                // exactly like the jump would, without needing a label.
531                // Same for a jump to a LOOP EXIT that heads a pure
532                // terminator chain (stmt-bearing fall-through links
533                // ending in return/throw): resolve_goto's loop-exit arm
534                // would turn it into a bare `break`, silently dropping
535                // the chain's statements between the loop end and the
536                // shared return (jdk26 Bits.reserveMemory phase-1:
537                // `if (interrupted) <goto interrupt-block>; ` rendered
538                // as else-break and the Thread.currentThread()
539                // .interrupt() vanished — interruption lost). The
540                // inlined copy keeps the abrupt ending (no fall-out
541                // duplication); a genuine fall-out exit still breaks.
542                //
543                let inline_terminator = self.if_follows.contains(&target)
544                    && matches!(self.results[target].term, Term::Return(_) | Term::Throw(_))
545                    && !self.tail_chain_writes_final(target);
546                if inline_terminator {
547                    // Copy the WHOLE terminator chain: the target's own
548                    // statements plus every fall-through link down to
549                    // the final return/throw (a single-block chain is
550                    // the original if-follow case).
551                    let mut v: Vec<Stmt> = Vec::new();
552                    let mut t = target;
553                    let mut guard = 0;
554                    loop {
555                        v.extend(self.results[t].stmts.clone());
556                        match &self.results[t].term {
557                            Term::Return(e) => {
558                                v.push(Stmt::Return(e.clone()));
559                                break;
560                            }
561                            Term::Throw(e) => {
562                                v.push(Stmt::Throw(e.clone()));
563                                break;
564                            }
565                            Term::Fallthrough | Term::Goto
566                                if self.cfg.blocks[t].succ.len() == 1 && guard < 8 =>
567                            {
568                                t = self.cfg.blocks[t].succ[0];
569                                guard += 1;
570                            }
571                            _ => break,
572                        }
573                    }
574                    return if v.len() == 1 {
575                        v.into_iter().next().unwrap()
576                    } else {
577                        Stmt::Block(v)
578                    };
579                }
580                if let Some(j) = self.resolve_goto(target) {
581                    match j {
582                        Jump::Break(lbl) => {
583                            if let Some(l) = &lbl {
584                                self.used_labels.insert(l.clone());
585                            }
586                            Stmt::Break(lbl)
587                        }
588                        Jump::Continue(lbl) => {
589                            if let Some(l) = &lbl {
590                                self.used_labels.insert(l.clone());
591                            }
592                            Stmt::Continue(lbl)
593                        }
594                        Jump::ContinueVia(t, lbl) => {
595                            if let Some(l) = &lbl {
596                                self.used_labels.insert(l.clone());
597                            }
598                            let mut cv = if self.prev_copy.get() == Some(t) {
599                                // Statements already emitted by the
600                                // immediately preceding CopyStmts.
601                                Vec::new()
602                            } else {
603                                self.results[t].stmts.clone()
604                            };
605                            cv.push(Stmt::Continue(lbl));
606                            if cv.len() == 1 {
607                                cv.into_iter().next().unwrap()
608                            } else {
609                                Stmt::Block(cv)
610                            }
611                        }
612                        Jump::RawGoto(t) => {
613                            if crate::dbg_flag!("JCDC_DBG_GOTO") {
614                                eprintln!(
615                                    "RAWGOTO t={} copied_tails={:?} if_follows={:?} last={}",
616                                    t, self.copied_tails, self.if_follows, self.goto_is_last
617                                );
618                            }
619                            // A Goto region is always the last part of its
620                            // walk, so the jump either falls through to the
621                            // enclosing merge or must be inlined.
622                            let term_copy =
623                                matches!(self.results[t].term, Term::Return(_) | Term::Throw(_))
624                                    && !self.tail_chain_writes_final(t)
625                                    && !self.is_retry_loop_header(t);
626                            if term_copy {
627                                let mut cv = self.results[t].stmts.clone();
628                                match &self.results[t].term {
629                                    Term::Return(e) => cv.push(Stmt::Return(e.clone())),
630                                    Term::Throw(e) => cv.push(Stmt::Throw(e.clone())),
631                                    _ => {}
632                                }
633                                if cv.len() == 1 {
634                                    cv.into_iter().next().unwrap()
635                                } else {
636                                    Stmt::Block(cv)
637                                }
638                            } else if self
639                                .loops
640                                .iter()
641                                .any(|l| l.exits.contains(&t) || l.header == t)
642                                && (self.goto_is_last || self.if_follows.contains(&t))
643                            {
644                                // The target is an ENCLOSING LOOP's exit:
645                                // inside a loop body, falling out of an if
646                                // continues the LOOP — the if-follow
647                                // elision would silently drop the jump
648                                // (jdk11 ObjectInputStream.readSerialData's
649                                // finally-copy retry loop lost its `break`,
650                                // the finally's while(true) never completed
651                                // normally, and the tail went unreachable —
652                                // 无法访问的语句 x2 trees). Materialize the
653                                // break (innermost enclosing loop).
654                                Stmt::Break(None)
655                            } else if !matches!(
656                                self.results[t].term,
657                                Term::Return(_) | Term::Throw(_)
658                            ) && !self.results[t].stmts.is_empty()
659                                && !self.copied_tails.contains(&t)
660                                && self.prev_copy.get() != Some(t)
661                                && self
662                                    .expect_next
663                                    .get()
664                                    .map(|nx| {
665                                        // The next emission is a copy that
666                                        // STARTS PAST t (at a successor of t
667                                        // already in copied_tails): eliding
668                                        // would silently skip t's own
669                                        // statements, and no emitted copy
670                                        // covers them. (sj17 receive:
671                                        // Goto{16=SET}, expect=Some(17),
672                                        // 17 copied, 16 not.) If t itself is
673                                        // in copied_tails, an emitted full
674                                        // copy already includes t's stmts —
675                                        // inlining would double-emit (w26
676                                        // receive: Goto{14}, 14 copied).
677                                        let mut x = t;
678                                        for _ in 0..8 {
679                                            if x == nx {
680                                                return x != t;
681                                            }
682                                            if self.copied_tails.contains(&x) {
683                                                return false;
684                                            }
685                                            match self.cfg.blocks[x].succ.first() {
686                                                Some(&n) => x = n,
687                                                None => return false,
688                                            }
689                                        }
690                                        false
691                                    })
692                                    .unwrap_or(false)
693                                && (self
694                                    .if_follows
695                                    .iter()
696                                    .any(|f| self.cfg.blocks[t].succ.contains(f)))
697                            {
698                                // Statement-bearing target FIRST: eliding
699                                // (goto_is_last / expect / copy-tail) would
700                                // silently skip t's own statements when the
701                                // next emission is a copy of t's SUCCESSOR
702                                // rather than t itself (jdk17
703                                // DatagramChannelImpl.receive: Goto{16=
704                                // `sender = sourceSocketAddress()`} elided
705                                // via reaches_copy_tail(16->17) into the
706                                // arm's Basic(17) copy — the n==0&&isOpen
707                                // path returned a null sender). The inline
708                                // below emits t's statements and then falls
709                                // through to the same continuation.
710                                let cv = self.results[t].stmts.clone();
711                                if cv.len() == 1 {
712                                    cv.into_iter().next().unwrap()
713                                } else {
714                                    Stmt::Block(cv)
715                                }
716                            } else if self.goto_is_last
717                                || self.expect_next.get() == Some(t)
718                                || self.reaches_copy_tail(t)
719                            {
720                                // Natural fallthrough reaches the same
721                                // continuation the jump targeted.
722                                Stmt::Block(vec![])
723                            } else if !matches!(
724                                self.results[t].term,
725                                Term::Return(_) | Term::Throw(_)
726                            ) && self.prev_copy.get() != Some(t)
727                                && (self
728                                    .if_follows
729                                    .iter()
730                                    .any(|f| self.cfg.blocks[t].succ.contains(f)))
731                            {
732                                // The jump re-enters an already-structured
733                                // block whose flow ends at an enclosing
734                                // if-follow: inline its statements; the
735                                // natural continuation matches the jump's.
736                                let cv = self.results[t].stmts.clone();
737                                if cv.len() == 1 {
738                                    cv.into_iter().next().unwrap()
739                                } else {
740                                    Stmt::Block(cv)
741                                }
742                            } else {
743                                Stmt::Goto(t as u32)
744                            }
745                        }
746                    }
747                } else {
748                    Stmt::Block(vec![])
749                }
750            }
751        }
752    }
753
754    /// Statements of a block including its terminal (return/throw), but NOT
755    /// the structural terminals (cond/switch/goto — handled by regions).
756    fn block_stmts(&mut self, block: usize) -> Stmt {
757        let mut v = self.block_stmts_no_term(block);
758        match &self.results[block].term {
759            Term::Return(e) => v.push(Stmt::Return(e.clone())),
760            Term::Throw(e) => v.push(Stmt::Throw(e.clone())),
761            Term::Jsr | Term::Ret => v.push(Stmt::Comment("// jsr/ret not supported".into())),
762            _ => {}
763        }
764        if v.len() == 1 {
765            v.pop().unwrap()
766        } else {
767            Stmt::Block(v)
768        }
769    }
770
771    fn block_stmts_no_term(&self, block: usize) -> Vec<Stmt> {
772        // Monitor markers are kept; synchronized reconstruction happens later.
773        self.results[block].stmts.clone()
774    }
775
776    /// Resolve a goto target against the scope stack.
777    fn resolve_goto(&mut self, target: usize) -> Option<Jump> {
778        // continue: innermost loop whose header == target
779        if let Some(i) = (0..self.loops.len())
780            .rev()
781            .find(|&i| self.loops[i].header == target)
782        {
783            let depth = self.loops.len() - 1 - i;
784            return Some(if depth == 0 {
785                Jump::Continue(None)
786            } else {
787                Jump::Continue(Some(self.loops[i].label.clone()))
788            });
789        }
790        // break out of a loop whose exit set contains the target; pick the
791        // OUTERMOST such loop — an exit of nested loops (e.g. `break outer`)
792        // belongs to every enclosing loop's exit set, and breaking only the
793        // innermost would wrongly re-enter the outer loop.
794        if let Some(li) = (0..self.loops.len()).find(|&i| self.loops[i].exits.contains(&target)) {
795            // RESTART-STUB exit: the target chains through statement-free
796            // stubs back into an enclosing-or-self loop HEADER — the jump
797            // re-iterates THAT loop, it does not break out of anything
798            // (jdk11/17/26 ConcurrentLinkedQueue.poll's p==q arm `goto
799            // restartFromHead`: exit 12 is the `goto 0` stub; rendering
800            // break+break fell to the post-loop tail and returned the
801            // STALE item instead of restarting from head). The broad
802            // walk-side continue-stub precedence was reverted once
803            // (Pattern.clazz: switch-case goto stubs must stay
804            // RawGoto-elided so the emitter re-appends case breaks) —
805            // this form is scoped to genuine loop exits with no open
806            // switch on the stack.
807            if self.switches.is_empty() {
808                let mut x = target;
809                let mut guard = 0;
810                let header_hit = loop {
811                    if let Some(hi) = self.loops.iter().position(|l| l.header == x) {
812                        if hi <= li {
813                            break Some(hi);
814                        }
815                    }
816                    if guard >= 8 {
817                        break None;
818                    }
819                    let r = &self.results[x];
820                    let stub = r.stmts.is_empty()
821                        && r.out_stack.is_empty()
822                        && matches!(r.term, Term::Fallthrough | Term::Goto)
823                        && self.cfg.blocks[x].succ.len() == 1;
824                    if !stub {
825                        break None;
826                    }
827                    x = self.cfg.blocks[x].succ[0];
828                    guard += 1;
829                };
830                if let Some(hi) = header_hit {
831                    let depth = self.loops.len() - 1 - hi;
832                    return Some(if depth == 0 {
833                        Jump::Continue(None)
834                    } else {
835                        Jump::Continue(Some(self.loops[hi].label.clone()))
836                    });
837                }
838            }
839            // switches opened after that loop intercept a plain `break`
840            let crosses_switch = !self.switches.is_empty();
841            return Some(if li + 1 == self.loops.len() && !crosses_switch {
842                Jump::Break(None)
843            } else {
844                Jump::Break(Some(self.loops[li].label.clone()))
845            });
846        }
847        if let Some(si) = (0..self.switches.len())
848            .rev()
849            .find(|&i| self.switches[i].follow == Some(target))
850        {
851            return Some(
852                if si + 1 == self.switches.len()
853                    && self.loops.len() == self.switches[si].loops_depth
854                {
855                    Jump::Break(None)
856                } else {
857                    Jump::Break(Some(self.switches[si].label.clone()))
858                },
859            );
860        }
861        // Natural flow to the enclosing if's merge point: no statement.
862        // Only the INNERMOST open follow qualifies — eliding a jump to an
863        // outer follow would silently skip the statements between (e.g. a
864        // shared throw at the end of an else-branch).
865        if self.if_follows.last() == Some(&target) {
866            return None;
867        }
868        // Inside a loop, a jump to an already-structured block that flows
869        // back into the loop is a CONTINUE of the innermost such loop: the
870        // target's statements were emitted earlier in this body, and the
871        // natural continuation after this region is the loop bottom (an
872        // if-follow or a fall-out exit), never the target. Eliding it as
873        // fallthrough (the RawGoto `goto_is_last` arm) silently dropped the
874        // iteration: jdk11/17 InstantPrinterParser.format's second loop
875        // copy emitted `if (i >= fractionalDigits) {}` with no way out --
876        // the digit append + back edge vanished (semantically an infinite
877        // loop; javac: 缺少返回语句 on the method tail).
878        // A target that is a loop EXIT keeps its RawGoto resolution (the
879        // conv arm materializes the break): Pattern.clazz's switch-case
880        // `break` targets sit on the loop-bottom path AND name exits —
881        // continuing there made every switch exit abrupt and the post-
882        // switch tail unreachable (无法访问的语句 x3 trees).
883        let is_any_exit = self.loops.iter().any(|l| l.exits.contains(&target));
884        if !is_any_exit && self.switches.is_empty() {
885            // STUB-TARGET continue: a statement-free confluence stub that
886            // can reach an enclosing header re-iterates that loop (its own
887            // flow was emitted earlier in this body copy; IPP's can_reach
888            // targets t=43/18/96/89/114/21/7 are all stubs).
889            // HEADER-ADJACENT statement-bearing target: the jump re-runs a
890            // shared tail whose statements were NOT emitted on this path —
891            // inline them and continue (jdk11 BigInteger.nextProbablePrime
892            // sieve arms: Goto{result=result.add(TWO)} flowing straight to
893            // the header; a bare Continue skipped the advance and left
894            // while(true) as the method's only completion: 缺少返回语句).
895            // Statement-bearing targets with a BRANCHING or statement
896            // bearing chain to the header keep RawGoto resolution (huc
897            // getInputStream0's clone/getValue tails flow through shared
898            // forward code — continuing there would skip it).
899            let mut via_label: Option<Option<String>> = None;
900            if self.results[target].stmts.is_empty() {
901                if let Some(i) = (0..self.loops.len()).rev().find(|&i| {
902                    crate::structure::can_reach_cfg(self.cfg, target, self.loops[i].header, 4096)
903                }) {
904                    let depth = self.loops.len() - 1 - i;
905                    via_label = Some(if depth == 0 {
906                        None
907                    } else {
908                        Some(self.loops[i].label.clone())
909                    });
910                }
911            } else {
912                let mut x = target;
913                for _ in 0..8 {
914                    if let Some(hi) = self.loops.iter().position(|l| l.header == x) {
915                        let depth = self.loops.len() - 1 - hi;
916                        via_label = Some(if depth == 0 {
917                            None
918                        } else {
919                            Some(self.loops[hi].label.clone())
920                        });
921                        break;
922                    }
923                    if !self.results[x].stmts.is_empty() && x != target {
924                        break;
925                    }
926                    if self.cfg.blocks[x].succ.len() != 1 {
927                        break;
928                    }
929                    x = self.cfg.blocks[x].succ[0];
930                }
931            }
932            if let Some(lbl) = via_label {
933                return Some(if self.results[target].stmts.is_empty() {
934                    Jump::Continue(lbl)
935                } else {
936                    Jump::ContinueVia(target, lbl)
937                });
938            }
939        }
940        // Inside a loop, a forward escape that cannot reach back into the
941        // loop is a break of the innermost loop.
942        if !self.loops.is_empty()
943            && !crate::structure::can_reach_cfg(
944                self.cfg,
945                target,
946                self.loops[self.loops.len() - 1].header,
947                4096,
948            )
949        {
950            return Some(Jump::Break(None));
951        }
952        // Unstructured jump.
953        Some(Jump::RawGoto(target))
954    }
955
956    /// True if `a` post-dominates `b` (every path from b exits through a).
957    /// True when a jump to `t` is equivalent to natural fallthrough: `t`
958    /// (or the end of a chain of statement-free blocks starting at `t`) is
959    /// a copied shared tail or an open if-follow that the enclosing flow
960    /// will emit next anyway.
961    /// True when `t` is the head of a javac retry loop: some exception
962    /// handler protecting `t` has no normal preds and flows straight back
963    /// to `t` (`catch (InterruptedException e) { ..; goto head }`). A Goto
964    /// to such a header re-executes the protected body — inlining the
965    /// body's terminator as a "shared tail copy" drops it out of its try
966    /// (jdk26 Future.exceptionNow: the IE catch got `get(); throw ISE`
967    /// unprotected — 未报告的异常错误 InterruptedException).
968    fn is_retry_loop_header(&self, t: usize) -> bool {
969        self.cfg.exc_edges.iter().any(|e| {
970            e.from == t
971                && e.to != t
972                && self.cfg.blocks[e.to].pred.is_empty()
973                && self.cfg.blocks[e.to].succ.len() == 1
974                && self.cfg.blocks[e.to].succ[0] == t
975        })
976    }
977
978    fn reaches_copy_tail(&self, t: usize) -> bool {
979        let mut x = t;
980        for _ in 0..8 {
981            if self.copied_tails.contains(&x) || self.if_follows.contains(&x) {
982                return true;
983            }
984            if !self.results[x].stmts.is_empty() {
985                return false;
986            }
987            match &self.results[x].term {
988                Term::Fallthrough | Term::Goto => match self.cfg.blocks[x].succ.first() {
989                    Some(&n) => x = n,
990                    None => return false,
991                },
992                _ => return false,
993            }
994        }
995        false
996    }
997
998    #[allow(dead_code)]
999    fn postdominates(&self, a: usize, b: usize, universe: &HashSet<usize>) -> bool {
1000        // BFS from b avoiding a; if no exit block is reachable, a post-dominates.
1001        let mut seen = HashSet::default();
1002        let mut q = std::collections::VecDeque::new();
1003        q.push_back(b);
1004        seen.insert(b);
1005        while let Some(x) = q.pop_front() {
1006            for &s in &self.cfg.blocks[x].succ {
1007                if s == a {
1008                    continue;
1009                }
1010                if !universe.contains(&s) {
1011                    continue;
1012                }
1013                if seen.insert(s) {
1014                    q.push_back(s);
1015                }
1016            }
1017            if self.cfg.blocks[x].succ.is_empty() && x != a && x != b {
1018                // reached an exit without passing through a
1019                return false;
1020            }
1021        }
1022        // also treat blocks leaving the universe as exits
1023        true
1024    }
1025
1026    /// Classify a loop region into while / do-while / infinite and assemble
1027    /// the final statement.
1028    fn classify_loop(&mut self, header: usize, body_stmt: Stmt, ctx: LoopCtx) -> Stmt {
1029        let out = self.classify_loop_impl(header, body_stmt, ctx);
1030        if crate::dbg_flag!("JCDC_DBG_LOOP") {
1031            fn shape(s: &Stmt) -> String {
1032                fn short(e: &Expr) -> String {
1033                    format!("{:?}", e).chars().take(90).collect()
1034                }
1035                match s {
1036                    Stmt::While { cond, body } => {
1037                        format!("While({}) body={}", short(cond), shape(body))
1038                    }
1039                    Stmt::DoWhile { cond, .. } => format!("DoWhile({})", short(cond)),
1040                    Stmt::For { cond, .. } => {
1041                        format!("For({})", cond.as_ref().map(short).unwrap_or_default())
1042                    }
1043                    Stmt::Block(v) => format!(
1044                        "Block[{}]",
1045                        v.iter()
1046                            .take(4)
1047                            .map(|x| match x {
1048                                Stmt::ExprStmt(_) => "expr".to_string(),
1049                                Stmt::LocalDef { .. } => "def".to_string(),
1050                                Stmt::If { cond, .. } => format!("if({})", short(cond)),
1051                                Stmt::While { cond, .. } => format!("while({})", short(cond)),
1052                                Stmt::DoWhile { .. } => "do".to_string(),
1053                                Stmt::Break(_) => "break".to_string(),
1054                                Stmt::Continue(l) => format!("cont{:?}", l),
1055                                Stmt::Return(_) => "ret".to_string(),
1056                                Stmt::Throw(_) => "throw".to_string(),
1057                                Stmt::Try { .. } => "try".to_string(),
1058                                _ => "…".to_string(),
1059                            })
1060                            .collect::<Vec<_>>()
1061                            .join(",")
1062                    ),
1063                    _ => "…".to_string(),
1064                }
1065            }
1066            eprintln!("CLASSIFY-OUT header={} {}", header, shape(&out));
1067        }
1068        out
1069    }
1070
1071    fn classify_loop_impl(&mut self, header: usize, body_stmt: Stmt, ctx: LoopCtx) -> Stmt {
1072        let term = self.results[header].term.clone();
1073        let succs = self.cfg.blocks[header].succ.clone();
1074
1075        match term {
1076            Term::Cond { cond } if succs.len() == 2 => {
1077                let taken = succs[1];
1078                let fall = succs[0];
1079                if taken == header && ctx.exits.contains(&fall) {
1080                    // Self-loop: the header block holds both the body and
1081                    // the trailing conditional back edge → do-while. The
1082                    // fallthrough side is the loop exit. (When the fall
1083                    // side is INTERIOR the self-edge is an in-body retry
1084                    // jump of a loop with no normal completion at all —
1085                    // rotating it into a do-while bottom invents an exit
1086                    // path: jdk11/17/26 DSAParameterGenerator.generatePandQ's
1087                    // `if (!resultQ.isProbablePrime(..)) goto <Q-gen>`
1088                    // self-retry falls through into the P-search interior;
1089                    // the fabricated do-while fell out to the method end —
1090                    // 缺少返回语句. Those shapes take the interior-diamond
1091                    // while(true) arm below.)
1092                    // A compound (multi-test) trailing condition folds first:
1093                    // `if(c1)continue; .. if(cN)continue; else exit;` becomes
1094                    // `do{body}while(c1||..||cN); exit`.
1095                    if let Some((stmts, c, exit)) = extract_compound_do_while(&body_stmt) {
1096                        // An extracted exit that is the loop's OWN unlabeled
1097                        // break must be dropped: the do-while condition false
1098                        // already exits, and re-emitting the break OUTSIDE the
1099                        // loop is a stray `break;` (compile error). SESE's
1100                        // explicit `Goto{header}` -> continue makes self-loop
1101                        // bodies hit this shape (`if (c) continue; else break;`).
1102                        // return/throw/labeled-break exits stay (real fall-out).
1103                        if matches!(exit, Stmt::Break(None)) {
1104                            return Stmt::DoWhile {
1105                                body: Box::new(Stmt::Block(stmts)),
1106                                cond: c,
1107                            };
1108                        }
1109                        return Stmt::Block(vec![
1110                            Stmt::DoWhile {
1111                                body: Box::new(Stmt::Block(stmts)),
1112                                cond: c,
1113                            },
1114                            exit,
1115                        ]);
1116                    }
1117                    let (stmts, c) = match extract_trailing_do_while(&body_stmt) {
1118                        Some(x) => x,
1119                        None => (stmt_to_vec(body_stmt), cond.clone()),
1120                    };
1121                    return Stmt::DoWhile {
1122                        body: Box::new(Stmt::Block(stmts)),
1123                        cond: c,
1124                    };
1125                }
1126                if fall == header && ctx.exits.contains(&taken) {
1127                    // Inverted self-loop: `while (!(c)) body` — the taken
1128                    // side exits, the fallthrough loops back.
1129                    let inner2 = strip_trailing_continue(body_stmt);
1130                    // Stale-phi guard: the body's FIRST statement
1131                    // recomputes the header's condition local (`X =
1132                    // call(); if (X) {body}` — weixin DatabaseUtils'
1133                    // allocRow cursor loop, hu4/k iterator). `while (X)`
1134                    // would read X's PRE-loop value on the first test
1135                    // (可能尚未初始化变量 / stale register), and a plain
1136                    // do-while bottom test could read a register the
1137                    // body interior reused. Inject the else-break and
1138                    // rotate to do-while(true): the exit test then runs
1139                    // exactly where the bytecode has it — after the
1140                    // compute, before the body interior.
1141                    if cond_var_reassigned_first(&inner2, &cond) {
1142                        match try_protected_dowhile(inner2, &cond) {
1143                            Ok(dw) => return dw,
1144                            Err(inner) => {
1145                                return Stmt::While {
1146                                    cond: negate(cond.clone()),
1147                                    body: Box::new(inner),
1148                                };
1149                            }
1150                        }
1151                    }
1152                    return Stmt::While {
1153                        cond: negate(cond.clone()),
1154                        body: Box::new(inner2),
1155                    };
1156                }
1157                let taken_is_exit = ctx.exits.contains(&taken);
1158                if crate::dbg_flag!("JCDC_DBG_LOOP") {
1159                    eprintln!(
1160                        "CLASSIFY2 header={} taken={} fall={} exits={:?}",
1161                        header, taken, fall, ctx.exits
1162                    );
1163                }
1164                if !taken_is_exit && !ctx.exits.contains(&fall) {
1165                    // Multi-block loop condition. The header's fallthrough
1166                    // may be a pure condition-continuation test block whose
1167                    // taken side is the loop exit and whose fall side rejoins
1168                    // the body (`taken`). javac compiles `while (cH || !cB)`
1169                    // exactly this way:
1170                    //   H: if cH goto body;  B: if cB goto exit;  body: ..; goto H
1171                    // Fold B into the while condition. Without this the body
1172                    // walk re-emits H and B as empty `if`s inside a
1173                    // `while(true)`, dropping the exit and leaving an infinite
1174                    // loop plus an unreachable trailing statement.
1175                    if self.results[fall].stmts.is_empty() {
1176                        if let Term::Cond { cond: cb } = &self.results[fall].term {
1177                            let bs = self.cfg.blocks[fall].succ.clone();
1178                            if bs.len() == 2 && ctx.exits.contains(&bs[1]) && bs[0] == taken {
1179                                let combined = Expr::Bin {
1180                                    op: BinOp::LogOr,
1181                                    l: Box::new(cond.clone()),
1182                                    r: Box::new(negate(cb.clone())),
1183                                    ty: None,
1184                                };
1185                                // The body is the converted region
1186                                // remainder, NOT the taken block's raw
1187                                // statements: when `taken` is wrapped in
1188                                // regions (a try/catch group around the
1189                                // body call), the raw-stmt body silently
1190                                // drops them (jdk11 SocketChannelImpl
1191                                // .implCloseSelectableChannel: the
1192                                // wait()-loop's try/catch(Interrupted
1193                                // Exception) region vanished — bare
1194                                // wait() — 未报告的异常错误). The
1195                                // leading guard If is the condition being
1196                                // folded here; strip it and keep the rest.
1197                                let mut parts = stmt_to_vec(body_stmt);
1198                                if matches!(parts.first(), Some(Stmt::If { .. })) {
1199                                    parts.remove(0);
1200                                }
1201                                let body = if parts.is_empty() {
1202                                    Stmt::Block(self.results[taken].stmts.clone())
1203                                } else {
1204                                    let rest = if parts.len() == 1 {
1205                                        parts.pop().unwrap()
1206                                    } else {
1207                                        Stmt::Block(parts)
1208                                    };
1209                                    strip_trailing_continue(rest)
1210                                };
1211                                if crate::dbg_flag!("JCDC_DBG_LOOP") {
1212                                    eprintln!(
1213                                        "CLASSIFY-CONDCHAIN header={} B={} body={} exits={:?}",
1214                                        header, fall, taken, ctx.exits
1215                                    );
1216                                }
1217                                return Stmt::While {
1218                                    cond: combined,
1219                                    body: Box::new(body),
1220                                };
1221                            }
1222                        }
1223                    }
1224                    // Neither successor leaves the loop: the header's Cond
1225                    // is an interior diamond (e.g. a ternary inside an
1226                    // infinite `for(;;)` whose back edge targets the header
1227                    // top), not an exit test. Keep it in the body.
1228                    return Stmt::While {
1229                        cond: Expr::Const(ConstVal::Int(1)),
1230                        body: Box::new(body_stmt),
1231                    };
1232                }
1233                // The body walk re-emitted the header's own If region; unwrap
1234                // it so the loop reads `while (C) { else-part }`. `stripped`
1235                // is false when the test was NOT the body's first statement
1236                // (the header COMPUTES its condition: `v = it.hasNext();
1237                // if-eqz v`) — the re-emission then stays in the body.
1238                let (inner, exit_stmts, stripped) = split_leading_if(&body_stmt, &cond);
1239                // A top test the structurer wrapped in a TRY (the header
1240                // block sits inside its handler's protected range — jdk26
1241                // Bits.reserveMemory's backoff retry loop tests
1242                // tryReserveOrClean(..) throws InterruptedException under
1243                // its own catch(IE) retry handler): rotating the test into
1244                // `while (C)` evaluates the call OUTSIDE every handler —
1245                // 未报告的异常错误InterruptedException. split_leading_if
1246                // deliberately cannot reach through the Try (the rotated
1247                // cond would still be unprotected), so keep the test where
1248                // the bytecode has it and stay on while(true). The earlier
1249                // implCloseSelectableChannel guard (raw-stmt bodies losing
1250                // the leading If's statements) does not apply: the If lives
1251                // intact inside the Try.
1252
1253                if crate::dbg_flag!("JCDC_DBG_LOOP") {
1254                    eprintln!(
1255                        "CLASSIFY header={} taken_is_exit={} exit_stmts={}",
1256                        header,
1257                        taken_is_exit,
1258                        exit_stmts.len()
1259                    );
1260                }
1261                let inner = strip_trailing_continue(inner);
1262                let while_cond = if taken_is_exit {
1263                    negate(cond.clone())
1264                } else {
1265                    cond.clone()
1266                };
1267                let plain_exit = exit_stmts.is_empty()
1268                    || matches!(exit_stmts.as_slice(), [Stmt::Break(_)])
1269                    || matches!(exit_stmts.as_slice(), [Stmt::Block(b)] if b.is_empty());
1270                if taken_is_exit && plain_exit {
1271                    match try_protected_dowhile(inner, &cond) {
1272                        Ok(dw) => dw,
1273                        Err(inner) => {
1274                            if crate::dbg_flag!("JCDC_DBG_LOOP") {
1275                                eprintln!(
1276                                    "STALEDBG arm=taken_plain header={} stripped={} leading={} cond={:?}",
1277                                    header,
1278                                    stripped,
1279                                    leading_test_present(&inner, &cond, true),
1280                                    cond
1281                                );
1282                            }
1283                            // Strip failed and the header's own test-and-
1284                            // break is still inside the body: the rotated
1285                            // cond would re-read the condition local's
1286                            // STALE pre-loop value — `while (v) {
1287                            // v = it.hasNext(); ... }` is a definite-
1288                            // assignment error (可能尚未初始化变量, weixin
1289                            // ×14k files), and even a pre-initialized v
1290                            // tests the stale register on entry. The
1291                            // in-body break is the real exit; while(true)
1292                            // is the faithful header (same intent as the
1293                            // TRY-wrapped guard above).
1294                            let hc = if !stripped && leading_test_present(&inner, &cond, true) {
1295                                Expr::Const(ConstVal::Int(1))
1296                            } else {
1297                                while_cond
1298                            };
1299                            Stmt::While {
1300                                cond: hc,
1301                                body: Box::new(inner),
1302                            }
1303                        }
1304                    }
1305                } else if taken_is_exit {
1306                    // Exit branch runs statements before leaving:
1307                    // while (true) { if (C_exit) { stmts; break; } body }
1308                    // `cond` IS the exit test here (the taken side exits —
1309                    // negate(cond) inverted the guard: jdk17 CHM
1310                    // comparableClassFor emitted `if (i < len) return null`
1311                    // inside while(true), inverting the loop). The break is
1312                    // only appended when the exit statements do not already
1313                    // terminate (return null + break = unreachable stmt).
1314                    let mut ex = exit_stmts;
1315                    let already_term = matches!(
1316                        ex.last(),
1317                        Some(Stmt::Return(_)) | Some(Stmt::Throw(_)) | Some(Stmt::Break(_))
1318                    );
1319                    if !already_term {
1320                        ex.push(Stmt::Break(None));
1321                    }
1322                    let guard = Stmt::If {
1323                        cond: cond.clone(),
1324                        then_stmt: Box::new(Stmt::Block(ex)),
1325                        else_stmt: None,
1326                    };
1327                    let mut v = vec![guard];
1328                    v.extend(stmt_to_vec(inner));
1329                    Stmt::While {
1330                        cond: Expr::Const(ConstVal::Int(1)),
1331                        body: Box::new(Stmt::Block(v)),
1332                    }
1333                } else {
1334                    // Unusual orientation: the fallthrough side exits.
1335                    if exit_stmts.is_empty()
1336                        || matches!(exit_stmts.as_slice(), [Stmt::Break(_)])
1337                        || matches!(exit_stmts.as_slice(), [Stmt::Block(b)] if b.is_empty())
1338                    {
1339                        // then-side was the exit scaffolding; body is `inner`
1340                        match try_protected_dowhile(inner, &cond) {
1341                            Ok(dw) => dw,
1342                            Err(inner) => {
1343                                // Same stale-phi guard as the taken_is_exit
1344                                // arm above (this orientation keeps `cond`
1345                                // un-negated).
1346                                let hc = if !stripped && leading_test_present(&inner, &cond, false) {
1347                                    Expr::Const(ConstVal::Int(1))
1348                                } else {
1349                                    while_cond
1350                                };
1351                                Stmt::While {
1352                                    cond: hc,
1353                                    body: Box::new(inner),
1354                                }
1355                            }
1356                        }
1357                    } else {
1358                        // then-side carries the body ending with the back
1359                        // edge; fallthrough exits.
1360                        let body2 = strip_trailing_continue(stmts_to_stmt(exit_stmts));
1361                        Stmt::While {
1362                            cond: while_cond,
1363                            body: Box::new(body2),
1364                        }
1365                    }
1366                }
1367            }
1368            Term::Goto | Term::Fallthrough => {
1369                // Possibly do-while: the body ends with a conditional jump
1370                // back to the header (`if (c) continue;` or inverted), or a
1371                // COMPOUND run of such tests ending in the loop exit.
1372                if let Some((stmts, c, exit)) = extract_compound_do_while(&body_stmt) {
1373                    // Same guard as the self-loop compound path above: an
1374                    // extracted exit that is the loop's OWN unlabeled break
1375                    // must be dropped — the do-while condition false already
1376                    // exits, and a `break;` re-emitted OUTSIDE the loop is a
1377                    // stray (it also makes stmt_terminates treat the sequence
1378                    // as terminated, so prune_unreachable drops the real
1379                    // tail: jdk26 Resolver.bind `return this` — 缺少返回语句).
1380                    // SESE's rotated do-whiles hit this shape: the tail test
1381                    // block becomes `if (c) continue; else break;`.
1382                    // return/throw/labeled-break exits stay (real fall-out).
1383                    if matches!(exit, Stmt::Break(None)) {
1384                        return Stmt::DoWhile {
1385                            body: Box::new(Stmt::Block(stmts)),
1386                            cond: c,
1387                        };
1388                    }
1389                    return Stmt::Block(vec![
1390                        Stmt::DoWhile {
1391                            body: Box::new(Stmt::Block(stmts)),
1392                            cond: c,
1393                        },
1394                        exit,
1395                    ]);
1396                }
1397                if let Some((stmts, c)) = extract_trailing_do_while(&body_stmt) {
1398                    return Stmt::DoWhile {
1399                        body: Box::new(Stmt::Block(stmts)),
1400                        cond: c,
1401                    };
1402                }
1403                Stmt::While {
1404                    cond: Expr::Const(ConstVal::Int(1)),
1405                    body: Box::new(body_stmt),
1406                }
1407            }
1408            _ => Stmt::While {
1409                cond: Expr::Const(ConstVal::Int(1)),
1410                body: Box::new(body_stmt),
1411            },
1412        }
1413    }
1414
1415    /// True if `target` is inside the loop currently being classified
1416    /// (approximated: target can reach the header again).
1417    #[allow(dead_code)]
1418    fn loop_contains(&self, header: usize, target: usize) -> bool {
1419        can_reach(self.cfg, target, header, 8192)
1420    }
1421
1422    #[allow(dead_code)]
1423    fn loop_exit_contains(&self, header: usize, target: usize) -> bool {
1424        // The loop currently being classified is the innermost on the stack
1425        // BEFORE it was popped; ctx.exits was consumed. Recompute cheaply:
1426        // target is an exit if it is not reachable-back-to-header within the
1427        // loop members — approximated by checking the parent scope recorded
1428        // in loops stack is empty here; use cfg: header's own successors that
1429        // leave... For the header itself, an exit is any successor that is
1430        // not dominated by header OR is not in the natural loop. Simple
1431        // approximation: a successor s is "in loop" if s can reach header
1432        // again via normal edges.
1433        can_reach(self.cfg, target, header, 4096).not()
1434    }
1435}
1436
1437#[allow(dead_code)]
1438trait NotExt {
1439    fn not(self) -> bool;
1440}
1441impl NotExt for bool {
1442    fn not(self) -> bool {
1443        !self
1444    }
1445}
1446
1447#[allow(dead_code)]
1448fn can_reach(cfg: &Cfg, from: usize, to: usize, budget: usize) -> bool {
1449    if from == to {
1450        return true;
1451    }
1452    let mut seen = HashSet::default();
1453    let mut q = std::collections::VecDeque::new();
1454    q.push_back(from);
1455    seen.insert(from);
1456    let mut n = 0;
1457    while let Some(b) = q.pop_front() {
1458        n += 1;
1459        if n > budget {
1460            return false;
1461        }
1462        for &s in &cfg.blocks[b].succ {
1463            if s == to {
1464                return true;
1465            }
1466            if seen.insert(s) {
1467                q.push_back(s);
1468            }
1469        }
1470    }
1471    false
1472}
1473
1474/// If the body starts with the loop header's own `If` (same condition
1475/// expression), return (else-branch content, then-branch content).
1476/// Otherwise return (body, []).
1477fn stmts_to_stmt(v: Vec<Stmt>) -> Stmt {
1478    match v.len() {
1479        0 => Stmt::Block(vec![]),
1480        1 => v.into_iter().next().unwrap(),
1481        _ => Stmt::Block(v),
1482    }
1483}
1484
1485/// When the loop's top test lives inside a Try in `inner` (the header
1486/// block sits in its handler's protected range — jdk26
1487/// Bits.reserveMemory's backoff retry loop tests
1488/// `tryReserveOrClean(..) throws InterruptedException` under its own
1489/// catch(IE) handler), the `while (C)` rotation would evaluate C
1490/// OUTSIDE every handler — 未报告的异常错误InterruptedException.
1491/// Rewrite as a do-while instead: inject `break` into the embedded
1492/// If's EMPTY arm (the exit side — the exit blocks live outside the
1493/// loop region as its continuation siblings, so the empty arm is the
1494/// fall-out) and loop unconditionally. The sibling tail then stays
1495/// reachable as the do-while's natural completion. Returns None when
1496/// the test is not try-wrapped (plain rotation applies) or when the
1497/// shape is ambiguous (both If arms materialized).
1498// The Err payload IS the statement being rejected — boxing it would
1499// churn every call site for a cold path.
1500#[allow(clippy::result_large_err)]
1501fn try_protected_dowhile(inner: Stmt, cond: &Expr) -> Result<Stmt, Stmt> {
1502    // The exit side of the embedded test: an Empty region conversion
1503    // (empty block / missing else) or an ALREADY-materialized plain
1504    // `break` (the merged-span shape walks the sleep region as the
1505    // then-arm and breaks on the exit side directly).
1506    fn is_empty(s: &Stmt) -> bool {
1507        match s {
1508            Stmt::Block(v) => v.is_empty() || (v.len() == 1 && is_empty(&v[0])),
1509            Stmt::Break(None) => false,
1510            _ => false,
1511        }
1512    }
1513    fn inject(s: &mut Stmt, cond: &Expr, ncond: &Expr) -> bool {
1514        match s {
1515            Stmt::If {
1516                cond: c,
1517                then_stmt,
1518                else_stmt,
1519            } if same_test(c, cond) => {
1520                let then_empty = is_empty(then_stmt);
1521                let else_empty = else_stmt.as_ref().map(|e| is_empty(e)).unwrap_or(true);
1522                if then_empty && !else_empty {
1523                    **then_stmt = Stmt::Break(None);
1524                    true
1525                } else if else_empty && !then_empty {
1526                    *else_stmt = Some(Box::new(Stmt::Break(None)));
1527                    true
1528                } else {
1529                    false
1530                }
1531            }
1532            // Flipped polarity: the emission normalizer dropped an EMPTY
1533            // exit arm and re-oriented the test (`if (X) {body}` where
1534            // X = ¬cond — weixin hu4/k `while (hasNext) { hasNext =
1535            // it.hasNext(); if (hasNext) {…} }`). THEN is the interior
1536            // and ELSE the exit side; only the missing/empty-else shape
1537            // is safely fillable (`if (X) {body} else break;`). An empty
1538            // THEN with a materialized ELSE would need the break AFTER
1539            // the else statements — this filler cannot express that, so
1540            // refuse rather than skip the exit path.
1541            Stmt::If {
1542                cond: c,
1543                then_stmt,
1544                else_stmt,
1545            } if same_test(c, ncond) => {
1546                let else_empty = else_stmt.as_ref().map(|e| is_empty(e)).unwrap_or(true);
1547                if else_empty && !is_empty(then_stmt) {
1548                    *else_stmt = Some(Box::new(Stmt::Break(None)));
1549                    true
1550                } else {
1551                    false
1552                }
1553            }
1554            Stmt::Try { body, .. } | Stmt::TryWithResources { body, .. } => {
1555                inject(body, cond, ncond)
1556            }
1557            Stmt::Block(v) => {
1558                for st in v.iter_mut() {
1559                    match st {
1560                        // The header's condition computation precedes the
1561                        // re-emitted test — scan on.
1562                        Stmt::ExprStmt(_)
1563                        | Stmt::LocalDef { .. }
1564                        | Stmt::Comment(_)
1565                        | Stmt::MonitorEnter(_)
1566                        | Stmt::MonitorExit(_) => continue,
1567                        other => return inject(other, cond, ncond),
1568                    }
1569                }
1570                false
1571            }
1572            Stmt::Labeled { body, .. } | Stmt::Synchronized { body, .. } => {
1573                inject(body, cond, ncond)
1574            }
1575            _ => false,
1576        }
1577    }
1578    let mut b = inner;
1579    let ncond = negate(cond.clone());
1580    let r = inject(&mut b, cond, &ncond);
1581    if crate::dbg_flag!("JCDC_DBG_LOOP") {
1582        eprintln!(
1583            "TPD inject={} cond={} body0={}",
1584            r,
1585            format!("{:?}", cond).chars().take(70).collect::<String>(),
1586            match &b {
1587                Stmt::Block(v) => v
1588                    .iter()
1589                    .take(3)
1590                    .map(|x| match x {
1591                        Stmt::If { cond: c, then_stmt, else_stmt } => format!(
1592                            "if({} )then_empty={} else={}",
1593                            format!("{:?}", c).chars().take(50).collect::<String>(),
1594                            matches!(&**then_stmt, Stmt::Block(bb) if bb.is_empty()),
1595                            else_stmt.is_some()
1596                        ),
1597                        Stmt::LocalDef { .. } => "def".to_string(),
1598                        Stmt::ExprStmt(_) => "expr".to_string(),
1599                        _ => "other".to_string(),
1600                    })
1601                    .collect::<Vec<_>>()
1602                    .join("|"),
1603                Stmt::If { cond: c, .. } => format!("topif({:?})", c).chars().take(60).collect(),
1604                _ => "nonblock".to_string(),
1605            }
1606        );
1607    }
1608    if r {
1609        Ok(Stmt::DoWhile {
1610            body: Box::new(b),
1611            cond: Expr::Const(ConstVal::Int(1)),
1612        })
1613    } else {
1614        Err(b)
1615    }
1616}
1617
1618/// Unwrap the header's re-emitted `if (cond) {exit} else {body}` from the
1619/// top of a loop body. The bool reports whether the strip happened: false
1620/// means the test was NOT the body's first statement (a condition-computing
1621/// header leads with its Assign) and the re-emission is still inside.
1622fn split_leading_if(body: &Stmt, cond: &Expr) -> (Stmt, Vec<Stmt>, bool) {
1623    let items = match body {
1624        Stmt::Block(v) => v,
1625        other => {
1626            return match other {
1627                Stmt::If {
1628                    cond: c,
1629                    then_stmt,
1630                    else_stmt,
1631                } if c == cond => (
1632                    else_stmt
1633                        .as_ref()
1634                        .map(|e| (**e).clone())
1635                        .unwrap_or(Stmt::Block(vec![])),
1636                    stmt_to_vec((**then_stmt).clone()),
1637                    true,
1638                ),
1639                _ => (other.clone(), vec![], false),
1640            };
1641        }
1642    };
1643    if let Some(Stmt::If {
1644        cond: c,
1645        then_stmt,
1646        else_stmt,
1647    }) = items.first()
1648    {
1649        {
1650            if c == cond {
1651                let inner = if items.len() == 1 {
1652                    else_stmt
1653                        .as_ref()
1654                        .map(|e| (**e).clone())
1655                        .unwrap_or(Stmt::Block(vec![]))
1656                } else {
1657                    let mut rest = vec![else_stmt
1658                        .as_ref()
1659                        .map(|e| (**e).clone())
1660                        .unwrap_or(Stmt::Block(vec![]))];
1661                    rest.extend(items[1..].iter().cloned());
1662                    Stmt::Block(rest)
1663                };
1664                return (inner, stmt_to_vec((**then_stmt).clone()), true);
1665            }
1666        }
1667    }
1668    (body.clone(), vec![], false)
1669}
1670
1671/// Canonical form of a boolean test for STRUCTURAL comparison. The header
1672/// Term keeps the raw dex idiom (`X == 0` for if-eqz on a boolean
1673/// register) while the re-emitted in-body If often carries the simplified
1674/// `X` / `!X` — raw `==` misses the pair (weixin allocRow cursor-loop
1675/// family stayed stale-rotated without this). Folds `X == 0` → `!X`,
1676/// `X != 0` → `X`, and double negations.
1677fn canon_test(e: &Expr) -> Expr {
1678    fn is_zero(c: &Expr) -> bool {
1679        matches!(c, Expr::Const(ConstVal::Int(0)))
1680    }
1681    match e {
1682        Expr::Bin {
1683            op: BinOp::Eq,
1684            l,
1685            r,
1686            ..
1687        } => {
1688            if is_zero(r) {
1689                Expr::Un {
1690                    op: UnOp::Not,
1691                    e: Box::new(canon_test(l)),
1692                }
1693            } else if is_zero(l) {
1694                Expr::Un {
1695                    op: UnOp::Not,
1696                    e: Box::new(canon_test(r)),
1697                }
1698            } else {
1699                e.clone()
1700            }
1701        }
1702        Expr::Bin {
1703            op: BinOp::Ne,
1704            l,
1705            r,
1706            ..
1707        } => {
1708            if is_zero(r) {
1709                canon_test(l)
1710            } else if is_zero(l) {
1711                canon_test(r)
1712            } else {
1713                e.clone()
1714            }
1715        }
1716        Expr::Un {
1717            op: UnOp::Not,
1718            e: inner,
1719        } => match canon_test(inner) {
1720            Expr::Un {
1721                op: UnOp::Not,
1722                e: x,
1723            } => *x,
1724            other => Expr::Un {
1725                op: UnOp::Not,
1726                e: Box::new(other),
1727            },
1728        },
1729        _ => e.clone(),
1730    }
1731}
1732
1733/// Structural equality of two boolean tests modulo the zero-comparison
1734/// idiom (`X == 0` ≡ `!X`).
1735fn same_test(a: &Expr, b: &Expr) -> bool {
1736    if a == b {
1737        return true;
1738    }
1739    canon_test(a) == canon_test(b)
1740}
1741
1742/// True when the loop body still carries the header block's re-emitted exit
1743/// test at its leading position: zero or more condition-computing simple
1744/// statements followed by `if (cond) …` (possibly under region wrappers).
1745/// The first If/control statement encountered is decisive — a later
1746/// same-condition diamond deeper in the body does not count.
1747///
1748/// `exit_on_taken` orients the test: the taken_is_exit arm of classify_loop
1749/// has the EXIT on the term's taken side (`if (c) {exit}`), the fall-exit
1750/// arm has it on the else (`if (c) {interior} else {exit}`). The exit side
1751/// must TERMINATE (break/return/throw/labeled-continue — recursively
1752/// through blocks and both-arm-terminating ifs); the interior side may end
1753/// anywhere (falling to the loop bottom IS the back edge). Without a
1754/// terminating exit side, `while (true)` would spin forever — the
1755/// empty-exit-arm shapes belong to try_protected_dowhile's break
1756/// injection, which runs first.
1757fn leading_test_present(s: &Stmt, cond: &Expr, exit_on_taken: bool) -> bool {
1758    fn terminates(t: &Stmt) -> bool {
1759        match t {
1760            Stmt::Break(_) | Stmt::Return(_) | Stmt::Throw(_) => true,
1761            // A LABELED continue targets an enclosing loop — it leaves
1762            // this one, so the in-body exit is real (weixin aa2/b:
1763            // `if (!hasNext) continue L1;`). A BARE continue is this
1764            // loop's own back edge — NOT an exit (while(true) would
1765            // re-spin the test forever); it must stay on the old path.
1766            Stmt::Continue(Some(_)) => true,
1767            Stmt::Block(v) => v.last().map(terminates).unwrap_or(false),
1768            // An if whose BOTH arms leave the loop terminates too.
1769            Stmt::If {
1770                then_stmt,
1771                else_stmt,
1772                ..
1773            } => {
1774                terminates(then_stmt)
1775                    && else_stmt
1776                        .as_ref()
1777                        .map(|e| terminates(e))
1778                        .unwrap_or(false)
1779            }
1780            Stmt::Labeled { body, .. } => terminates(body),
1781            _ => false,
1782        }
1783    }
1784    fn term_opt(o: &Option<Box<Stmt>>) -> bool {
1785        o.as_ref().map(|e| terminates(e)).unwrap_or(false)
1786    }
1787    fn go(s: &Stmt, cond: &Expr, ncond: &Expr, exit_on_taken: bool) -> bool {
1788        match s {
1789            Stmt::If {
1790                cond: c,
1791                then_stmt,
1792                else_stmt,
1793            } => {
1794                let direct = same_test(c, cond);
1795                let flipped = !direct && same_test(c, ncond);
1796                if !direct && !flipped {
1797                    return false;
1798                }
1799                // Exit side: a direct-oriented test carries the term's
1800                // taken side in THEN; a flipped test swaps the arms.
1801                // `exit_on_taken` says which term side exits in the
1802                // calling classify arm.
1803                let exit_is_then = direct == exit_on_taken;
1804                if exit_is_then {
1805                    terminates(then_stmt)
1806                } else {
1807                    term_opt(else_stmt)
1808                }
1809            }
1810            Stmt::Try { body, .. }
1811            | Stmt::TryWithResources { body, .. }
1812            | Stmt::Labeled { body, .. }
1813            | Stmt::Synchronized { body, .. } => go(body, cond, ncond, exit_on_taken),
1814            Stmt::Block(v) => {
1815                for st in v {
1816                    match st {
1817                        Stmt::ExprStmt(_)
1818                        | Stmt::LocalDef { .. }
1819                        | Stmt::Comment(_)
1820                        | Stmt::MonitorEnter(_)
1821                        | Stmt::MonitorExit(_) => continue,
1822                        other => return go(other, cond, ncond, exit_on_taken),
1823                    }
1824                }
1825                false
1826            }
1827            _ => false,
1828        }
1829    }
1830    go(s, cond, &negate(cond.clone()), exit_on_taken)
1831}
1832
1833/// True when the body's first meaningful statement assigns the SAME local
1834/// the header condition tests (`X = call(); …` where cond = ±X) — the
1835/// compute-then-test shape whose rotated `while (X)` header would read X's
1836/// stale pre-loop value.
1837fn cond_var_reassigned_first(s: &Stmt, cond: &Expr) -> bool {
1838    fn cond_local(c: &Expr) -> Option<u32> {
1839        match c {
1840            Expr::Local { var, .. } => Some(*var),
1841            Expr::Un {
1842                op: UnOp::Not,
1843                e,
1844            } => cond_local(e),
1845            // boolean-zero idiom: `X == 0` / `0 != X` (dex if-eqz on a
1846            // boolean register keeps this raw form in the Term).
1847            Expr::Bin {
1848                op: BinOp::Eq | BinOp::Ne,
1849                l,
1850                r,
1851                ..
1852            } => {
1853                if matches!(&**r, Expr::Const(ConstVal::Int(0))) {
1854                    cond_local(l)
1855                } else if matches!(&**l, Expr::Const(ConstVal::Int(0))) {
1856                    cond_local(r)
1857                } else {
1858                    None
1859                }
1860            }
1861            _ => None,
1862        }
1863    }
1864    let Some(v) = cond_local(cond) else {
1865        return false;
1866    };
1867    fn go(s: &Stmt, v: u32) -> bool {
1868        match s {
1869            Stmt::ExprStmt(Expr::Assign { target, .. }) => {
1870                matches!(&**target, Expr::Local { var, .. } if *var == v)
1871            }
1872            Stmt::LocalDef {
1873                var, init: Some(_), ..
1874            } => *var == v,
1875            Stmt::Block(b) => b.first().map(|f| go(f, v)).unwrap_or(false),
1876            Stmt::Try { body, .. }
1877            | Stmt::TryWithResources { body, .. }
1878            | Stmt::Labeled { body, .. }
1879            | Stmt::Synchronized { body, .. } => go(body, v),
1880            _ => false,
1881        }
1882    }
1883    go(s, v)
1884}
1885
1886/// Remove a trailing `continue;` (loop back-edge) from a statement list.
1887fn strip_trailing_continue(s: Stmt) -> Stmt {
1888    match s {
1889        Stmt::Block(mut v) => {
1890            while matches!(v.last(), Some(Stmt::Continue(None))) {
1891                v.pop();
1892            }
1893            if v.len() == 1 && matches!(v[0], Stmt::Block(_)) {
1894                let inner = match v.pop().unwrap() {
1895                    Stmt::Block(inner) => inner,
1896                    _ => unreachable!(),
1897                };
1898                return strip_trailing_continue(Stmt::Block(inner));
1899            }
1900            Stmt::Block(v)
1901        }
1902        Stmt::Continue(None) => Stmt::Block(vec![]),
1903        other => other,
1904    }
1905}
1906
1907pub fn negate(e: Expr) -> Expr {
1908    match e {
1909        Expr::Un { op: UnOp::Not, e } => *e,
1910        // De Morgan for the short-circuit operators: !(A && B) =
1911        // !A || !B, !(A || B) = !A && !B — recursing through negate
1912        // keeps comparison operands cleanly inverted. An op-only swap
1913        // (the historical invert() LogAnd<->LogOr mapping) silently
1914        // changed the condition's meaning: Arrays.equals' folded
1915        // `a == null || a2 == null` inverted to `a == null && a2 ==
1916        // null`, NPE-ing the a2==null path.
1917        Expr::Bin {
1918            op: op @ (crate::ir::expr::BinOp::LogAnd | crate::ir::expr::BinOp::LogOr),
1919            l,
1920            r,
1921            ty,
1922        } => {
1923            let swapped = if matches!(op, crate::ir::expr::BinOp::LogAnd) {
1924                crate::ir::expr::BinOp::LogOr
1925            } else {
1926                crate::ir::expr::BinOp::LogAnd
1927            };
1928            Expr::Bin {
1929                op: swapped,
1930                l: Box::new(negate(*l)),
1931                r: Box::new(negate(*r)),
1932                ty,
1933            }
1934        }
1935        Expr::Bin { op, l, r, ty } => {
1936            if let Some(inv) = op.invert() {
1937                Expr::Bin { op: inv, l, r, ty }
1938            } else {
1939                Expr::Un {
1940                    op: UnOp::Not,
1941                    e: Box::new(Expr::Bin { op, l, r, ty }),
1942                }
1943            }
1944        }
1945        other => Expr::Un {
1946            op: UnOp::Not,
1947            e: Box::new(other),
1948        },
1949    }
1950}
1951
1952/// True for a bare `continue;` (or a block wrapping just one).
1953fn is_continue_stmt(s: &Stmt) -> bool {
1954    matches!(s, Stmt::Continue(None))
1955        || matches!(s, Stmt::Block(v) if v.len() == 1 && matches!(v[0], Stmt::Continue(None)))
1956}
1957
1958/// Detect a COMPOUND trailing do-while condition: the body ends with a run of
1959/// `if (c1) continue;` ... `if (cN) continue; else <exit>;`. javac emits this
1960/// for `do { body } while (c1 || ... || cN);` followed by the loop exit when
1961/// the condition is a short-circuit `||` whose tests each jump back to the
1962/// body head (e.g. `Random.internalNextInt`: `while (r < origin || r >=
1963/// bound)`). Fold the run into a single do-while whose condition is the `||`
1964/// of the tests, returning the body statements, that condition, and the exit
1965/// action (`<exit>`) to emit right after the loop.
1966fn extract_compound_do_while(body: &Stmt) -> Option<(Vec<Stmt>, Expr, Stmt)> {
1967    // Flatten nested straight-line blocks: the loop body may group the first
1968    // condition test(s) inside a nested Block (the self-loop header's region)
1969    // while later tests are siblings. Splicing pure sequence blocks is safe.
1970    fn flatten_seq(s: &Stmt) -> Vec<Stmt> {
1971        match s {
1972            Stmt::Block(v) => {
1973                let mut out = Vec::new();
1974                for x in v {
1975                    out.extend(flatten_seq(x));
1976                }
1977                out
1978            }
1979            other => vec![other.clone()],
1980        }
1981    }
1982    let mut stmts = flatten_seq(body);
1983    // Unnest the rendered if/else-if CHAIN spine: javac's bottom-tested
1984    // `if (c1) continue; if (c2) continue; ... else exit;` disjunct run
1985    // converts to ONE nested If whose else arms hold the remaining
1986    // tests — opaque to the backwards scan below, which would fold only
1987    // c1 and push the whole c2..cN chain out of the loop as the "exit"
1988    // (jdk26 ML_DSA.signInternal: `do { expandMask.. } while
1989    // (vectorNormBound(z,..));` with the c2/hint chain — and its
1990    // continue/break — stranded after the loop, continue 在 loop 外部
1991    // ×2). Splitting each continue-then If back into siblings restores
1992    // the flat run; semantics are identical (the then arm is a bare
1993    // continue with no fallthrough, and the LAST else is preserved as
1994    // the final statement's else = the loop's exit route).
1995    {
1996        let mut flat: Vec<Stmt> = Vec::new();
1997        for st in stmts.into_iter() {
1998            let mut cur = st;
1999            loop {
2000                match cur {
2001                    Stmt::If {
2002                        cond,
2003                        then_stmt,
2004                        else_stmt: Some(e),
2005                    } if is_continue_stmt(&then_stmt) => {
2006                        flat.push(Stmt::If {
2007                            cond,
2008                            then_stmt,
2009                            else_stmt: None,
2010                        });
2011                        cur = *e;
2012                    }
2013                    other => {
2014                        flat.push(other);
2015                        break;
2016                    }
2017                }
2018            }
2019        }
2020        stmts = flat;
2021    }
2022    let n = stmts.len();
2023    if n < 2 {
2024        return None;
2025    }
2026    // Last statement: `if (c_last) continue; else <exit>` — the exit is the
2027    // loop's fall-out (a return/throw/break), NOT another continue.
2028    let (c_last, exit) = match &stmts[n - 1] {
2029        Stmt::If {
2030            cond,
2031            then_stmt,
2032            else_stmt: Some(e),
2033        } if is_continue_stmt(then_stmt) && !is_continue_stmt(e) => (cond.clone(), (**e).clone()),
2034        _ => return None,
2035    };
2036    // Walk backwards over preceding `if (ci) continue;` (no else) tests.
2037    let mut conds = vec![c_last];
2038    let mut i = n - 1;
2039    while i >= 1 {
2040        match &stmts[i - 1] {
2041            Stmt::If {
2042                cond,
2043                then_stmt,
2044                else_stmt: None,
2045            } if is_continue_stmt(then_stmt) => {
2046                conds.push(cond.clone());
2047                i -= 1;
2048            }
2049            _ => break,
2050        }
2051    }
2052    let body_stmts: Vec<Stmt> = stmts[..i].to_vec();
2053    // cond = c1 || c2 || ... || c_last (conds currently reversed).
2054    let mut it = conds.into_iter().rev();
2055    let mut cond = it.next().unwrap();
2056    for c in it {
2057        cond = Expr::Bin {
2058            op: BinOp::LogOr,
2059            l: Box::new(c),
2060            r: Box::new(cond),
2061            ty: None,
2062        };
2063    }
2064    Some((body_stmts, cond, exit))
2065}
2066
2067/// Detect a trailing do-while condition at the end of a loop body:
2068/// `if (c) continue;` → cond c, or `if (c) {} else continue;` → cond !c.
2069fn extract_trailing_do_while(body: &Stmt) -> Option<(Vec<Stmt>, Expr)> {
2070    let mut stmts = match body {
2071        Stmt::Block(v) => v.clone(),
2072        other => vec![other.clone()],
2073    };
2074    if stmts.is_empty() {
2075        return None;
2076    }
2077    let n = stmts.len() - 1;
2078    if let Stmt::If {
2079        cond,
2080        then_stmt,
2081        else_stmt,
2082    } = &stmts[n]
2083    {
2084        let then_is_cont = |s: &Stmt| {
2085            matches!(s, Stmt::Continue(None))
2086                || matches!(s, Stmt::Block(v) if v.len() == 1 && matches!(v[0], Stmt::Continue(None)))
2087        };
2088        if then_is_cont(then_stmt) && else_stmt.is_none() {
2089            let c = cond.clone();
2090            stmts.pop();
2091            return Some((stmts, c));
2092        }
2093        if else_stmt.as_ref().map(|e| then_is_cont(e)).unwrap_or(false)
2094            && (matches!(&**then_stmt, Stmt::Block(v) if v.is_empty()))
2095        {
2096            let c = negate(cond.clone());
2097            stmts.pop();
2098            return Some((stmts, c));
2099        }
2100    }
2101    None
2102}
2103
2104/// If the body ends with `if (c) continue;` (Continue(None), no else),
2105/// strip it and return (body without trailing if, c).
2106#[allow(dead_code)]
2107fn extract_trailing_continue_if(body: &Stmt) -> Option<(Vec<Stmt>, Expr)> {
2108    let mut stmts = match body {
2109        Stmt::Block(v) => v.clone(),
2110        other => vec![other.clone()],
2111    };
2112    if stmts.is_empty() {
2113        return None;
2114    }
2115    let n = stmts.len() - 1;
2116    // Direct trailing `if (c) continue;`.
2117    let is_direct = match &stmts[n] {
2118        Stmt::If {
2119            then_stmt,
2120            else_stmt: None,
2121            ..
2122        } => {
2123            matches!(&**then_stmt, Stmt::Continue(None))
2124                || matches!(&**then_stmt, Stmt::Block(tb) if tb.len() == 1 && matches!(tb[0], Stmt::Continue(None)))
2125        }
2126        _ => false,
2127    };
2128    if is_direct {
2129        if let Stmt::If { cond, .. } = stmts.pop().unwrap() {
2130            return Some((stmts, cond));
2131        }
2132    }
2133    // Descend into a trailing block.
2134    if matches!(stmts.last(), Some(Stmt::Block(_))) {
2135        let last = stmts.pop().unwrap();
2136        if let Some((inner, c)) = extract_trailing_continue_if(&last) {
2137            if !inner.is_empty() {
2138                stmts.push(Stmt::Block(inner));
2139            }
2140            return Some((stmts, c));
2141        } else {
2142            stmts.push(last);
2143        }
2144    }
2145    None
2146}
2147
2148/// Separate the first Basic block's statements from the rest of a body.
2149/// (Bodies built by `walk` start with the header's statements.)
2150#[allow(dead_code)]
2151fn split_first_block(body: Stmt) -> (Stmt, Option<usize>) {
2152    // The body is whatever the walk produced; we keep it intact — header
2153    // statements were already woven in by conv(Basic). Return as-is.
2154    (body, None)
2155}
2156
2157fn stmt_to_vec(s: Stmt) -> Vec<Stmt> {
2158    match s {
2159        Stmt::Block(v) => v,
2160        other => vec![other],
2161    }
2162}
2163
2164/// Strip `Labeled` wrappers no Break/Continue ended up referencing.
2165/// Wrappers are attached unconditionally during conv (a break born in a
2166/// copied/follow region lands after its loop's region-close, so the
2167/// at-close used_labels check orphaned it); after the full conversion
2168/// used_labels is complete and the unused wrappers come off. An unused
2169/// wrapper's body replaces it in-place (a Block body stays a nested
2170/// block — downstream cleanup flattens).
2171fn prune_unused_labels(st: &mut Stmt, used: &HashSet<String>) {
2172    loop {
2173        let take = matches!(st, Stmt::Labeled { label, .. } if !used.contains(label));
2174        if !take {
2175            break;
2176        }
2177        let inner = match std::mem::replace(st, Stmt::Block(Vec::new())) {
2178            Stmt::Labeled { body, .. } => *body,
2179            _ => unreachable!(),
2180        };
2181        *st = inner;
2182    }
2183    match st {
2184        Stmt::Block(v) => v.iter_mut().for_each(|x| prune_unused_labels(x, used)),
2185        Stmt::Labeled { body, .. } => prune_unused_labels(body, used),
2186        Stmt::If { then_stmt, else_stmt, .. } => {
2187            prune_unused_labels(then_stmt, used);
2188            if let Some(e) = else_stmt {
2189                prune_unused_labels(e, used);
2190            }
2191        }
2192        Stmt::While { body, .. }
2193        | Stmt::DoWhile { body, .. }
2194        | Stmt::ForEach { body, .. }
2195        | Stmt::Synchronized { body, .. } => prune_unused_labels(body, used),
2196        Stmt::For { init, body, .. } => {
2197            init.iter_mut().for_each(|x| prune_unused_labels(x, used));
2198            prune_unused_labels(body, used);
2199        }
2200        Stmt::Switch { cases, default, .. } => {
2201            for c in cases.iter_mut() {
2202                c.body.iter_mut().for_each(|x| prune_unused_labels(x, used));
2203            }
2204            if let Some(d) = default {
2205                prune_unused_labels(d, used);
2206            }
2207        }
2208        Stmt::Try { body, catches, finally }
2209        | Stmt::TryWithResources { body, catches, finally, .. } => {
2210            prune_unused_labels(body, used);
2211            for c in catches.iter_mut() {
2212                prune_unused_labels(&mut c.body, used);
2213            }
2214            if let Some(f) = finally {
2215                prune_unused_labels(f, used);
2216            }
2217        }
2218        _ => {}
2219    }
2220}