Skip to content

[AUTOMATED] [PROPOSAL] JVM engine: decompile Java and Kotlin class files - #1528

Draft
mahaloz wants to merge 480 commits into
mainfrom
feat/jvm-decompiler
Draft

mahaloz wants to merge 480 commits into
mainfrom
feat/jvm-decompiler

Conversation

@mahaloz

@mahaloz mahaloz commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

kuna could only decompile native binaries. This adds a second engine for the JVM: point the same commands at a .class file, a jar or a class directory and get Java back, or Kotlin for Kotlin classes.

$ kuna decompile tests/fixtures/jvm/app.jar com.ex.util.Helper.loop

Before:

error: could not build an architecture for tests/fixtures/jvm/app.jar: unrecognized input format; ...

After:

public static int loop(int n) {
   int total = 0;

   while (n > 0) {
      if (n % 3 == 0) {
         total += n;
      } else {
         total--;
      }

      n--;
   }

   return total;
}

What it is

  • decompiler/nonnative/kuna-jvm is a Rust port of Vineflower 1.12.0 (Apache-2.0, credited in NOTICE) with its Kotlin, IDEA-not-null and variable-renaming plugins. It keeps Vineflower's Java-level IR and does not lower bytecode to p-code: class files carry types, exception tables and structure that p-code would discard and the native passes would have to guess back. It shares the CLI, the option catalog, the output shapes and the phase-folder layout with the native engine, and no code with kuna-decomp.
  • With --option jvm_vineflower_compat on the output is byte-identical to Vineflower's for the same input, options and JDK, which makes Vineflower's own test suite the regression oracle. By default kuna also fixes Vineflower bugs (below).
  • decompile, decompile-all, decompile-project, functions, catalog --engine jvm, test --jvm and docs jvm accept JVM inputs. The engine is picked from the file's magic; flags that only make sense for native images exit 2. The 74 engine options are jvm_* rows in the same catalog (docs/options-jvm.md).
  • Native decompilation is unchanged: no datatest or stage assertion moved, a whole-binary export of /usr/bin/gzip is byte-identical, each invocation costs about 1 ms more, and the binary grows from 38.4 to 50.1 MB.

Compared with Vineflower

All 110,238 methods of five projects, scored against the original source on control flow, types and recompiled bytecode. In compat mode every score is identical to Vineflower's; the times below are from that mode, both tools back to back on one machine, Vineflower with 12 threads. docs/jvm-benchmark.md has the commands to rerun it (scripts/javabench).

Project Methods Perfect on every metric (compat) kuna wall / peak memory Vineflower wall / peak memory
guava 12,167 91.86% 2.1 s / 1.5 GiB 4.5 s / 3.8 GiB
spring-boot 3,967 88.98% 0.7 s / 0.7 GiB 2.7 s / 1.6 GiB
spring-framework 13,874 90.83% 3.2 s / 1.5 GiB 6.0 s / 4.7 GiB
ghidra 18,153 80.05% 3.4 s / 1.4 GiB 7.1 s / 6.9 GiB
elasticsearch 62,077 84.20% 17.7 s / 7.4 GiB 29.2 s / 11.5 GiB

On a single thread kuna is still 1.2 to 2.2 times slower than Vineflower.

Where kuna departs from Vineflower

kuna fixes 238 Vineflower 1.12.0 bugs in its own engine, 131 of them in Kotlin output. They are always on and have no options; --option jvm_vineflower_compat on turns them all off. Each has a case in tests/jvm/kuna-fixes/ checked in both modes, and docs/jvm.md lists them.

  • 26 decompile methods or classes Vineflower fails on: irreducible loops holding a try, pattern switches, Kotlin references, default-argument constructors and suspend functions, a finally kotlinc split around a suspension point, exception ranges kotlinc 1.3 made overlap without nesting, ...
  • 202 correct code Vineflower prints wrong or javac or kotlinc rejects. In Java: (long) a * b printed as an int product that overflows, a cast that makes javac call the method itself (Guava's Joiner.appendTo), a finally run once per loop iteration, unboxing dropped so that boxes are compared by identity, raw generic constructors, assert as an expression, the casts and type arguments generic inference needs, Arrays.asList(new T[n]) printed as Arrays.asList(), build(new String[0]) printed as a call to itself, casts dropped so that another overload is called, doubles printed as float literals, Objects.requireNonNull(x); deleted, o.super(..) printed as super(..), a write in a finally lost to the code after the try, a synchronized body run outside its lock or under a re-read lock, hand-written code turned into try-with-resources, ... In Kotlin: suspend functions printed as kotlinc's state machine (continuation class, label dispatch, spilled locals) rather than their body, raw generic types, which Kotlin does not have, data classes whose properties are declared in the body, inner classes constructed through the outer class's name, kotlinc 1.3's parameter checks on inline lambda parameters, Byte and Char values used as Int without the conversion Kotlin requires, default values printed as = ... or as another function's, when over an enum through kotlinc's $WhenMappings arrays, the objects kotlinc copies out of inline functions printed as separate $$inlined classes, @Volatile and @Transient dropped from properties, enum constants that lost their arguments, extension properties declared without their receiver, accessors that call themselves instead of reading field, $ in strings read as a template, property initializers and companion constants lost, @Synchronized accessors that lost the flag, !! dropped, nullable values cast as non-null, an extension property assignment that assigned the receiver, calls through facade classes Kotlin cannot name, operators parenthesised by Java's precedence (x or 1 shl k means (x or 1) shl k), != for identity tests, Char and Byte arithmetic typed as in Java, line breaks that end a statement early, parameters named new or default read through undeclared variables, field reads and calls inlined past code that changes their result, $ names and keywords without backticks, dropped returns, wrong for loop bounds, $default bridge calls, constructors printed as functions, abstract functions with bodies, Java member names (.length(), charAt), classes without Kotlin metadata written in Java syntax, ...
  • 8 rebuild what the bytecode shows: try-with-resources whose resource class is outside the input or whose javac 11+ class file targets Java 8, compound assignments (no longer writing x = e + x as x += e either), new Inner() instead of Outer.this.new Inner(), switch cases in source order without javac's table fillers and synthetic default, and a test and cast where javac's bytecode shows no pattern. 2 make failure comments name their cause.

Three options, on by default and off under jvm_vineflower_compat on, write equivalent code in the form the class file shows the source had: jvm_javac_layout lays out ifs, loops and exits as javac laid out the bytecode (two returns stay two returns rather than return c ? a : b, ifs keep the source's polarity, early return;, continue; and break; stay, also inside loops and try, code javac placed after a loop stays after it, while and for as the source wrote them, false == x stays), for classes without Kotlin metadata; jvm_source_locals keeps the locals the class file names instead of folding them into their use, declares them in slot order and in the source's scopes, and keeps ++ and -- statements the source wrote as statements; jvm_source_patterns keeps a test and a cast the line table shows the source wrote, rather than an instanceof pattern.

On the benchmark, default mode raises the methods perfect on every metric from 94,106 to 109,007 (85.37% to 98.88%; every project above 98.7%), recovers all 17 methods Vineflower fails or prints unparseable, and makes 380 more classes recompile (7,988 of 8,155), losing none. 15,365 methods score better; 32 score worse on at least one metric (docs/jvm-benchmark.md lists them with the cause). On Kotlin libraries the methods left as failure comments drop from 38 to 0 in kotlin-stdlib, from 2 to 0 in okio, from 3 to 0 in okhttp and from 71 to 0 in kotlinx-coroutines (decompiled as Java with jvm_plugins 0, no method fails either, where Vineflower fails 24), and calls through $default bridges from 446 to 20 over okio, okhttp, kotlin-stdlib and kotlinx-coroutines. Compiled whole with kotlinc 2.0, the decompiled okio has 96 errors instead of 6,185, okhttp 441 instead of 9,988 and kotlinx-coroutines 6,876 instead of 18,722 (31 of 50, 63 of 131 and 110 of 396 files compile, instead of 2 of 50, 3 of 148 and 43 of 506).

Robustness: recursion too deep for the stack fails that one method or class with a StackOverflowError instead of ending the run, and the per-method time limit counts work rather than wall time, so the same methods stop on every run at any worker count.

Tests

  • make test-jvm runs 1,151 cases: 607 byte-identical to Vineflower's output in compat mode (530 Java cases from Vineflower's suite, 6 for the two small plugins, 71 Kotlin), and the 272 fix and option cases in both modes. No JDK is needed, and decompiler/nonnative/kuna-jvm/tools/oracle regenerates the expectations from Vineflower.
  • 724 engine tests, most of them replaying vectors produced by Vineflower's own classes, plus 44 CLI tests and tests/cli probes for the new surface.
  • Whole jars in compat mode: the five benchmark jars, okio and okhttp come out identical, apart from the stack-trace text in the comment written when Vineflower itself fails a method.

Not covered yet on JVM inputs: strings, xrefs, disassemble and the call-graph options.

🤖 Generated with Claude Code

https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4

mahaloz and others added 11 commits November 9, 2026 13:36
…tiple_of)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
Parallel scaling meets Kotlin: companion members are appended to a class's
member lists through new StructClass mutators, which read the lazy lists first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
…age flag

