Pangram verdict · v3.3
We believe that this text is a mix of AI and human-written content.
AI likelihood · overall
AIArticle text · 1,226 words · 20 segments analyzed
Many years ago I worked on an alternate Java compiler and runtime called gcj, the GNU Compiler for the Java Programming Language. These days, my preferred programming language for most tasks is Common Lisp, but I’m still interested in Java runtimes.Back in late 2024, I wanted to use a Java library from SBCL, but couldn’t find an integration approach that suited the application. This led me to explore the idea of building a simple JVM bytecode processor in Lisp so I could execute the code directly in process.I started before coding agents became useful for this kind of work, and wrote the first working version by hand.OpenLDK is a JIT compiler and runtime for Java, written in Common Lisp.
It reads Java bytecode, translates it into Lisp, and lets SBCL compile that Lisp into native machine code.
Java objects live in the same process and heap as the Lisp application.My goal is to make Java libraries feel at home in Common Lisp.
For general Java execution, HotSpot will be much faster.In the last few days, however, OpenLDK has gotten much better. I’ve experimented with JVM-hosted languages and Spring Boot application execution.OpenLDK leads to some wonderfully strange possibilities.
Java classes become CLOS classes.
Lisp can add methods around methods written in Kotlin. A Clojure function can become something you pass to funcall. Java’s own compiler runs inside SBCL. And an initialized Spring Boot application can be saved in a Lisp image and launched again in about 84 milliseconds.You can even connect Emacs and SLY to a Spring Boot application running on OpenLDK, inspect its objects, and monkey-patch its behavior while it keeps running.Here is a tour of how it works, what it can do, and what that startup number actually means.The compilation pipeline begins with ordinary class files. OpenLDK targets JDK 25 and uses OpenJDK’s runtime libraries, reading them from the JDK’s JMOD files or its lib/modules container.When a class loads, OpenLDK creates its CLOS representation and installs stubs for its methods. Calling a method for the first time triggers compilation: bytecode becomes an intermediate representation, then Lisp, then native code through SBCL. The compiled definition replaces the stub.That laziness matters. Loading a library does not mean compiling every method in it. Much of a large library may never be used by a particular application.Consider a Java method:public static int add(int x, int y) { return x + y; } Stripped of the generated control-flow scaffolding and exception machinery, its translated body amounts to:(sb-c::mask-signed-field 32 (+ x y)) The signed-field operation preserves Java’s 32-bit integer overflow behavior. With typed operands, SBCL can compile this to a machine addition. OpenLDK handles Java’s semantics while drawing on the compiler SBCL already provides.The translation also makes the program accessible to Lisp.
Java fields become slots.
Instance methods become methods of CLOS generic functions, identified by their JVM signatures so overloaded methods remain distinct.
Static methods become ordinary Lisp functions.
Java’s exception hierarchy has a corresponding Lisp condition hierarchy.Compiled methods are retained in a persistent code cache. Subsequent runs can load cached code when the classes it depends on remain unchanged, and cache entries are separated by OpenLDK build. Different images can reuse compiled JDK methods too.The most interesting consequences appear when an application starts using these representations directly.Foreign source can appear directly inside Lisp source. After starting the relevant languages and enabling OpenLDK’s source syntax, a Kotlin expression looks like this:(let ((n 4)) #@kt{ (1..$n).map { it * it } }) ;; => (1 4 9 16) $n passes the Lisp value into the Kotlin code as an object. Lexical variables, collections, and callbacks can cross the boundary without being serialized into source strings.The same mechanism supports Scala, Groovy, Clojure, Kawa, JScheme, and ABCL. Each block keeps the language’s own syntax. Clojure also has a #[ ... ] notation.A Clojure function can come back as a callable foreign-function object. The surrounding Lisp invokes it with ordinary funcall, with argument and result conversion handled by the interop layer. The reverse direction works too: Lisp functions can become callbacks for the other languages.You can also nest those source blocks. Here is the entire examples/one-form.lisp file, verbatim. After the setup, one expression passes a value through all seven languages:;;;; One form, seven languages ;;;; ;;;; A single Lisp expression in which every JVM language OpenLDK runs does ;;;; one thing it is known for, written in its own syntax, and hands its ;;;; result to the next. Read it from the inside out: ;;;; ;;;; Clojure takes ten primes from an infinite lazy sequence ;;;; (2 3 5 7 11 13 17 19 23 29) ;;;; Groovy squares them with the spread-dot operator and sums: 2397 ;;;; Kawa adds one with Java's Long.sum, by colon notation, and ;;;; divides exactly by the first of 6, 5, 4, 3, 2 that doesn't ;;;; divide it evenly, escaping the search with call/cc: 1199/3, ;;;; returned as its numerator and denominator ;;;; JScheme sums each one's digits in a named let: (20 3) ;;;; Scala matches the pair against a list pattern with a guard: 60 ;;;; Kotlin pours it into a local data class, copies it, and sweetens it ;;;; with when and the Elvis operator: 61 ;;;; ABCL writes it with FORMAT, in Roman numerals and in English ;;;; ;;;; Each block is source in its language, as the language writes it.
;;;; $#@lang{ ...
} (or $#[ ...
] for Clojure) splices the value of the ;;;; block inside into the language around it, passed as an object.
;;;; ;;;; ./sbcl-polyglot --load examples/one-form.lisp ;;;; ;;;; prints: LXI (sixty-one) (require :asdf) (push (uiop:getcwd) asdf:*central-registry*) (asdf:load-system "openldk/polyglot") (polyglot:startup :verbose nil) (setf *readtable* (polyglot:enable-syntax (copy-readtable))) (format t "~&~A~%" #@abcl{ ; ABCL: FORMAT's Roman numerals and English (let ((n $#@kt{ // Kotlin: a local data class, copy, when, ?: data class Cup(val tea: Int, val sugar: Int?
= null) val cup = Cup($#@sc{ // Scala: a list pattern with a guard $#@jscheme{ ; JScheme: digit sums in a named let (map (lambda (n) (let loop ((n n) (sum 0)) (if (< n 1) sum (loop (quotient n 10) (+ sum (remainder n 10)))))) $#@kawa{ ; Kawa: Java by colon notation; call/cc; exact division (let* ((m (java.lang.Long:sum $#@gv{ // Groovy: the spread-dot operator $#[ (->> (iterate inc 2) ; Clojure: lazy and infinite (filter (fn [n] (not-any?
#(zero?
(rem n %)) (range 2 n)))) (take 10)) ]*.power(2).sum() } 1)) (d (call-with-current-continuation (lambda (found) (for-each (lambda (d) (if (not (= 0 (remainder m d))) (found d))) '(6 5 4 3 2)) 1))) (r (/ m d))) (list (numerator r) (denominator r))) }) } match { case List(a: Int, b: Int) if a > b => a * b case List(a: Int, b: Int) => a + b } }).copy(sugar = null) when { cup.tea > 40 -> cup.tea + (cup.sugar ?: 1); else -> cup.tea } })) (format nil "~@R (~R)" n n)) }) The result:LXI (sixty-one) The $ before a nested block passes its result to the enclosing language.
The expression crosses seven language implementations, using their own syntax and libraries, inside one Lisp process.
I think that’s extremely cool.CLOS integration goes further.
Lisp can add a method that runs before Java’s ArrayList.clear():(openldk.interop:define-java-method :before ("java.util.ArrayList" "clear()V") (list) (format t "~&clearing ~D elements~%" (openldk.interop:java-call "size()I" list))) The signature’s V means the method returns Java’s void; size()I returns an integer.