Kotlin printer: review of 2026-10-15, idiom gaps and order of work for KotlinConf
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 10/100
Research direction
This is a review and planning ticket, not a single change: it asks for a prioritised list of printer improvements, to be split into their own issues. Start with the Kotlin printer's expression code, including KotlinNullability.receiverWithDot and the SAFE_CALL path, and with KotlinMethodPrinter:56. Done means the listed items exist as separate issues or checklist entries, each with its own acceptance criteria.
Written by the indexing model from the issue text.
Description
A review of the Java → Kotlin translation (2026-10-15), against two goals: the KotlinConf claim (correct output, more idiomatic than J2K, the gain attributable to the analyses) and the longer one of a translator whose output a Kotlin developer would accept as hand-written. Sources: the ratchets, the scoreboard numbers on maddi-mod#35/#36, the printer source at f012f16e9, and the output of the #125 measurement run (build/j2k/langchain4j-preconditions-off, fernflower-preconditions-off).
Verdict. On track for the first goal; not yet for the second. The output is correct, compact, Java-shaped Kotlin: the structure is idiomatic, the expressions are not.
What is strong
- Correctness: langchain4j-core 344/346 compiling, 1,106 tests (J2K 148 / 326); fernflower 199/199, 264 (J2K 37 / 39); guava translates in full where J2K does not complete.
- The ablation attributes it to nullability: langchain4j 301 → 343 compiling from nullability alone.
- Structural idioms are done:
when,object :,enum class,inner,fun interface,use,synchronized, labels,IntArray, expression bodies,const val,@JvmStaticcompanions, smart casts afterisand!!,valfor once-assigned locals, records →data class.
What the output looks like
ToolExecutionResultMessage.kt (langchain4j, fresh run):
private val contents: MutableList<Content> // built by Utils.copy, never mutated → List
this.contents = java.util.List.of(TextContent.from(builder.text)) // → listOf(...)
if (contents.size == 1 && contents.get(0) is TextContent) {return (contents.get(0) as TextContent).text() }
return (Objects.equals(this.id, that.id) && ...) // → this.id == that.id
override fun toString(): String = "ToolExecutionResultMessage{id='" + id + '\'' + ... // → a template
open class Builder { internal var id: String? = null ... } // `open` on everything
MergeHelper.kt (fernflower), the densest !! region:
initExprents[1]!!.fillBytecodeRange(initExprents[1]!!.getRight()!!.bytecode)
stat.getIncExprent()!!.fillBytecodeRange(initExprents[1]!!.getRight()!!.bytecode)
val arr: ArrayExprent?
arr = firstDoExprent.getRight() as ArrayExprent?
Every !! there is correct (short-circuit || defeats the smart cast), but a Kotlin author hoists initExprents[1]?.right into a val once. That shape is where fernflower's 2,946 !! come from: repeated calls to nullable getters, which no smart-cast pruning removes.
Translated tests: memory!!.add(...) on a fixture set in @BeforeEach (→ lateinit), UserMessage.from(("1" as String?)) (redundant cast and parentheses), java.util.List.of(m1, m2).
Idiom scorecard (printer source today)
| Done | Partial | Absent |
|---|---|---|
val/var, when from switch, object :, enums, inner, use, synchronized, labels, IntArray, lambdas/SAM, ternary → if, smart casts, lateinit, isNullOrEmpty, records → data class |
read-only collections (73 of 659 langchain4j declarations), properties (JDK getters only; a Java getX() stays a call), ?./?: (only under NullCheck.SAFE_CALL, off by default, or from Kotlin-parsed source), ranges (no indices/withIndex), index syntax (charAt only), also/run (generated wrappers only), variance (use-site only; multiple bounds dropped) |
string templates, ?./?: derived from Java null checks, let/apply, trailing lambdas and it, listOf/emptyMap, == for equals, streams → collection ops, Optional, buildString, object singletons / top-level functions, default arguments, @Throws, when from if-chains, equals/hashCode/toString → data class |
Counted on the fresh langchain4j output (29.6k lines with tests): ?. 0, ?: 0, .let 0, apply 0, string templates 8 against 194 concatenations, .stream() 61, Optional 101, fun getX()/setX()/isX() 63. fernflower: !! 2,946, ?. 0, ?: 0.
Risks
!!is the headline idiom number, and fernflower is 3× J2K (2,946 vs 997). J2K's output does not compile, which is the answer, but the table will be read.- The modification half of the claim is thin.
val/varis identical in all four ablation arms; read-only collections reach 11% of declarations. #25's world-wide parameters and locals are the remaining work; otherwise the claim should say "nullability" and present read-only types as the secondary effect. - Zero
?./?:is the first thing a Kotlin audience notices. The printer has theSAFE_CALLpath (KotlinNullability.receiverWithDot) and an elvis path for Kotlin-parsed source; deriving them from Java'sx != null ? x.m() : nullandif (v != null) v else dis local, pattern-based work. - Architecture. Straight from the Java AST to text, rewrites scattered through a 2,136-line expression printer, no intermediate Kotlin model. Fast for structural idioms; each expression-level rewrite (templates, collection ops, hoisting) becomes a special case in that file.
- Docs and tests lag.
kotlin-printing.mdhas no dated log and still quotes 84 compiling files; javadocs inKotlinTypeName,KotlinFieldPrinter,KotlinMethodPrintercall implemented features "refinements";KotlinMethodPrinter:56(static initializer asinit) may be a real gap. 92@Testmethods for 7.3k lines; the ratchets measure compile-and-pass, not idiom. The idiom numbers live only in issue comments. - Noise that costs nothing:
openon every class and method (the closed world knows what is subclassed), one-line{x }blocks, splitval arr: T?/arr = …,java.util.List.of,(x as T?).
Proposed order, for 11-25
?./?:from Java null-check shapes;==forObjects.equals/equals.- Hoist repeated nullable getter calls into a
val(finishKotlinValCopies): the fernflower!!lever. - maddi-mod#25 across the world for parameters and locals, so the modification analysis moves a number. If it cannot land, narrow the claim's wording now.
- One cleanup pass: drop
openwhere nothing in the world subclasses;listOf/emptyMap/list[i]; templates for concatenations;lateinitfor@BeforeEachfixtures; brace-block formatting. - Commit the scoreboard as a results file with SHAs; give
kotlin-printing.mda dated log, asnullability.mdhas.
Parked until after the proposal: streams → collection ops, Optional, data classes from equals/hashCode/toString, getX() → properties for Java-parsed code, default arguments, object singletons.
Each item here should become its own issue or a checklist entry when picked up; this ticket is the review.
🤖 Generated with Claude Code
- Dominant language
- Java
- Stars
- 1
- Forks
- 1
- Avg merge
- 1h 58m
- Merged PRs (30d)
- 5
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from CodeLaser/maddi
-
build/ci good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
bug front-end:javac
Difficulty 4/5 3-5 days Newbie friendliness 42/100
-
C# printer: an inner class of a generic class needs the outer type's arguments (Outer<A>.Inner)Openfront-end:csharp
Difficulty 4/5 3-5 days Newbie friendliness 30/100
-
Java → C# translation: an idiomatic C# printer driven by the modification and nullability analysesOpenenhancement extension front-end:csharp
Difficulty 5/5 Over a week Newbie friendliness 8/100
-
enhancement extension front-end:csharp
Difficulty 5/5 Over a week Newbie friendliness 8/100
Similar issues
-
BC remittance upload: a claim MSP refused (S03) is saved with a blank status instead of refused (F)Opentype: bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
carlos-emr/carlos#4933 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Vakore/ZappierGames#147 ·
-
spigotmc
Difficulty 1/5 Under an hour Newbie friendliness 92/100
Dans-Plugins/Wild-Pets#356 ·
Maintainers usually reply within 1 day
-
spigotmc
Difficulty 1/5 Under an hour Newbie friendliness 85/100
Dans-Plugins/Medieval-Roleplay-Engine#371 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
eddyizm/tempus#1220 · 1 comment ·
Maintainers usually reply within 2 days