main's global --image NAME (and --image=NAME) selects one image of a dyld
shared cache. On a JVM input the engine refuses it, in either spelling and on
either side of the input operand, with the native-flag message (exit 2); a
native input behaves as on main. A test next to the other refused-flag tests
pins that, and docs/cli.md's --image section says so, as its --slice section
does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
…the final build

The numbers predated the merge of the two performance branches. Measured
with decompile-project on input-full.jar, best of three, 80 cores, load
average 2-22 (mostly the runs themselves), 40 GB scope:
--jobs 1 / 8 / 24 (auto): Elasticsearch 190 / 27 / 12.5 s, peaks 2.5 /
4.2 / 7.4 GB; Guava 20 / 3.4 / 1.9 s, 0.37 / 0.74 / 1.5 GB; spring-boot
0.16 / 0.31 / 0.67 GB; Ghidra 0.82 / 1.4 GB at 8 / 24. The memory estimate
still bounds every peak. The speed paragraph compared a 3-8x slowdown from
when the port first reached parity; one instance now takes 1.2-2.2x
Vineflower's -thr=1 wall time (spring-boot 7.2 vs 6.1 s, Guava 20 vs 13,
spring-framework 38 vs 21, Elasticsearch 190 vs 85), and with both tools'
default flags kuna is faster on all four with a third of the CPU time or
less. Elasticsearch output is identical at 1, 8 and 24 workers.
kuna_reclaim joins the list of kuna's own modules, and docs/cli.md's
kuna test --jvm summary line shows the 607-case corpus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
… inflation and the heap policy

Three behaviours the performance branches shipped had no prose in the live
spec: kuna_reclaim's per-method windows and per-class releases (10.13),
inflating a large archive's class entries on up to eight threads (10.2), and
the glibc heap policy the CLI sets before a JVM decompile (10.14).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
…oracle

The scripts and driver sources that build the Java and Kotlin oracle corpora tests/jvm is cut from: fetch (pinned hashes), compile, manifest, oracle run, validation and cross-checks, with every download and output under KUNA_JVM_ORACLE instead of a home directory. No data, jars or JDKs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
…mark.md

The DecBench-style Java benchmark that compares kuna's JVM engine with Vineflower 1.12.0 on five large projects, as a scripts/ package: every path under one workspace (--workspace or JAVABENCH_WORKSPACE), the dataset's pinned SHA-256s shipped as pins/, the metric tests as unittest cases that skip without tree-sitter or a JDK, and a standard-library test of the workspace convention in make test-tools. docs/jvm-benchmark.md states the result: identical scores on all 110,238 methods, timings of one back-to-back run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
…directory defaults

Every script, test helper and README that located the Vineflower jar, the JDKs, kotlinc, the oracle corpora or their tools under a home directory now defaults to the oracle workspace named by KUNA_JVM_ORACLE (tools/oracle/README.md), keeps its per-script overrides, and stops with a one-line message naming the variable when it is unset. The Kotlin real-world jars come from the oracle's pinned Maven Central downloads, vf-decompile from tools/oracle. f6_support's jar lookup reads VINEFLOWER_JAR, then KUNA_JVM_ORACLE, and tests still skip without a jar. tests/jvm/README.md and kotlin.json are regenerated; every other file under tests/jvm regenerates byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
…-image refusal

