Skip to content

Commit 0b72e71

Browse files
committed
classdec/method: records, called synthetic helpers, and thrown-value typing
Three defects found by compiling the offline corpus' emitted source with the era javac (the record one was reported as "invalid canonical constructor"): 1. A record's canonical constructor carried `super();`. The bytecode calls `Record.<init>`, but the source form may not (JLS 8.10.4.1) — javac rejects the class. Strip that prologue for record `<init>` bodies (the implicit canonical ctor is not emitted at all, so only explicit ones are affected). 2. A synthetic helper that the emitted code still CALLS was hidden by the `--synthetic` default. JetBrains' @NotNull instrumentation adds `private static void $$$reportNull$$$0(int)` and every null check calls it, so the recompiled class had an unresolved symbol. The skip rule now rescues a referenced synthetic method — but only real helpers: bridges, the javac-inlined families (access$, lambda$, $deserializeLambda$), constructors, record components and the enum `$values` builder stay hidden (their call sites are rewritten or are implicit in the source). 3. A value merged across branches keeps the plain-Object join, which is fine for reads but not for a throw: `throw stack3;` with an Object declaration does not compile, and casting to Throwable trips the checked- exception rule. A THROWN local now takes the pool-aware least upper bound of its evidence (IllegalStateException + IllegalArgumentException → RuntimeException), computed only for that consumer — narrowing other merged vars could change which overload a call binds to. Verified on the record that motivated it (emits `RuntimeException stack3; … throw stack3;` and now compiles against the era javac with a stub for the annotation package) and with the full gates: features 59/59 walk + 59/59 SESE, offline corpus back to its one-file baseline (the record error is gone), rt.jar unchanged and deterministic, workspace tests green. The JDK 28 image changes in exactly 3 units, each an intended consequence (array-type fix variable identity, thrown-value widening).
1 parent 7030d26 commit 0b72e71

2 files changed

Lines changed: 202 additions & 28 deletions

File tree

‎crates/decompiler/src/classdec.rs‎

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,29 @@ let text = Printer::new(&__ctx, &vt).with_indent(indent + 1).into_string(&body)
14541454
Ok(())
14551455
}
14561456

