A Report from the Trenches
Mateusz Kubuszok
Scala developer for almost 12 years
co-author and maintainer of Chimney for about 9 of them
blogged at Kubuszok.com
wrote Things you need to know about JVM (that matter in Scala)
several presentations about metaprogramming in Scala
| What a game needs | libGDX + Gradle | Scala + sbt |
|---|---|---|
| Desktop apps for Windows / macOS / Linux, built on one host | Construo: bundles a trimmed JVM and a launcher | sbt-native-packager: packages only for the platform you build on |
| Android: convert bytecode, shrink, sign, package | Android Gradle Plugin | sbt-android: unmaintained for years |
| iOS: compile to native code, package the app | RoboVM / MobiVM Gradle plugin | Nothing: RoboVM doesn't support Scala, Scala Native has no iOS |
| Browser build, and knowing which assets exist at runtime | GWT Gradle plugin; the build generates assets.txt, which GWT's preloader reads to fetch and list assets | Scala.js has no classpath, so getResourceAsStream finds nothing; and no build step embeds the assets or lists them |
| Native libraries for every platform | gdx-jnigen builds them; one JAR of native libraries per platform | JVM: extract from the JAR by hand; Scala Native: install the library on the system and pass linker flags yourself |
I had a demo like this in March
What you’ve just seen is the first iteration of a different approach
It needed tooling that Scala didn’t provide
SGE: Scala Game Engine
SSG: Static Site Generator
re-scale: a utility to trace the progress of porting with LLMs
Baltic Porter: a transpiler from Java to Scala
// gx += xAdvances[ii] commented out, so every letter is drawn in one spotThe agents aren't trustworthy because they are non-deterministic.
re-scale: a command-line tool and Claude Code plugin that checks every ported file against its original
| The check | How the agents got past it |
|---|---|
| Every file records the original's methods and size; CI checks the record | The agents marked 1,200 files "complete" in one go, recording whatever each file already had, and turned the CI check into a warning |
| Same methods as the original | Every method there, the insides hollow: leaving a loop became leaving the whole method; the line that moves to the next letter commented out |
| About the same size as the original | Caught a colour library at a fifth of its size, but full-size files could still be wrong: the minifier was bigger than the original, and about 40% of its tests passed |
| A scanner for words like "TODO" or "stub" | The agents reworded their comments until the scanner stopped finding them |
| A review by a different model | A model reviewing its own work explained the gaps away; with Fable off, the reviewer silently became the same model; one reviewer invented 3 of its 5 findings |
Baltic Porter: a translator to Scala 3, from Java first, later TypeScript, JavaScript and Dart. Same input, same output. Agents improve the translator; they don't port code.
| The approach | What still went wrong |
|---|---|
Each project has its own rules: Scala names, Nullable instead of null, opaque types, a few hand-written files | The rules are code too, so they need review |
| Agents work on the translator, never on the port | One "generator" for Mermaid only contained 187 pieces of the old agent port, copied byte for byte |
| Tests decide | An agent switched off 28 test suites and reported "0 failures"; another switched off 23 tests half an hour after regenerating the code |
| Generated code is never edited by hand | Agents patched it with find-and-replace and loosened test assertions; all reverted later |
| Same input, same output | Compiling is not the same as correct: a lost break, a wrong equality check; only running the tests found them |
The limit is now test coverage, not honesty.
Everything built along the way became multiarch-scala: sbt plugins and small runtime libraries
| In libGDX | JVM | Scala Native | Browser |
|---|---|---|---|
| LWJGL (OpenGL) | ANGLE via Panama | ANGLE via @extern | WebGL |
| LWJGL (windows, audio) | GLFW, miniaudio via Panama | GLFW, miniaudio | DOM, Web Audio |
| libGDX's own C code (buffers, images) | Rust library via Panama | same library, same C interface | pure Scala |
| FreeType (fonts) | Rust wrapper via Panama | static library | none |
| Box2D / Bullet (physics) | Rapier via Panama | static library | Rapier as WASM |
| Problem before | What multiarch-scala adds |
|---|---|
| JVM: no standard way to ship and load native libraries | A JAR with the native library for each platform, plus a small description file; a loader picks the right one and loads it |
| Scala Native: install the library on the system yourself, and write the linker flags yourself | The same kind of JAR carries ready-to-link libraries and their flags; adding the dependency is enough |
| You can only build for the machine you are on | Native binaries cross-compiled with zig; desktop JVM apps packaged for every system from one machine |
| No maintained sbt plugin for Android | An Android plugin: converting to Android bytecode, resources, signing |
Scala.js: no classpath, no ServiceLoader | Assets embedded at build time behind one API; a ServiceLoader generated at compile time |
| Part of SGE | State | Still maintained by hand |
|---|---|---|
| libGDX core | generated from upstream at build time | 5 hand-written overrides |
| Utilities and 5 small extensions | generated | almost nothing |
| The other ~690 extension files | porting rules not written yet | still the agent port |
Tests pass on JVM, JS and Native
Every change the translator makes to SGE core, by kind
getX()/setX() turned into Scala properties, empty () droppedNullable instead of nullPixels, Align, Input.Button, …)Sge context instead of global variables
Three SGE pull requests: about +8.8k / −133k lines, with tests and demos still passing
| Libraries ported into SSG | Source language | State |
|---|---|---|
| Markdown parser, Liquid templates | Java | generated, tests pass |
| KaTeX, terser, rough.js, Mermaid | TypeScript / JavaScript | still the agent port; 0–5% translated |
| Sass compiler | Dart | still the agent port; nothing translated yet |
| HTML/CSS minifier | Ruby | agent port, no translator |
Over twelve thousand tests pass on every platform, but the non-Java ones still run the agent-written code
Automatic porting of libraries is not a solved problem
Every LLM will mislead you
Being certain means reviewing all of the code: impossible at this size
So take away every chance to cheat: guardrails shrink the review, they never remove it
Use the LLM to build a deterministic migration, then run that
Scala already compiles to the browser and to native code; Java needs separate external compilers for both
The community already builds tooling for multiple platforms, without weird hacks in the build
Out of the box: backends, sometimes frontends, CLI tools - but we can make it work better on desktop and mobile
Scala Native works flawlessly only if the native libraries are already installed on the system - but we can support some way of distibuting precompiled binaries as well
All of it looks solvable, often by reusing existing solutions