Repository navigation
Releases: ejfkdev/jcdc
Release list
v0.1.3
更新内容
正确性(本版重点)
- 条件表达式不再重复求值已存储的调用。
x = f(); if (x == null)的字节码是dup; astore; ifnull;旧实现把调用表达式留在操作数栈上,条件里又打印一次f()——while (reader.readLine() != null)与循环体各读一次,MailcapFile 每轮消耗 3 行、AppleScriptEngine 2 行。现在分支读取刚写入的局部变量,且仅限紧随的条件分支(允许中间隔常量比较,如dup; istore; iconst_m1; if_icmpeq),不存在「局部变量已被重新赋值」的风险。 - 布尔类型推断收紧。 裸
v == 0不再单独作为 boolean 的证据:javac 对 int 与 boolean 使用同一条测试指令,int p = ports[i]; if (p == 0)以前会被声明成boolean p(boolean p = ports[i]根本无法编译)。现在要求 0/1 值域证据,并让明确的 int 生产点(调用结果、数组元素、int 字段/强转、算术)否决 boolean;v = v & flag、拷贝等歧义形态既不证明也不否决。 - 量化:把 rt.jar 中受影响的类重新交给 javac(
--release 8,rt.jar 作 classpath),327 个文件的编译错误减少、净减 2,695 条;仅 1 个原本已损坏的文件增加 2 条;没有新增语法错误文件(jclassd 全量语法门禁仍是基线的 1 个——ASM 类名含(),任何 Java 源码都无法拼写)。
Java 27 / 28 支持
- 解析 class 文件版本 71/72(Java 27/28),语料补入 Temurin 27+35 与 28-beta+18;完整 JDK 27/28 镜像反编译 0 失败,features 套件在两版各 10/10。
其他缺陷修复
- record 规范构造器不再输出非法的
super();。 - 被调用、却被
--synthetic隐藏的 helper 不再删除(JetBrains 的$$$reportNull$$$0)。 throw的合并值改用池感知的 LUB(此前退化成 Object)。- 数组 checkcast 按描述符解析类型(此前 76 个单元把
[Ljava/lang/Class;当成类名)。
性能
- CLI 改用 mimalloc 全局分配器;内部迁移到 jdc-core 0.2.17(
Arc<str>IR、Fx 哈希)。 - 嵌套类反向引用索引改为单飞构建:此前每个渲染 worker 各建一次、全部争用类池互斥锁(JDK 28 镜像系统态时间 19.3s→11.1s)。
- 18 核 Apple M5 Pro:
rt.jar并行 2.3s / 686MB(单线程 9.4s / 427MB);JDK 28 完整镜像(15,039 个单元)4.7s / 978MB(单线程 23.3s / 525MB)。 - 对比 Vineflower / CFR / Procyon / fernflower:
rt.jar上约 11× / 23× / 48× / 93×,JDK 28 镜像上约 31× / 106× / 154×(Vineflower 无法完成该负载)。 - 输出确定性保持不变:并行与
JCDC_THREADS=1逐字节相同。
验证:features 59/59 × 两条结构化路径;语料 spot 与上一版逐方法字节一致;rt.jar 12,609 个单元全量、无 panic;jclassd 全量语法门禁 1 个(既有基线)。
What's changed (English)
Correctness (the headline of this release)
- Conditions no longer re-evaluate a call the store already performed.
x = f(); if (x == null)compiles todup; astore; ifnull; the old builder left the CALL on the operand stack, so the branch printedf()a second time —while (reader.readLine() != null)above a body reading the same stream again (MailcapFile consumed three lines per iteration, AppleScriptEngine two). The store now rewrites the surviving duplicate to a read of the local it just wrote, restricted to the immediately-following conditional branch (constants in between are fine, e.g.dup; istore; iconst_m1; if_icmpeq), so a reassignment can never make the local stale. - Boolean inference tightened. A bare
v == 0is not boolean evidence on its own: javac has no distinct test opcode, soint p = ports[i]; if (p == 0)used to be declaredboolean p— which does not even compile (boolean p = ports[i]). A boolean inference now also requires 0/1 value-range evidence, and an unambiguous int producer (call result, array element, int field/cast, arithmetic) vetoes it; ambiguous shapes (v = v & flag, copies) neither prove nor veto. - Measured by recompiling the affected rt.jar classes with javac (
--release 8, rt.jar as classpath): 327 files with fewer errors, −2,695 total; one already-broken file gains 2; no new syntax-error file (the jclassd corpus gate stays at its baseline of 1 — ASM's class name containing(), unspeakable in Java source).
Java 27 / 28 support
- Class-file versions 71/72 are parsed; Temurin 27+35 and 28-beta+18 are in the corpus. Full JDK 27/28 images decompile with 0 failures; the features suite is 10/10 on each.
Other fixes
- Record canonical constructors no longer emit an illegal
super();. - Called helpers hidden by
--syntheticare no longer dropped (JetBrains$$$reportNull$$$0). - Merged
throwvalues take a pool-aware LUB instead of degrading to Object. - Array checkcasts resolve their type from the descriptor (76 units previously treated
[Ljava/lang/Class;as a class name).
Performance
- mimalloc as the global allocator; internal migration to jdc-core 0.2.17 (
Arc<str>IR, Fx hashing). - The nested-class reverse-reference index is built once per pool instead of once per render worker (JDK 28 image: kernel-side time 19.3 s → 11.1 s).
- Apple M5 Pro, 18 cores:
rt.jar2.3 s / 686 MB parallel (9.4 s / 427 MB single-threaded); full JDK 28 image (15,039 units) 4.7 s / 978 MB (23.3 s / 525 MB single-threaded). - Against Vineflower / CFR / Procyon / fernflower: ~11× / 23× / 48× / 93× on
rt.jar, and ~31× / 106× / 154× on the JDK 28 image (Vineflower cannot finish it). - Deterministic output is unchanged: parallel and
JCDC_THREADS=1runs are byte-identical.
Verification: features 59/59 on both structurer paths; corpus spot byte-identical per method to the previous release; rt.jar 12,609 units, panic-free; jclassd syntax gate at its baseline of 1.
Full Changelog: v0.1.2...v0.1.3
v0.1.2
更新内容
语料规模语法校验:1,671 → 1 个语法错误文件。 用 48,251 个第三方 .class(CFR 各 javac 版本回归套件、fernflower/ASM 测试数据、完整 JDK 26 镜像含源码、java-class-rs 边角用例)做「反编译 → javac 回编译」全量校验,逐类修复解析级错误:
- 目录输入的类池索引:原先按「输入相对路径」而非 class 自身内部名索引,凡目录树不是以包根为根(如
cfr/java_8/…、jdk26/jdk26/class/<module>/…)就导致按内部名查找全部落空——匿名类无法内联、输出非法的Outer.1。现按this_class索引(不可解析的文件保留路径键),重复名的解析顺序也确定化了。这一类占原错误的约 1,300 个文件。 - 被丢弃的值合法化:
a.a;、Outer.this;、(Object[]) foo.clone();都不是合法语句表达式——强转剥到有副作用的核心,可能抛 NPE 的读取物化进合成局部变量(保留原语义)。 $名字渲染:字面$名(DolTest2$$dollah$$)与 javac 局部类编码(Outer$1Name)在点号化后无法解析时一律保留$。- 数组下标加括号:
(new int[1][0])[0] = …(裸写会被重解析成三维数组创建)。 - try-with-resources 残留形态:资源被改写为赋值时先输出赋值再普通
try;无资源且无 catch/finally 时退化为语句块(裸try {无法编译)。 - Java 21+ 模式 switch 还原:
enumSwitch常量标签(含EnumDesc条件常量解码)、typeSwitch整数标签——修复 JDK 26PackageBuilder/DocLint一类的方法。 - 匿名类作为构造参数:内联后继续下钻参数,
new Outer(new Inner(null) {…}) {…}正确还原;常量监视器(synchronized (null))物化为局部变量。
唯一剩余文件是 ASM 的 Artificial$()$Structures——类名含 (),任何 Java 源码都无法拼写(CFR 输出同样非法),属不可还原输入。
验证:features 语义套件 56/56;rt.jar 全量 12,609 文件零 panic;语料保真度 A/B 与改动前完全一致(fail_files 集合、6/6 族可重编译)。
性能基准更新:加入 JetBrains fernflower 对照;第二个负载换成完整 JDK 26 模块镜像(JDK 9+ 已无 rt.jar,boot classpath 变为 lib/modules)。18 核 M5 Pro:rt.jar 6.8s / 599MB(fernflower 196.8s,Vineflower 26.2s,CFR 59.4s,Procyon 113.1s);JDK 26 镜像 11.9s / 801MB(CFR 175.8s,fernflower 513.1s,Procyon 789.4s;Vineflower 跑不完该负载——8g/16g 堆耗尽,32g 封顶 40 分钟仍 0 输出)。
新增 scripts/jclassd_syntax_check.py(语料语法校验脚本)与 “Built with ZCode” 徽章。
What's changed (English)
Corpus-scale syntax sweep: 1,671 → 1 files with parse errors, measured by decompiling a 48,251-class third-party corpus and feeding every emitted source back through javac:
- Pool indexing for directory inputs now keys classes by their own
this_classinstead of the input-relative path — any tree that is not a package tree rooted at the input dir made every internal-name lookup miss, killing family/anonymous enumeration and emitting illegalOuter.1references (~1,300 files). - Discarded-value legalization:
a.a;,Outer.this;,(Object[]) foo.clone();are not statement expressions — casts strip to their side-effecting core, trapping reads materialize into a synthetic local (NPE semantics preserved). $-name rendering keeps$when dotting cannot parse (literal-$names, javac'sOuter$1Namelocal-class encoding).- Array indexing parenthesizes its base:
(new int[1][0])[0] = …. - Vestigial try-with-resources: hoisted resources print before a plain
try; a resource-lesstrywith no catch/finally becomes a block. - Java 21+ pattern switches restored, including
enumSwitchconstant labels andEnumDescconditional-constant decoding (fixes JDK 26PackageBuilder/DocLint-style methods). - Anonymous classes as constructor arguments are now inlined; constant monitors (
synchronized (null)) materialize into a local.
The one remaining file is ASM's Artificial$()$Structures — a class name containing () that no Java source can spell (CFR emits the identical invalid form).
Verification: features battery 56/56; rt.jar smoke 12,609 files, no panics; corpus fidelity A/B byte-identical to the pre-change baseline. Benchmarks updated with JetBrains fernflower and a second workload: the full JDK 26 module image (JDK 9+ has no rt.jar) — jcdc 11.9 s / 801 MB vs CFR 175.8 s, fernflower 513.1 s, Procyon 789.4 s; Vineflower cannot finish it.
Full Changelog: v0.1.1...v0.1.2
v0.1.1
v0.1.0
Full Changelog: https://github.com/ejfkdev/jcdc/commits/v0.1.0