1457+
/// `(name, descriptor)` of every method this class's bytecode references on
1458+
/// ITSELF (`Methodref`/`InterfaceMethodref` owned by this class) — used to
1459+
/// keep synthetic helpers the emitted source still calls.
1460+
fn referenced_self_methods(pc: &PoolClass) -> HashSet<(String, String)> {
1461+
let mut out = HashSet::new();
1462+
for e in &pc.cf.constant_pool {
1463+
let (ci, nti) = match e {
1464+
jcdc_classfile::ConstantPoolEntry::Methodref(m) => (m.class_index, m.name_and_type_index),
1465+
jcdc_classfile::ConstantPoolEntry::InterfaceMethodref(m) => {
1466+
(m.class_index, m.name_and_type_index)
1467+
}
1468+
_ => continue,
1469+
};
1470+
if pc.class_name(ci) != Some(pc.internal_name.as_str()) {
1471+
continue;
1472+
}
1473+
if let Some((n, d)) = pc.name_and_type(nti) {
1474+
out.insert((n.to_string(), d.to_string()));
1475+
}
1476+
}
1477+
out
1478+
}
1479+
14571480
fn methods_to_skip(
14581481
pc: &PoolClass,
14591482
pool: &ClassPool,
@@ -1463,17 +1486,56 @@ fn methods_to_skip(
14631486
) -> HashSet<usize> {
14641487
let mut skip = HashSet::new();
14651488
let lambda_methods = collect_lambda_methods(pc);
1489+
// Synthetic helpers the emitted code still CALLS must be declared.
1490+
// JetBrains' @NotNull instrumentation adds `private static void
1491+
// $$$reportNull$$$0(int)` and every null check calls it; hiding it left
1492+
// the recompiled class with an unresolved symbol (for a record javac
1493+
// reports "invalid canonical constructor" first, so the missing helper
1494+
// surfaced under a misleading name). Bridges and the javac-inlined
1495+
// families (access$, lambda$) stay hidden: jcdc rewrites their call
1496+
// sites, so the output references nothing.
1497+
let referenced: HashSet<(String, String)> = if opts.show_synthetic {
1498+
HashSet::new()
1499+
} else {
1500+
referenced_self_methods(pc)
1501+
};
14661502
for mi in 0..pc.cf.methods.len() {
14671503
let m = &pc.cf.methods[mi];
14681504
let name = pc.utf8(m.name_index).unwrap_or("").to_string();
1505+
// A synthetic helper the emitted code still CALLS must be declared
1506+
// (see `referenced`: JetBrains' `$$$reportNull$$$0` is synthetic,
1507+
// private and every null check calls it). Bridges and the
1508+
// javac-inlined families stay hidden — jcdc rewrites their sites.
1509+
let bridge = m.access_flags.contains(MethodAccessFlags::BRIDGE);
1510+
let synthetic = m.access_flags.contains(MethodAccessFlags::SYNTHETIC);
1511+
let inlined_family = name.starts_with("access$")
1512+
|| name.starts_with("lambda$")
1513+
|| name.contains("$lambda$")
1514+
|| name.starts_with("$deserializeLambda$");
1515+
// Constructors and record components are implicit in the source
1516+
// (a record's canonical ctor and its accessors, an inner class's
1517+
// outer-capture ctor): emitting them is legal but not what the
1518+
// source says, so only real HELPERS are rescued.
1519+
let implicit_in_source = name == "<init>"
1520+
// enum's generated backing-array builder: `values()` is printed
1521+
// with its own clone() body, so nothing in the OUTPUT calls this.
1522+
|| name == "$values"
1523+
|| (is_record && {
1524+
let comps = record_component_names(pc);
1525+
comps.iter().any(|c| c == &name)
1526+
});
1527+
let called = !bridge
1528+
&& !inlined_family
1529+
&& !implicit_in_source
1530+
&& pc
1531+
.method_desc(mi)
1532+
.map(|d| referenced.contains(&(name.clone(), d.to_string())))
1533+
.unwrap_or(false);
14691534
if name == "<clinit>" || lambda_methods.contains(&mi) {
14701535
skip.insert(mi);
14711536
continue;
14721537
}
1473-
if !opts.show_synthetic
1474-
&& (m.access_flags.contains(MethodAccessFlags::SYNTHETIC)
1475-
|| m.access_flags.contains(MethodAccessFlags::BRIDGE))
1476-
{
1538+
if !opts.show_synthetic && (synthetic || bridge) && !called {
14771539
skip.insert(mi);
14781540
continue;
14791541
}
@@ -1496,7 +1558,7 @@ fn methods_to_skip(
14961558
continue;
14971559
}
14981560
}
1499-
if is_record && is_synthetic_record_method(pc, pool, mi) {
1561+
if is_record && is_synthetic_record_method(pc, pool, mi) && !called {
15001562
skip.insert(mi);
15011563
continue;
15021564
}

‎crates/decompiler/src/method.rs‎

Lines changed: 135 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ pub struct MethodBody {
2525
/// Decompile one method. Returns Err only for malformed input; unsupported
2626
/// constructs degrade to comments inside the statement tree.
2727
#[track_caller]
28+
/// Drop the `super(...)` prologue a record's canonical constructor carries
29+
/// in bytecode (`invokespecial java/lang/Record.<init>`). Returns true when
30+
/// a statement was removed.
31+
fn strip_record_super_call(body: &mut Stmt) {
32+
fn is_super_ctor(s: &Stmt) -> bool {
33+
matches!(s, Stmt::ExprStmt(Expr::Method { name, is_super: true, .. })
34+
if &*name == "<init>")
35+
}
36+
if let Stmt::Block(v) = body {
37+
v.retain(|x| !is_super_ctor(x));
38+
} else if is_super_ctor(body) {
39+
*body = Stmt::Block(Vec::new());
40+
}
41+
}
42+
2843
pub fn decompile_method(
2944
pc: &PoolClass,
3045
pool: &ClassPool,
@@ -735,7 +750,7 @@ pub fn decompile_method(
735750
// Type inference for synthetic (no-LVT) variables, then fix the embedded
736751
// types in Local expressions.
737752
let ret_ty = desc.ret.clone();
738-
infer_var_types(&mut vt, &mut body, &ret_ty, pc);
753+
infer_var_types(&mut vt, &mut body, &ret_ty, pc, pool);
739754
// After synthetic-var inference: stack-merge locals now carry their
740755
// Int type in the VarTable (PKCS12KeyStore.isPasswordless'
741756
// `return var6_581` under a boolean return).
@@ -829,6 +844,16 @@ pub fn decompile_method(
829844
let n = errors.len();
830845
prepend_comment(&mut body, format!("$JCDC: {} block(s) failed to decompile", n));
831846
}
847+
848+
// A record's canonical constructor must not spell `super(...)`: its
849+
// bytecode calls `Record.<init>`, but the source form may not
850+
// (JLS 8.10.4.1) — javac rejects the class with "invalid canonical
851+
// constructor in record". Only EXPLICIT canonical constructors reach
852+
// this point (the implicit one is not emitted), and the call is the
853+
// constructor prologue, i.e. a top-level statement.
854+
if pc.is_record() && pc.method_name(m_idx) == Some("<init>") {
855+
strip_record_super_call(&mut body);
856+
}
832857
// Post-pipeline safety net for the matexit splices: the conversion-
833858
// time trial cannot see pathologies CREATED BY THE POST-CONVERT
834859
// PASSES (classify_loop/rotate_empty_then rotations turning a spliced
@@ -4573,11 +4598,18 @@ enum Evidence {
45734598
BoolCandidate,
45744599
}
45754600

4576-
fn infer_var_types(vt: &mut VarTable, body: &mut Stmt, ret_ty: &JavaType, pc: &PoolClass) {
4601+
fn infer_var_types(
4602+
vt: &mut VarTable,
4603+
body: &mut Stmt,
4604+
ret_ty: &JavaType,
4605+
pc: &PoolClass,
4606+
pool: &jcdc_jvm::ClassPool,
4607+
) {
45774608
let n = vt.vars.len();
45784609
let mut ev: Vec<Vec<Evidence>> = vec![Vec::new(); n];
45794610
let mut bool_vars: std::collections::HashSet<u32> = std::collections::HashSet::new();
4580-
gather_evidence(body, &mut ev, ret_ty, &mut bool_vars);
4611+
let mut throw_vars: std::collections::HashSet<u32> = std::collections::HashSet::new();
4612+
gather_evidence(body, &mut ev, ret_ty, &mut bool_vars, &mut throw_vars);
45814613
let mut changed: Vec<Option<JavaType>> = vec![None; n];
45824614
for (v, evs) in ev.iter().enumerate() {
45834615
if !vt.vars[v].synthetic_name || vt.vars[v].is_param {
@@ -4616,6 +4648,7 @@ fn infer_var_types(vt: &mut VarTable, body: &mut Stmt, ret_ty: &JavaType, pc: &P
46164648
if crate::dbg_flag!("JCDC_DBG_MERGE") && vt.stack_vars.contains(&(v as u32)) {
46174649
eprintln!("INFER stackvar v={} old={:?} t={:?} wide={}", v, old, t, vt.wide_stack_vars.contains(&(v as u32)));
46184650
}
4651+
let mut throw_lub: Option<JavaType> = None;
46194652
if vt.wide_stack_vars.contains(&(v as u32)) {
46204653
// Branches disagreed (or carried a generic/Object value);
46214654
// keep the wide type so every assignment type-checks —
@@ -4636,6 +4669,28 @@ fn infer_var_types(vt: &mut VarTable, body: &mut Stmt, ret_ty: &JavaType, pc: &P
46364669
matches!(t, JavaType::Object(n) if n == "java/lang/Object")
46374670
};
46384671
let uniform = ev_types.windows(2).all(|w| w[0] == w[1]);
4672+
// A pool-aware join that lands on a REAL common superclass
4673+
// (IllegalStateException + IllegalArgumentException →
4674+
// RuntimeException) is just as usable as a uniform type:
4675+
// both assignments convert to it. Without this the merged
4676+
// value stays Object, and `throw stack3;` then fails to
4677+
// compile (JetBrains @NotNull `$$$reportNull$$$0`).
4678+
let lub = {
4679+
let mut acc: Option<JavaType> = None;
4680+
for t in &ev_types {
4681+
acc = Some(match acc {
4682+
None => (*t).clone(),
4683+
Some(a) => join_types_pool(&a, t, pool),
4684+
});
4685+
}
4686+
acc
4687+
};
4688+
let lub_concrete = lub
4689+
.as_ref()
4690+
.is_some_and(|j| !matches!(j, JavaType::Object(n) if n == "java/lang/Object"));
4691+
if throw_vars.contains(&(v as u32)) && lub_concrete {
4692+
throw_lub = lub.clone();
4693+
}
46394694
// Array covariance: `return sharedTail;` merges e.g. Class[]
46404695
// and Type[] branch values; when the method return type is
46414696
// an array and every observed value is an array, the return
@@ -4645,7 +4700,14 @@ fn infer_var_types(vt: &mut VarTable, body: &mut Stmt, ret_ty: &JavaType, pc: &P
46454700
&& ev_types.iter().all(|t| matches!(t, JavaType::Array(_)));
46464701
let concrete = !ev_types.is_empty()
46474702
&& !ev_types.iter().any(plain_object)
4648-
&& (uniform || covariant_ret_array);
4703+
&& (uniform || covariant_ret_array
4704+
// A THROWN local is the exception: `throw x` needs a
4705+
// Throwable, so a pool-aware LUB (IllegalState +
4706+
// IllegalArgument → RuntimeException) is required
4707+
// where keeping Object would not compile. Other
4708+
// merged vars keep Object — narrowing them could
4709+
// change overload resolution.
4710+
|| (throw_vars.contains(&(v as u32)) && lub_concrete));
46494711
if crate::dbg_flag!("JCDC_DBG_MERGE") {
46504712
eprintln!("WIDENARROW v={} ev={:?} concrete={}", v, ev_types, concrete);
46514713
}
@@ -4658,6 +4720,12 @@ fn infer_var_types(vt: &mut VarTable, body: &mut Stmt, ret_ty: &JavaType, pc: &P
46584720
// over the 0/1-carrying Int merge type.
46594721
if matches!(t, JavaType::Boolean) && matches!(old, JavaType::Int) {
46604722
JavaType::Boolean
4723+
} else if let Some(l) = throw_lub {
4724+
// A thrown value must be a Throwable: take the pool-aware
4725+
// LUB of its evidence (see `lub_concrete`). Kept to this
4726+
// one consumer — narrowing other merged vars could change
4727+
// which overload a call binds to.
4728+
l
46614729
} else if vt.wide_stack_vars.contains(&(v as u32)) {
46624730
// Widened merges narrowed here take the evidence type
46634731
// directly; joining with the plain-Object placeholder
@@ -4993,6 +5061,48 @@ fn combine_types(types: &[&JavaType]) -> Option<JavaType> {
49935061
acc
49945062
}
49955063

5064+
/// Superclass chain of an internal name (itself first), or empty when the
5065+
/// class is not in the pool.
5066+
fn super_chain(pool: &jcdc_jvm::ClassPool, name: &str) -> Vec<String> {
5067+
let mut out = Vec::new();
5068+
let mut cur = name.to_string();
5069+
for _ in 0..256 {
5070+
out.push(cur.clone());
5071+
let Some(pc) = pool.get(&cur) else { break };
5072+
match pc.super_name() {
5073+
Some(s) if s != cur => cur = s.to_string(),
5074+
_ => break,
5075+
}
5076+
}
5077+
out
5078+
}
5079+
5080+
/// Nearest common superclass of two internal names (None when unrelated).
5081+
fn common_superclass(pool: &jcdc_jvm::ClassPool, a: &str, b: &str) -> Option<String> {
5082+
let cb: std::collections::HashSet<String> = super_chain(pool, b).into_iter().collect();
5083+
super_chain(pool, a).into_iter().find(|n| cb.contains(n))
5084+
}
5085+
5086+
/// `join_types` with hierarchy access: two unrelated reference types join to
5087+
/// their nearest common SUPERCLASS instead of plain Object. Object-typed
5088+
/// merges are only usable for reads — a merged value that is thrown must be
5089+
/// a Throwable (`throw stack3;` with an Object declaration is a javac error;
5090+
/// JetBrains @NotNull instrumentation's `$$$reportNull$$$0` merges
5091+
/// IllegalStateException / IllegalArgumentException). Interfaces and absent
5092+
/// classes fall back to the plain-Object rule.
5093+
fn join_types_pool(a: &JavaType, b: &JavaType, pool: &jcdc_jvm::ClassPool) -> JavaType {
5094+
if let (JavaType::Object(na), JavaType::Object(nb)) = (a, b) {
5095+
if na != "java/lang/Object" && nb != "java/lang/Object" && na != nb {
5096+
if let Some(lub) = common_superclass(pool, na, nb) {
5097+
if lub != "java/lang/Object" && lub != "java/lang/Throwable" {
5098+
return JavaType::Object(lub.into());
5099+
}
5100+
}
5101+
}
5102+
}
5103+
join_types(a, b)
5104+
}
5105+
49965106
fn join_types(a: &JavaType, b: &JavaType) -> JavaType {
49975107
use JavaType::*;
49985108
if a == b {
@@ -5054,9 +5164,10 @@ fn gather_evidence(
50545164
ev: &mut Vec<Vec<Evidence>>,
50555165
ret_ty: &JavaType,
50565166
bool_vars: &mut std::collections::HashSet<u32>,
5167+
throw_vars: &mut std::collections::HashSet<u32>,
50575168
) {
50585169
match s {
5059-
Stmt::Block(v) => v.iter().for_each(|x| gather_evidence(x, ev, ret_ty, bool_vars)),
5170+
Stmt::Block(v) => v.iter().for_each(|x| gather_evidence(x, ev, ret_ty, bool_vars, throw_vars)),
50605171
Stmt::ExprStmt(e) => gather_expr(e, ev, None, bool_vars),
50615172
Stmt::LocalDef { var, init, .. } => {
50625173
if let Some(e) = init {
@@ -5103,6 +5214,7 @@ fn gather_evidence(
51035214
gather_expr(e, ev, None, bool_vars);
51045215
// A thrown local must be a Throwable.
51055216
if let Expr::Local { var, .. } = e {
5217+
throw_vars.insert(*var);
51065218
push_ev(
51075219
ev,
51085220
*var,
@@ -5113,67 +5225,67 @@ fn gather_evidence(
51135225
Stmt::If { cond, then_stmt, else_stmt } => {
51145226
mark_bool_cond(cond, bool_vars);
51155227
gather_expr(cond, ev, None, bool_vars);
5116-
gather_evidence(then_stmt, ev, ret_ty, bool_vars);
5228+
gather_evidence(then_stmt, ev, ret_ty, bool_vars, throw_vars);
51175229
if let Some(x) = else_stmt {
5118-
gather_evidence(x, ev, ret_ty, bool_vars);
5230+
gather_evidence(x, ev, ret_ty, bool_vars, throw_vars);
51195231
}
51205232
}
51215233
Stmt::While { cond, body } => {
51225234
mark_bool_cond(cond, bool_vars);
51235235
gather_expr(cond, ev, None, bool_vars);
5124-
gather_evidence(body, ev, ret_ty, bool_vars);
5236+
gather_evidence(body, ev, ret_ty, bool_vars, throw_vars);
51255237
}
51265238
Stmt::DoWhile { body, cond } => {
51275239
mark_bool_cond(cond, bool_vars);
51285240
gather_expr(cond, ev, None, bool_vars);
5129-
gather_evidence(body, ev, ret_ty, bool_vars);
5241+
gather_evidence(body, ev, ret_ty, bool_vars, throw_vars);
51305242
}
51315243
Stmt::For { init, cond, update, body } => {
5132-
init.iter().for_each(|i| gather_evidence(i, ev, ret_ty, bool_vars));
5244+
init.iter().for_each(|i| gather_evidence(i, ev, ret_ty, bool_vars, throw_vars));
51335245
if let Some(c) = cond {
51345246
mark_bool_cond(c, bool_vars);
51355247
gather_expr(c, ev, None, bool_vars);
51365248
}
51375249
update.iter().for_each(|u| gather_expr(u, ev, None, bool_vars));
5138-
gather_evidence(body, ev, ret_ty, bool_vars);
5250+
gather_evidence(body, ev, ret_ty, bool_vars, throw_vars);
51395251
}
51405252
Stmt::ForEach { iterable, body, .. } => {
51415253
gather_expr(iterable, ev, None, bool_vars);
5142-
gather_evidence(body, ev, ret_ty, bool_vars);
5254+
gather_evidence(body, ev, ret_ty, bool_vars, throw_vars);
51435255
}
51445256
Stmt::Switch { selector, cases, default, .. } => {
51455257
gather_expr(selector, ev, None, bool_vars);
51465258
for c in cases {
5147-
c.body.iter().for_each(|st| gather_evidence(st, ev, ret_ty, bool_vars));
5259+
c.body.iter().for_each(|st| gather_evidence(st, ev, ret_ty, bool_vars, throw_vars));
51485260
}
51495261
if let Some(d) = default {
5150-
gather_evidence(d, ev, ret_ty, bool_vars);
5262+
gather_evidence(d, ev, ret_ty, bool_vars, throw_vars);
51515263
}
51525264
}
51535265
Stmt::Try { body, catches, finally } => {
5154-
gather_evidence(body, ev, ret_ty, bool_vars);
5266+
gather_evidence(body, ev, ret_ty, bool_vars, throw_vars);
51555267
for c in catches {
5156-
gather_evidence(&c.body, ev, ret_ty, bool_vars);
5268+
gather_evidence(&c.body, ev, ret_ty, bool_vars, throw_vars);
51575269
}
51585270
if let Some(f) = finally {
5159-
gather_evidence(f, ev, ret_ty, bool_vars);
5271+
gather_evidence(f, ev, ret_ty, bool_vars, throw_vars);
51605272
}
51615273
}
51625274
Stmt::TryWithResources { resources, body, catches, finally } => {
5163-
for res in resources.iter() { gather_evidence(res, ev, ret_ty, bool_vars); }
5164-
gather_evidence(body, ev, ret_ty, bool_vars);
5275+
for res in resources.iter() { gather_evidence(res, ev, ret_ty, bool_vars, throw_vars); }
5276+
gather_evidence(body, ev, ret_ty, bool_vars, throw_vars);
51655277
for c in catches {
5166-
gather_evidence(&c.body, ev, ret_ty, bool_vars);
5278+
gather_evidence(&c.body, ev, ret_ty, bool_vars, throw_vars);
51675279
}
51685280
if let Some(f) = finally {
5169-
gather_evidence(f, ev, ret_ty, bool_vars);
5281+
gather_evidence(f, ev, ret_ty, bool_vars, throw_vars);
51705282
}
51715283
}
51725284
Stmt::Synchronized { lock, body } => {
51735285
gather_expr(lock, ev, None, bool_vars);
5174-
gather_evidence(body, ev, ret_ty, bool_vars);
5286+
gather_evidence(body, ev, ret_ty, bool_vars, throw_vars);
51755287
}
5176-
Stmt::Labeled { body, .. } => gather_evidence(body, ev, ret_ty, bool_vars),
5288+
Stmt::Labeled { body, .. } => gather_evidence(body, ev, ret_ty, bool_vars, throw_vars),
51775289
_ => {}
51785290
}
51795291
}

0 commit comments

Comments
 (0)