The crate header says the engine also writes Kotlin and names the three bundled plugins; the estimate_memory comment points at docs/jvm.md instead of quoting superseded peaks; a JVM input refuses --image=X by naming the flag --image (the test pins it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
@mahaloz mahaloz added the full-ci Run the full cargo workspace suite on this PR before merge (internal PRs skip it by default) label Nov 9, 2026
mahaloz and others added 17 commits November 9, 2026 15:37
… scoring modules

report and compare resolved their run directories only after importing the
scoring stack, so on a machine without tree-sitter the missing-run error was an
import traceback. The Java parser is now loaded on first use.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
…h a helper

The b19 oracle generator reproduces golden.txt byte for byte.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
…t two fixes

The engine's contract was Vineflower 1.12.0's exact text, failures included.
kuna_fixes adds the mechanism to fix Vineflower's defects in kuna's own
engine: one enum variant and one jvm_fix_<slug> row (default on) per fix,
kuna_fixes::enabled() at every site that departs from the Java, and
jvm_vineflower_compat (default off) to turn every fix off at once.
Fernflower::new takes the switches off the property list into the context;
installing a context makes its set the one the gate answers from.

The corpus runner turns compat on unless a case says otherwise, so the
607 Vineflower-oracle cases keep guarding the port.

Fixes: jvm_fix_report_to_kuna (the failure comment's default note names
kuna's tracker) and jvm_fix_lambda_wildcard_return (NewExprent's guard
from upstream 13d4112e dereferenced the null return type of a method
reference on a Foo<?> receiver; the method now keeps the inferred mapping).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
A kuna-fixes case is a small class Vineflower 1.12.0 fails on. regen.py
appends it to cases.tsv twice: kuna-fixes-compat/<id> (jvm_vineflower_compat
on, expected = the release jar's text through vf-decompile --fixture) and
kuna-fixes/<id> (defaults, expected = kuna's reviewed output), and writes
kuna-fixes/README.md with the diff between the two. kuna_fix_case.py adds a
case from a class file end to end. regen.py still reproduces every other
vendored file byte for byte.

First case: pkg/TestMethodRefWildcardReceiver for
jvm_fix_lambda_wildcard_return; both runs are in baseline.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
CI lints with the current stable toolchain. Three more structural lints join the
port's allow list (collapsible_match, while_let_loop, question_mark); the jimage
directory reader uses as_chunks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
…diff checker, CLI test

- rt/exception.rs: KUNA_JVM_BACKTRACE=<text>|1 prints the Rust backtrace of a
  caught engine throw (the hook swallows them otherwise).
- examples/jvmtrace.rs: the trace differ's kuna side runs in compat mode, since
  it is compared with Vineflower's trace.
- tests/jvm/kuna_fix_diff.py: checks that two decompile-project trees (compat
  and default) differ only inside methods that failed in compat mode, in
  tracker notes or in imports.
- jvm_fix_report_to_kuna's row names every error comment it applies to; the
  JVM catalog preamble mentions the fixes and the compat switch.
- kuna-cli: the fix is on by default, compat and the per-fix option turn it
  off, the note names the right tracker, a bad switch value exits 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
docs/jvm.md: the fidelity contract now has two modes (compat: Vineflower's
exact text; default: the same except inside the methods kuna fixes and the
tracker note), a Kuna-only fixes section with the list of fixes, and the
recipe every fix follows (reproduce, diagnose in the Java, gate, case,
spec, evidence, gates). docs/spec/10-jvm.md 10.16: the gate, the compat
switch, one paragraph per fix. docs/cli.md, docs/jvm-benchmark.md: the
contract statements. docs/options-jvm.md regenerated (73 options).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
Root cause: TryWithResourcesProcessor.makeTryWithResourceJ11 (Vineflower
1.12.0 :166, unchanged on develop/1.13.0) recognizes a Java 11+
try-with-resources from its catch (Throwable) handler. When the handler is a
sequence starting with an if, it strips the negations off the condition and
takes the result for the resource's null check, reading operand 0 (must be a
variable) and operand 1 (must be null). A hand-written handler starting with
if (!flag) on a boolean local leaves a one-operand BOOL_NOT whose operand is a
variable, so operand 1 does not exist and the method fails with an index out
of bounds (spring-framework ConfigurationClassEnhancer.createClass). With the
fix, a condition with fewer than two operands is not a null check and the
matcher leaves the statement alone, as it does when the operand is not a
variable. The guard fires only where Vineflower always throws.

Evidence:
(b) pkg/TestTryCatchNotFlag (javac 17.0.20.1): the default output recompiles
    and javap -c -p of both classes is identical to the original;
    ConfigurationClassEnhancer recompiled (javac 21 --release 17): the outer
    class's javap -c -p, createClass included, is identical to the original.
(c) jvmdec --jobs 16, compat vs default, tests/jvm/kuna_fix_diff.py: 0 other
    hunks on guava, spring-boot, spring-framework (1 fixed: createClass),
    ghidra, elasticsearch, okio, okhttp, kotlin-stdlib.
(d) corpus 611/611, 0 regressions; cargo test -p kuna-jvm 484 passed,
    0 failed, 3 ignored; check_spec --strict OK; clippy 1.98.1 clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
kuna_fix_case.py and regen.py handled Java cases only (.java texts, no
plugins). A case now has a language, picked per case and Java by default:
a Kotlin case ("language": "kotlin" in kuna-fixes/cases.json; the
script's --language, defaulting to kotlin for a class with kotlin.Metadata)
gets its oracle text from vf-decompile --plugins, runs with jvm_plugins=1,
stores <id>.kt in expected-compat/ and expected/, and compares <Stem>.kt
(falling back to <Stem>.java, as the vendored Kotlin cases do). Java cases
and the rest of the corpus regenerate byte for byte; the kuna-fixes README
gains one sentence. No runner change: per-case opt lines already overlay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
IfHelper.ifElseChainDenesting rewrites `if (a) { if (b) X; ...; Y } Z` as
`if (!a) { Z } else { ... }` once the body has *some* jump to the end of the
enclosing sequence (hasDirectEndEdge: a catch's return, a finally handler's
rethrow edge). Unlike reorderIf before the same swapBranches, it does not
check that no jump from inside the if reaches the statements it moves, so a
body that also falls through to Z keeps break edges that now enter the
then-branch from outside (ValidationHelper: "Break edge with closure with
invalid direct path", right after the swap, in both engines). Upstream:
introduced in 91ff9353 (#69), unchanged on develop/1.13.0.

Effects in Vineflower: Z a lone return -> CondenseExits copies it, output
correct but restructured; otherwise a `break` past Z (skipped statement,
missing return: wrong code); with an assert before the try, AssertProcessor
moves the stale edge onto the assert block, Z is emptied and dropped, and
VarDefinitionHelper fails with "Could not find destination nodes for stat id"
(Elasticsearch InternalEngine.closeNoLock).

Fix: skip the swap when an edge from inside the if reaches a statement after
it in the sequence (KunaFix::IfDenestFallthrough, option row, gate in
if_helper.rs; Java kept verbatim under compat).

Evidence:
(a) pass trace (vftrace) identical to Vineflower's on the case; both engines'
    ValidationHelper fail every changed method at the validate call after
    ifElseChainDenesting (IfHelper.java:104 in the release jar); with the fix
    no validation error.
(b) case pkg/TestIfDenestFallthrough (javac 17.0.20.1 -g): default output
    recompiles, `javap -c -p` identical to the original class; the compat
    text does not compile (missing return). closeNoLock recompiled with the
    benchmark's javac/classpath: bytecode exact (169 insns).
(c) jvmdec compat vs default, kuna_fix_diff.py: guava, spring-boot,
    spring-framework, ghidra, elasticsearch (input-full and classes.jar),
    okio, okhttp, kotlin-stdlib, kotlinx-coroutines: differences only in
    closeNoLock (fixed, + its import) and two methods Vineflower printed via
    the healed path: ghidra DataTypeManagerDB.resolveBuiltIn (bytes 0.56 ->
    exact) and spring AnnotationTypeFilter.hasAnnotation (0.92 -> exact),
    both now the source's shape; plus tracker notes.
(d) corpus gate 611/611, 0 regressions; cargo test -p kuna-jvm 485 passed,
    3 ignored; check_spec --strict OK; clippy 1.98.1 -D warnings clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
Vineflower 1.12.0 SwitchExprent.toJava gives a constant yielded by a
single-expression arm the switch expression's type. That type is the
join of the arms' values (VarType.join -> findCommonAncestor), so when a
string or class literal sits next to an arm of an unrelated reference
type (a boxed char/int/boolean, a StringBuilder, a Class) the literal is
retyped to Serializable (Object without the JDK) and ConstExprent.toJava
throws "invalid constant type". Vineflower guards the same retype in
VarDefinitionHelper (canConstTypeMerge, aea5fd12) but not here; still
present on develop/1.13.0, and the 1.13.0-dev jar fails the same way.

With the fix the constant takes the switch's type only when
ConstExprent::can_be_written_as accepts it (exactly the types to_java
writes: a primitive or its box, null, String, Class, MethodType);
otherwise it keeps its own type, a subtype of the join that the
switch's assignment/invocation context widens as javac compiled it.
Case labels are not touched (crash/C19 is a different defect).

Evidence: (b) reproducer pkg/TestSwitchExprConstLub (javac 17.0.20.1,
-source/-target 17 -g): 8 methods fail in compat mode (as with the
release jar), 0 by default; the default output recompiles and javap -c
of all 18 methods matches the original, also for -g:none and with the
JDK 21 runtime loaded (join = Serializable). spring-framework
BasicJsonWriter.escape decompiles; recompiled, escape is identical and
lambda$escape$0 differs only in the layout order of its eight case
bodies (same key -> constant mapping). (c) jvmdec compat vs default,
kuna_fix_diff.py: guava, ghidra, spring-boot, elasticsearch, okio,
okhttp, stdlib: only tracker notes and the existing fix; spring-
framework: 1 method fixed, its one hunk re-wraps the enclosing
input.chars().forEach(...) statement of the failing lambda (inside
escape). (d) corpus 611/611, 0 regressions; cargo test -p kuna-jvm
486 passed, 3 ignored; check_spec --strict OK; clippy 1.98.1 clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
… merging two fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
Unchanged copy of ccc5b5c (branch jvm/fix-kwriter), so that the Kotlin
two-pass cases of this branch use the same harness: a case has a language;
a Kotlin case gets its oracle text from vf-decompile --plugins, runs with
jvm_plugins=1, stores <id>.kt and compares <Stem>.kt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
# Conflicts:
#	decompiler/nonnative/kuna-jvm/options.toml
#	tests/jvm/README.md
#	tests/jvm/cases.tsv
#	tests/jvm/kuna-fixes/README.md
#	tests/jvm/kuna-fixes/cases.json
A Kotlin primary constructor whose supertype call leaves out defaulted
arguments calls the superclass's synthetic default-arguments constructor
<init>(P1..Pn, int mask.., DefaultConstructorMarker). Vineflower's
KConstructor.writePrimaryConstructor removes only the marker
(KUtils.removeArguments), so the call names <init>(P1..Pn, int), which
does not exist: when the superclass is decompiled in the same run and its
wrapper is still alive, ExprUtil.getSyntheticParametersMask throws
"Constructor ... not found" outside any method guard and the whole class
fails (kotlinx-coroutines 1.8.0/1.10.2 BroadcastChannelImpl, kotlin-reflect
CallerImpl); otherwise the placeholders and mask are printed
(": Task(name, false, 2)"). Unchanged on develop/1.13.0.

The fix rewrites the call into the call it stands for, <init>(P1..Pn) with
the trailing defaulted arguments left out, when the shape, the constant
masks and placeholders, the synthetic constructor, the superclass's
metadata (a default declared for every left-out parameter) and the absence
of a competing constructor all confirm it; anything else keeps the Java.

Evidence: reproducer pkg/TestDefaultArgsSuperCall (kotlinc 2.0.0) fails 3
methods in compat exactly as vineflower-1.12.0.jar, 1 by default (the
competing-constructor shape, by design); a nested-class form recompiled
with kotlinc gives identical javap -c -p for every subclass. Whole-class
failures, single instance, compat -> default: coroutines 1.8.0 1 -> 0,
1.10.2 1 -> 0, kotlin-reflect 2.3.21 1 -> 0 (CallerImpl). Compat vs default
(jvmdec --jobs 12) on guava, spring-boot, spring-framework, ghidra,
elasticsearch, okio: only failed methods and tracker notes; outside them
exactly the same construct: okhttp RealWebSocket.WriterTask and stdlib
TerminateException now read Task("$name writer") / FileSystemException(file)
as in their sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179iMKReqhJewXF589wZswF
mahaloz and others added 30 commits November 11, 2026 03:34
…nd compound forms

source_scopes now accepts several split-off variables renamed to one local
(var2..var10 -> h in ShardRouting.hashCode, var15..var17 -> h1 in
MurmurHash3) and expands x op= e (shifts too) with parentheses unless e uses
only the same associative operator. Base vs HEAD default output on the five
Java jars, every hunk classified: 8 left unrecognised (SleighCompile's
do-while, BlendedTermQuery x3, AlpDoubleUtils, TriangleTreeWriter,
Iso8601Parser), all read: equivalent, the source's statements.
test_kuna_fix_diff: 137 tests OK; kuna_fix_check OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…exits, guards two Vineflower exits, adds TestJavacTails

Continues the WIP commit c693078 (tails after loops and switches, for headers
from the LocalVariableTable and LineNumberTable, value returns kept out of
try/synchronized, if (a || b) continue, early exits in try-with-resources,
finally and synchronized blocks):

- copied_exit_start reads the offset of a rebuilt block only when the block is
  the exit alone (ExitHelper's copy); a rebuilt branch that ends with the
  method's shared return keeps Vineflower's if-else (ES
  AbstractHttpServerTransport.dispatchRequest).
- LoopExtractHelper::extract_first_if no longer makes a loop's first if whose
  other path continues the loop that loop's exit (else_continues). With a tail
  moved after an outer loop the climb to a regular successor found one and
  printed `if (cmp(++i, v) < 0) return;` for an empty while sharing the start
  of a for (;;) (spring's SorterTemplate.quickSortHelper, found by the
  semantic check below); it also corrects Vineflower's inverted exit of that
  shape in ES BulkNeighborQueue$ReservoirTopK.quickSelectIntro, which now
  recompiles trace-equivalent to the original.
- Two-pass case kuna-fixes/pkg/TestJavacTails (javac 17.0.21, 12 methods):
  default output recompiles to the original bytecode for all 12, compat
  (Vineflower) differs in 9; tests/kuna_javac_tails.rs pins both modes.
- Spec 10.9 paragraph, docs/jvm.md paragraph, case counts (1043 runs).

Evidence (javabench full runs kuna-jtails-f3-* vs kuna-fix10-*): methods
perfect on every metric 108,652 -> 108,825 of 110,238, bytes exact
108,769 -> 108,942, classes recompiling 7,969 = 7,969; 198 methods better on
some metric, 7 worse, all read. 476 methods change text: 405 recompile
byte-exact, 56 trace-equivalent, 6 effect-equivalent, 9 read by hand (6 do
not compile in either run). Compat output byte-identical on the five Java jars
and on okio, okhttp, kotlin-stdlib and kotlinx-coroutines 1.8/1.10 (default
output of those too). Corpus 1043/1043, cargo test -p kuna-jvm 681 passed,
clippy 1.98.1 clean, check_spec --strict OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
jvm_source_locals now also keeps a declaration outside the block where
the slot numbers prove the source declared it there, merges back a local
Vineflower split when one LocalVariableTable entry names both halves, and
keeps x = a; a++; as two statements where the bytecode order shows the
source wrote them so. docs/options-jvm.md regenerated for the option's
new symptoms and modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…ls and while loops by method

javac_layout_tails, appended as a CONTEXT recogniser of jvm_javac_layout
(tried after the existing method recogniser): the methods around a hunk are
javac_layout's same code once a labelled break/continue counts as a bare one,
type arguments written after a name are erased (a for header's parts split at
their commas), and the varN Vineflower split off for a for header's init is
named as the variable the while loop keeps (mapped by its init, or the one
loop variable of the new method). Cached per method pair. Unit tests in
TestJtails (scripts/tests/test_kuna_fix_diff.py, 135 OK); kuna_fix_check OK.

Collateral, base -> new default output of the five Java jars (files differing /
unrecognised hunks): guava 23/0, spring-boot 7/0, spring-framework 46/0,
elasticsearch 134/2, ghidra 110/15 plus 2 files the tool did not finish in
10 min. Every unrecognised method was read: a for -> while that composes with
switch_expression_assignments (ES ScriptMetadata.fromXContent, recompiles
trace-equivalent), and tails after loops or while loops where Vineflower's
variable merge or split is undone (ghidra TokenPattern, EquateSymbol,
FunctionPrototype, PcodeSyntaxTree, PatternBlock, Varnode: now byte-exact;
PatchPackedEncode: trace-equivalent).

Also: rustfmt of the layout code, module-header notes for the new hooks in
merge_helper, inline_single_block_helper and dead_code_helper (no change in
output on the five jars; clippy 1.98.1 clean, corpus 1043/1043).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
jvm_javac_layout now also keeps code javac placed after a loop or try
there, chooses while or for from the local-variable and line tables,
keeps if (a || b) continue; as one if, and keeps early exits inside
try-with-resources, finally and synchronized. Both guards against the
wrong first-if exit stay (the layout's and loop_first_if_exit's).

With the layout's while/for choice, TestDeclarationScopes,
TestIncrementStatements and TestLoopFirstIfExit now print the source's
while loops and partition's tail after its inner loop; re-recorded. The
parent walk over a parameter is the layout's shape, not
jvm_source_locals', so kuna_source_scopes checks it in default mode only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
… kotlinc emits

Four kuna-only fixes (KunaFix ids; jvm_vineflower_compat on keeps Vineflower's text):

- kt_default_args_bridge_lookup: KFunction.parse looked for `<Kotlin name>$default`
  with a descriptor built from Kotlin types, only in the class. kotlinc names it after
  the JVM name (@JvmName, internal `$module`, inline-class hash) with the JVM
  parameters (unboxed inline classes, suspend continuation, erased type parameters)
  and puts an interface's in DefaultImpls. Defaults printed `= ...` drop from
  okio 5 / okhttp 27 / kotlin-stdlib 128 / coroutines 1.8 29 / 1.10 25 to 0 / 0 / 0 /
  3 / 0, and a `@JvmName("pickInts") fun pick` no longer takes the other pick's default.
- kt_default_value_names: default values name parameters by metadata, the extension
  receiver `this`, a member's instance `this@Class` in an extension (Vineflower's
  `this.length` read the receiver's length).
- kt_default_value_coercion_cast: kotlinc's checkcast to the parameter type is not
  printed as a cast (`= null as MediaType?`, `= ArrayList() as java.util.List`); kept
  on lambdas.
- kt_delegated_member_default: members generated for `by` delegation (K1 metadata)
  declare no defaults (coroutines 1.8: 3).

kotlinc 2.0 errors (ktc.sh): okio 307 -> 231, okhttp 1939 -> 1482 ("no parameter
with name" 75 -> 0, "Expecting member declaration" 157 -> 11), coroutines 1.10.2
12851 -> 12553. New defaults compared with the published sources: okio 6/6,
okhttp 35/35, stdlib 242 match (4 data-class copy without source; 4 differ only by
existing expression printing defects), coroutines 1.8 35 match + 7 explained.
Four two-pass cases (--wrong-output); default texts recompile with kotlinc 2.0.0 to
the original bytecode for every method and $default method (constructor init nops
aside). Base -> branch default hunks on the five Kotlin inputs: all recognised by the
four new recognisers alone; compat trees and the five Java jars (default)
byte-identical. Re-recorded: TestDefaultArgsCall (Shape.area(scale: Int = 1)).
Gates: cargo test -p kuna-jvm 688+9 passed; corpus 1097/1097, 0 regressions;
clippy 1.98.1 clean; check_spec --strict OK; kuna_fix_check OK;
test_kuna_fix_diff* OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
… scopes and loop layout

109,005 methods perfect on every metric (98.88%), 7,987 classes
recompile, 15,363 methods better and 32 worse (27 from the options, 5
from fixes); readTypeAnnotations, filterFields and
AbstractClassGenerator.generate no longer score worse than Vineflower.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…created; companion lookup and refreshed names

Three fixes (KunaFix ids, no options):

unlisted_anonymous_class (p8_structure/kuna_fix_unlisted_anonymous_class.rs,
hook in ClassesProcessor.loadClasses): kotlinc's copies of an inline function's
objects and lambdas ($$inlined$ classes) list themselves as anonymous and name
the method they were inlined into, which does not list them, so ClassesProcessor
left them (and their continuations) at top level: $-named constructor calls and
classes reading this$0 / $x$inlined fields. Such a class is now nested in the
class its EnclosingMethod names; NestedClassProcessor maps its captures at the
new. okhttp 17, kotlin-stdlib 37, coroutines 1.10.2 128 / 1.8.0 125 classes;
$$inlined mentions 33/75/404/359 -> 0. kotlinc 2.0 errors: okhttp 1939 -> 1646,
coroutines 1.10.2 12851 -> 11537; with the other two fixes 11535 (1.8.0 12525 ->
11180), okio 307 -> 306.
Reproducer pkg/TestUnlistedAnonymousClass (kotlinc 2.0.0): default text
recompiled unedited (send/offset) and run against the original on 48 inputs:
identical output.

kt_companion_nameless_class (kotlin_writer.rs): KotlinWriter.writeClass looks
the companion up by n.simpleName.equals(..) and throws on an anonymous class
sorting before it, failing the whole class (Vineflower 1.12.0 too:
KotlinWriter.java:301 NPE); nameless classes are skipped. Exposed in okhttp's
Http2Connection by the nesting; unit test on own classes (Vineflower writes no
unit for a class it fails whole, so no corpus case).

refresh_printed_var_names (p6_variables/kuna_fix_refresh_printed_var_names.rs):
VarProcessor.refreshVarNames, run by insertLocalVars on nested classes' methods,
renamed a parameter to the clashing name a same-named local already prints:
`var countx: Long = countx` (Vineflower jar too; okio Pipe), `valuex = valuex`
in 6 coroutines objects once nested. Clashing names are reserved first and only
variables without one are renamed. Case pkg/TestRefreshPrintedVarNames
(--wrong-output): Vineflower's text is rejected by kotlinc, the default text
compiles unedited, runs identically to the original on 30 inputs and its
javap -c matches the original's drain except the steps++ temp.

Collateral: base vs final default, every hunk accounted (kuna_fix_diff.py:
RECOGNISERS unlisted_anonymous_class / refresh_printed_var_names, new
UNIT_RECOGNISERS for the units the nesting removes); mine alone accept all but
one Rust line number in a failure comment. Five Java jars default byte-identical;
compat trees identical apart from kotlin_writer.rs line numbers; corpus
1093/1093, 0 regressions; cargo test -p kuna-jvm green; clippy 1.98.1 clean;
check_spec --strict OK. Cases re-recorded (diffs read): TestJavaTypeSyntax,
TestInlinedObjectCaptureKt$dropFirst$$inlined$source$1 and
TestMembersWithoutMetadataKt$counting$$inlined$feed$1$1 (new compare_fallback:
their class is now written inside another unit); the cell case drops
TestMembersWithoutMetadataKt.class from its input (Vineflower's text unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…empty primary constructors (fixes kt_enum_when_mappings, kt_constructor_accessor_call, kt_empty_primary_constructor; kt_extension_call and kt_legacy_constructor_parameter_checks widened)

- kt_enum_when_mappings: the Kotlin plugin never runs SwitchHelper, so a when over an
  enum was written over X$WhenMappings.$EnumSwitchMapping$N[e.ordinal] with numeric
  labels. A pass after the main loop reads the mapping from the mapping class's
  <clinit> bytecode and writes when (e) { E.A -> .. }, null for a nullable subject's -1,
  the default's labels dropped (okhttp 2, kotlin-stdlib 11, coroutines 3 whens).
- kt_constructor_accessor_call: a new of the synthetic <init>(.., DefaultConstructorMarker)
  that only forwards (recognised by its code) names the real constructor, marker dropped
  (okio 2, okhttp 16, kotlin-stdlib 26 calls).
- kt_empty_primary_constructor: `()` for a primary constructor without parameters when
  the class has secondary constructors (okhttp Dispatcher, OkHttpClient.Builder).
- kt_extension_call: a member extension's dispatch receiver may be a local assigned once
  from `this` (inlined apply/run/with); okhttp 5, coroutines 1.8.0 7 / 1.10.2 9 calls.
- kt_legacy_constructor_parameter_checks: the opening Kotlin 1.3 checks of a constructor
  whose delegation call comes later inside branches are dropped (okhttp MultipartReader);
  the call stays in the body.

Evidence: kotlinc 2.0 errors okio 307 -> 305, okhttp 1939 -> 1887, coroutines 1.10.2
12851 -> 12833. Reproducers (kotlinc 2.0.0, five two-pass --wrong-output cases): new
texts recompile unedited (accessor, empty primary, member extension) or with Byte->Int
and the enum getEntries removed (enum); reflection harness, all methods on all enum
values + null and pooled arguments: 33/25/17/73 calls equal to the originals. Five Java
benchmark jars byte-identical by default; compat trees of the five Kotlin inputs
byte-identical; base->new default hunks (96) all accepted by the new recognisers alone.
Gates: cargo test -p kuna-jvm 689+9 passed, 3 ignored; corpus 1099/1099, 0 regressions;
clippy 1.98.1 -D warnings clean; check_spec --strict OK; kuna_fix_check OK; recogniser
tests 200 OK. Re-recorded: TestHiddenEmptySuperCall (Companion(null) -> Companion()).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
… header hunks

The four default recognisers pair defaults by parameter name and, when the rest of a
header hunk changed too (types, a renamed receiver in the next line), put the changed
defaults back and let the tool classify what remains. A context recogniser
(FIX_GROUPS ktdefaults_composed) handles hunks the line matching joins because a
changed header no longer anchors it (okhttp RequestBody: removed @jvmoverloads
overloads before `File.asRequestBody`). Compat-vs-default `other` per changed file
(77 of 82 files; 5 exceed the 900 s cap in both runs): the same before and after.
test_kuna_fix_diff_ktdefaults 9 OK, test_kuna_fix_diff 145 OK, kuna_fix_check OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…hanges on the same lines

kt_enum_when_mappings in context: a when hunk whose subject or branch code another fix
also changed (compat `CancellableKt.startCoroutineCancellable(block, ..)` against
`block.startCoroutineCancellable(..)`) is accepted when each such line pair is another
recogniser's. Verified on kotlinx-coroutines 1.8.0/1.10.2 CoroutineStart.kt compat vs
default: 0 other (base 1 and 0). Recogniser tests 201 OK, kuna_fix_check OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
Fixes kt_default_args_bridge_lookup (Kotlin defaults read from the
$default method kotlinc emits, found by its JVM name and descriptor, also
in DefaultImpls; Vineflower looked it up by the Kotlin name and could
print another function's defaults), kt_default_value_names,
kt_default_value_coercion_cast and kt_delegated_member_default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…unk another fix changed too

With the anonymous-class constructor calls and the object expressions or lambdas
written for them one placeholder, and without the locals kept for an object that
nothing else reads, the rest of the hunk is unchanged or another fix's recogniser
accepts it (compat-vs-default in kotlinx-coroutines: BroadcastKt, FlowKt__ZipKt
and FlowKt__MigrationKt, where an extension or facade call is resugared around
the object).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…ls (fixes ambiguous_type_variable_argument, unboxed_argument_type)

ambiguous_type_variable_argument: appendParamList casts an ambiguous argument
to the parameter's type with the method's type variables replaced by the
inferred type. For a parameter declared as the method's own type variable that
is the argument's own type, which makes a more specific non-generic overload
applicable: Guava Joiner/MapJoiner appendTo(StringBuilder, Iterator) printed
this.appendTo((StringBuilder)builder, parts), a call of itself. Where the call
as written resolves elsewhere (JLS 15.12.2 phases 1-2 over the named class's
member methods), the argument is cast to the variable's erasure and inference
binds the variable to it, so a result cast the bytecode makes is printed:
appendTo((Appendable)builder, parts). No upstream fix on develop/1.13.0.

unboxed_argument_type: the generic signature's parameter type replaced the
primitive type appendParamList chose for an argument it writes without its
boxing call, so a byte/short constant lost or swapped its narrowing cast:
spring-framework BeanUtils Map.of(.., byte.class, 0, short.class, 0, ..)
boxed Integers. The chosen type now stays: (byte)0, (short)0.

Evidence: reproducers TestAmbiguousTypeVariableArgument and
TestUnboxedArgumentType (javac 17.0.21.0.3 -g); default output recompiles to
bytecode identical to the original for every class (javap -c -p); compat text
recompiled throws StackOverflowError in appendTo/appendAll, calls show(String),
boxes Byte/Short/Integer for Short/Byte/Byte. Collateral (default before vs
after, 5 Java jars, okio/okhttp/kotlin-stdlib/coroutines 1.8.0+1.10.2 as Kotlin
and as Java): 3 hunks, Joiner 2 and BeanUtils 1, all recognised; Kotlin output
byte-identical. Compat output byte-identical on the 5 Java jars. javabench
kuna-jwrong5 vs kuna-fix12: 2 methods better (Joiner appendTo, now
byte-exact), 0 worse; classes recompiling 7987 -> 7988.
Gates: cargo test -p kuna-jvm 687 passed, 0 failed, 3 ignored; corpus
1095/1095, 0 regressions; clippy 1.98.1 -D warnings clean; check_spec --strict
OK; kuna_fix_check OK; test_kuna_fix_diff 149 OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
Fixes ambiguous_type_variable_argument (an argument cast to its own type
for a type-variable parameter made javac call another overload: Guava's
Joiner.appendTo called itself) and unboxed_argument_type (byte and short
constants passed to a generic parameter boxed as Integer).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
Fixes kt_enum_when_mappings (Kotlin when over an enum printed through
its synthetic $WhenMappings array), kt_constructor_accessor_call
(synthetic DefaultConstructorMarker forwarders called with a null
marker) and kt_empty_primary_constructor, and widens kt_extension_call
and kt_legacy_constructor_parameter_checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…sted copy is given

Three nested copies lost a constructor argument: okhttp's
sendDegradedPingLater and pushRequestLater objects and kotlin-stdlib's
DeepRecursiveScopeImpl continuation (EmptyCoroutineContext) kept an
init { this.$x = $x } store of a value they did not show. kotlinc gives no
debug name to the locals holding an inlined function's arguments there;
StackVarsProcessor.setEffectivelyFinalVars keeps a local an anonymous
class's constructor takes only through its LocalVariableTable entry, so the
Kotlin inliner folded the local into the new and NestedClassProcessor saw no
capture. For a class unlisted_anonymous_class nested, a local whose
definition a constructor argument reads (directly, through SSA phi roots or
a stack copy) now stays a local (takes_local).

A captured variable took its name from the creating method variable's debug
entry, while the creating method declares the variable from its first
store, which has none when kotlinc re-stores an inlined argument under the
debug name (arrayOf(..) as flows$iv in coroutines' combine, okhttp's
pingIntervalNanos): the object read a name nothing declares. The entry is
renamed to the creating method's name (captured_lvt).

Kept as Vineflower's top-level classes: suspend lambdas in Kotlin output
(the writer prints a failure comment at the site, which shows no argument)
and copies created in synthetic methods remove-synthetic hides.

Old vs new constructor-call arguments of every nested copy (base vs final
default trees): okhttp 17 calls / 97 arguments, kotlin-stdlib 37 / 18,
coroutines 1.10.2 62 / 54, 1.8.0 62 / 52: no value argument missing; the 25
variable arguments the object does not mention (okhttp 20, coroutines 5 per
version) are fields the old class never reads outside its constructor.
Every argument whose field the old class reads reaches the object (okhttp
51, stdlib 18, co110 49, co18 44): written there or declared as a local the
object reads. $$inlined mentions okhttp 33 -> 0, stdlib 75 -> 0, coroutines
404 -> 21, 359 -> 21 (kept suspend lambdas and synthetic-method copies).
kotlinc 2.0 errors: okhttp 1939 -> 1637, coroutines 1.10.2 12851 -> 11786,
1.8.0 12525 -> 11428, okio 307 -> 306.

Unit tests on own kotlinc 2.0.0 classes (tests/data/ktinlined2): a nameless
captured local stays a local; a suspend-lambda copy and a synthetic-method
copy stay classes; a re-stored argument is read by the name its creator
declares (that text recompiles and returns what the original returns on
1920 inputs; the previous text failed: unresolved reference 'values$iv').
Five Java jars byte-identical to base; compat trees of the Kotlin inputs
identical to base apart from kotlin_writer.rs line numbers in failure
comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
Fixes unlisted_anonymous_class (the anonymous classes kotlinc copies out
of inline functions are nested where their EnclosingMethod says, so they
print as object expressions with every value they are given),
kt_companion_nameless_class and refresh_printed_var_names (a renamed
parameter took a local copy's name, printing `var countx = countx`).

Also carries the branch's edits to three existing kuna-fixes cases
(compare_fallback) and restores four case summaries that earlier merges
of ktunrep, jpatterns, ktanon and ktctors dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…rfect on every metric

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…l, kt_raw_generic_type

Kotlin output only; compat mode byte-identical to the base on okio 2.6.0, okhttp 4.7.2,
kotlin-stdlib 2.2.20 and kotlinx-coroutines 1.8.0/1.10.2; the five Java benchmark jars are
byte-identical to the base in default mode.

- kt_implicit_widening: a Byte, Short or Char value (or an Int behind an implicit i2l) stored,
  passed or returned where a wider numeric type is expected takes the conversion the source
  wrote (`this[i++].toInt()`, `c.code`, `readShortLe().toLong()`); Vineflower wrote it alone.
- kt_object_monitor_call: wait/notify/notifyAll are called on `(x as java.lang.Object)`, as
  the source writes them; Kotlin's Any has none of them.
- kt_raw_generic_type: kotlinc writes no LocalVariableTypeTable and Kotlin has no raw types.
  Locals of generic classes are declared with the arguments the class file proves (stored
  values' generic types, then parameter/return sinks and stored elements), mutable only
  where the method mutates through them, `ObjectRef<Any?>`, star projections where nothing
  proves them and nothing writes through; casts take their target's or operand's arguments,
  type tests star projections, both by Kotlin's read-only names; casts the typed locals make
  redundant are left out.

kotlinc 2.0 errors over the whole tree (default mode): okio 307 -> 197, okhttp 1939 -> 1343,
kotlinx-coroutines 1.10.2 12851 -> 11590. Cases TestKtImplicitWidening,
TestKtObjectMonitorCall, TestKtRawGenericType (kotlinc 2.0.0, two-pass); their default texts
recompile with kotlinc 2.0.0 and 13 of 18 methods are javap-identical to the originals (the
rest differ only in statement shapes Vineflower writes without these fixes; one lambda method
with a captured var is left out of the recompile). Every
base->branch hunk on the five Kotlin inputs is accepted by these fixes' recognisers alone.
Corpus 1095/1095 (14 kuna-fixes default texts re-recorded with this change).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…n fixes' hunks

kt_raw_generic_type's recogniser writes its types back raw and asks the other Kotlin fixes'
recognisers (and, for a hunk inside a local another fix keeps, their context recognisers);
the java.lang/java.util prefix is dropped only from the collection classes it renames. On
okio's and okhttp's 71 changed files the compat->default 'other' hunks are those of the base (13 = 13 on
the 42 files whose runs end within 300 s). Recogniser tests: 197 OK; kuna_fix_check OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
Fixes kt_raw_generic_type (Kotlin locals, casts and tests of generic
classes get the type arguments the class file proves, star projections
where nothing does), kt_implicit_widening (the .toInt()/.code/.toLong()
Kotlin needs where Java widens implicitly) and kt_object_monitor_call
(wait/notify/notifyAll through java.lang.Object).

Re-recorded TestKtEnumWhenMappings (a Byte temp returned as Int now
.toInt()) and TestUnlistedAnonymousClass (UnlistedSource<Int>,
UnlistedSink<*>) for the combination with this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…yped_array, kt_narrow_constant_local

Three kuna-only fixes of Kotlin output (no options; off under jvm_vineflower_compat):

- kt_legacy_parameter_checks: ResugarKotlinMethodsPass drops Kotlin 1.4's
  Intrinsics.checkNotNullParameter but keeps Kotlin 1.3's
  checkParameterIsNotNull; the checks that open a function's first block
  (each of a parameter written non-null) are dropped. okio 2.6.0 449 calls
  and okhttp 4.7.2 764 to 0; kotlinc 2.0 "illegal usage of inline
  parameter" okio 11 / okhttp 10 to 0.
- kt_inlined_to_typed_array: kotlinc's inlined toTypedArray()
  (Collection.toArray(new T[0]), Kotlin 1.3's with its TypeCastException
  null check) is written c.toTypedArray() (toTypedArray<T>() where the
  receiver's element type is not T); the null check is removed and the casts
  of the result left out. okhttp 18, okio 1, kotlin-stdlib 6, coroutines
  1.8.0/1.10.2 5+5 sites to 0.
- kt_narrow_constant_local: a local of integer constants (or if-expressions
  of them) that Vineflower types Byte/Short by its values is typed Int when
  every read takes an Int. okio 12, okhttp 6, kotlin-stdlib 5, coroutines
  3+3 locals.

Evidence: reproducers TestKtLegacyFunctionChecks, TestKtLegacyToTypedArray
(kotlinc 2.0 then rewritten to Kotlin 1.3's shapes with ASM),
TestKtNarrowConstantTemps recompile with kotlinc 2.0 (edits only for
pre-existing defects); javap -c -p per method: 10/13, 12/13 identical
(rest: Vineflower's own forms, checks identical), and every method of the
three classes run on sample inputs against the original: 151 + 27 + 56
calls, 0 differences. kotlinc 2.0 errors okio 117 -> 105, okhttp
529 -> 500, coroutines 1.10.2 10178 -> 10173. Base vs new default trees:
every hunk recognised by the new kuna_fix_diff recognisers (okio, okhttp,
kotlin-stdlib, coroutines 1.8.0 and 1.10.2); compat trees byte-identical;
the five Java benchmark jars byte-identical by default. Six kuna-fixes
cases re-recorded after review (their Kotlin 1.3 checks dropped, Byte
temporaries typed Int). cargo test -p kuna-jvm 719 passed, 3 ignored;
corpus 1129/1129, 0 regressions; clippy 1.98.1 -D warnings clean;
check_spec --strict OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…with other fixes' hunks

kuna_fix_diff.py: CONTEXT_RECOGNISERS['kt_legacy_parameter_checks'] takes
the Kotlin 1.3 checks that open functions out of the old file and accepts a
hunk holding such a check when every overlapping hunk of the result is
recognised (compat vs default, where one hunk holds a dropped check and
another fix's change). test_kuna_fix_diff_ktlegacy: 5 tests OK; all
test_kuna_fix_diff* modules 233 OK; kuna_fix_check OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…ructor, kt_data_class_property_parameters, kt_function_parameter_name, kt_contract_import, kt_unlabeled_sequence_break

Six Kotlin-output declarations and names kotlinc 2.0 rejects (KunaFix ids, no options):
- kt_escaped_class_name: a class whose Kotlin name is no identifier is written in
  backticks in short names, imports and outer-instance constructor calls
  (okio `public val Okio: `-DeprecatedOkio` = `-DeprecatedOkio``).
- kt_inner_class_constructor: an inner member class constructed on the current
  instance is called by its simple name (okhttp `RealCacheRequest(editor)`).
- kt_data_class_property_parameters: a data class's primary constructor declares
  the properties whose fields it stores first (stores taken out of init), and the
  SYNTHESIZED members (componentN, copy, equals, hashCode, toString) are hidden.
- kt_function_parameter_name: a function/constructor body reads a parameter without
  a LocalVariableTable entry by the header's metadata name, not var1.
- kt_contract_import: `contract` imported (or qualified) and the function annotated
  @OptIn(ExperimentalContracts::class).
- kt_unlabeled_sequence_break: an unlabelled break whose closure is a sequence is
  written `break` (Java's text), not `return@run` with no run around it.

Evidence (verified):
- kotlinc 2.0 over the whole default trees: okio 117 -> 108 errors (28 -> 29 files
  clean), okhttp 529 -> 470 (55 -> 60), kotlinx-coroutines 1.10.2 10,178 -> 10,080
  (104 -> 107).
- Six two-pass --wrong-output Kotlin cases (kotlinc 2.0.0 -jvm-target 17): every
  compat text rejected by kotlinc 2.0 (27/11/26/11/9/7 errors), every default text
  (decompiled with the JDK runtime) compiles; javap -c -p of the recompiled classes
  equals the original for all methods but init-block nops, VF's own `twice`/`shift`
  inlining forms and the sequence-break `write`, which ran 200,000 random inputs
  against the original with 0 differences.
- Collateral: every base->branch default hunk on okio, okhttp, kotlin-stdlib,
  kotlinx-coroutines 1.8.0 and 1.10.2 is accepted by these six recognisers alone;
  compat trees of the five Kotlin inputs byte-identical to base; the five Java
  benchmark jars byte-identical in default mode.
- cargo test -p kuna-jvm 714+9 passed, 3 ignored; corpus 1135/1135, 0 regressions;
  clippy 1.98.1 -D warnings clean; check_spec --strict OK; kuna_fix_check OK;
  test_kuna_fix_diff* 234 OK. Re-recorded (diffs read: an inner class constructed
  by its simple name): TestDefaultArgsCtorDescriptor, TestExtensionReceiver,
  TestSupertypeTypeArguments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…er Kotlin fixes' hunks

compat->default on the changed files of okio, okhttp, kotlin-stdlib, kotlinx-coroutines
1.8.0 and 1.10.2 (kuna_fix_diff.py per file, 300 s cap) showed six hunks where
kt_data_class_property_parameters met another fix's change in the same lines (Pair,
Triple, IndexedValue, TimedValue: generic types and Java casts; okhttp Header and
CustomTrustRootIndex: legacy parameter checks). The context recogniser now drops the
lines the fix takes out of the old side and takes the new header back to plain
parameters, then requires the rest of the hunk to be accounted for as before (one level
deep). Verified: those files' `other` hunks are now the base's own (AndroidPlatform:168);
every other changed file's `other` set equals the base's (AwaitAll checked with a
1800 s cap); every base->branch hunk is still accepted by the six recognisers alone;
kuna_fix_check OK, test_kuna_fix_diff* 234 OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
Fixes kt_escaped_class_name, kt_inner_class_constructor,
kt_data_class_property_parameters, kt_function_parameter_name,
kt_contract_import and kt_unlabeled_sequence_break: declarations and
names kotlinc rejected (class names with `-`, inner classes built
through the outer class's name, data class properties declared in the
body with their generated members printed, contracts without their
import, a return@run with no run).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
Fixes kt_legacy_parameter_checks (kotlinc 1.3's checkParameterIsNotNull
at the start of every Kotlin function, which kotlinc rejects on inline
lambda parameters), kt_inlined_to_typed_array (1.3's inlined
toTypedArray() printed as toArray(arrayOfNulls(0))) and
kt_narrow_constant_local (constant temps typed Byte or Short where every
use takes an Int).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
…ine state machine

Four Kotlin-output fixes (KunaFix ids, no options):

- kt_suspend_state_machine: a CFG pass first in the Kotlin graph passes
  recognises a metadata-suspend function's state machine instruction by
  instruction (entry, label dispatch, spill/label stores, the suspend call
  passing the continuation, the COROUTINE_SUSPENDED check, resume blocks of
  restores + throwOnFailure(result)) and rewrites the graph into the direct
  path; split try ranges are joined; a Unit tail call's check is removed.
  Shapes it cannot map (continuation used by inlined
  suspendCoroutineUninterceptedOrReturn, aliases in reused slots) keep the
  machine.
- kt_suspend_call_continuation: suspend calls in a suspend function are
  written without the $completion argument; $completion.getContext() is
  kotlin.coroutines.coroutineContext; the cast/unboxing kotlinc puts on a
  suspend call's Object result is left out where it is the callee's
  declared type; such results are as nullable as declared.
- kt_coroutine_boxing: Boxing.boxInt(x) etc. written as implicit boxing.
- kt_suspend_function_type: metadata suspend function types are written
  suspend (T) -> R.

Evidence (verified in this run):
- kotlinx-coroutines 1.10.2: 79 of 87 metadata-suspend state machines and
  52 tail-call checks rebuilt; kotlinc 2.0 errors 10,178 -> 6,979 (clean
  files 104 -> 107). 1.8.0: 106 of 114 machines, 52 checks; 9,792 -> 5,597.
  okio and okhttp output byte-identical (117 / 529 errors unchanged).
- Reproducers (kotlinc 2.0.0) recompiled from the decompiled text: 540/540
  scripted runs (suspending and immediate calls, resumption with values and
  exceptions) equal to the original classes; 14/21 methods byte-identical
  after normalisation, the rest differ in local slots, loop forms and an
  inline call that is inlined. Five rebuilt kotlinx-coroutines 1.8.0
  functions (joinAll x2, cancelAndJoin, fixedDelayTicker) recompiled
  unchanged and behave as the library on 5 scenarios; cancelAndJoin and
  fixedDelayTicker recompile to identical bytecode.
- Collateral: compat trees byte-identical to the base on okio, okhttp,
  kotlin-stdlib and both kotlinx-coroutines; base -> new default diffs on
  kotlin-stdlib, co110, co18: 0 other hunks in kuna_fix_diff.py with the new
  recognisers; the five Java benchmark jars byte-identical.
- Gates: cargo test -p kuna-jvm 714 + 9 passed, 3 ignored; corpus
  1133/1133, 0 regressions; check_spec --strict OK; clippy 1.98.1 clean;
  kuna-cli jvm_cli 43 passed; clitests 330/331 (the known datatest probe);
  test_kuna_fix_diff 155 OK.

Five new two-pass cases; seven existing kuna-fixes default texts
re-recorded (their suspend functions are now direct bodies) and the unit
tests of kt_synthetic_object_super_call, captured_var_through_cast,
handler_copy_shared_body and kt_anonymous_class_failure_reason run with
kt_suspend_state_machine off; the unlisted_anonymous_class test reads the
tail call without its check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4
Fixes kt_suspend_state_machine (a suspend function is printed as its
direct body instead of kotlinc's state machine: continuation class,
label dispatch, spills and COROUTINE_SUSPENDED checks), and
kt_suspend_call_continuation, kt_coroutine_boxing and
kt_suspend_function_type for the calls, boxing and function types around
it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BBaWRszChd3Ho4R3P4cA4

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

full-ci Run the full cargo workspace suite on this PR before merge (internal PRs skip it by default)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants