> I think they even haven't adopted newer JVM features
You don't know what you're talking about. Not only Clojure steadily adopting newer JVM features (for when that makes sense) - java streams, functional interfaces, qualified method values, interactive lib loading, JDK21 virtual threads, etc., Clojure constantly explores beyond the JVM - e.g., Jank targets LLVM and has C++ interop.
Pick some hardcore JVM topics and try searching what Clojurists think about them - GC, profiling, concurrency, etc. There's tons of interesting, deeply involved things constantly being hacked together by incredibly knowledgeable folks. You're casually name-dropping "complexity" maybe without even realizing that it's a community that includes people who have written production experience reports on Shenandoah GC, built profiling tools that work around safepoint bias, and given conference talks on tri-color marking algorithms. Dealing with complexity is their bread-n-butter. Challenging Clojurists to debate about "complexity" is like dropping "the brain has neurons" around a group of neurosurgeons. They'd quietly say nothing, so you can "win your argumentation", but they'll just... know.
I was talking about the JVM bytecodes for dynamic languages.
Also I remember watching a recent talk, where virtual threads was still "being considered".
Having to write portable code that has to take into account the host differences, and difference in execution semantics, and still delivery the same outcome, is also complexity that keeps neurons busy.
You're talkiing about invokedynamic - bytecode instruction added in Java 7, specifically to make dynamic language dispatch efficient, right? Explained simply: JVM was designed for static types - method calls resolved at compile time. In dynamic langs you don't know the type of something until runtime had to hack around, typically by boxing everything and doing manual type checks. This was slow and awkward. JRuby/Groovy adopted it eagerly. Clojure's dispatch model though is different. Most calls are either: direct interop (already statically typed), or calls through a Var (is a reference to a function value, not a dynamic method lookup). The Var indirection is a different shape of problem that invokedynamic doesn't solve as cleanly. It's not that it's useless, just that the fit isn't as natural.
> virtual threads was still "being considered"
That is an outdated info. Clojure 1.12.0 shipped two years ago with virtual thread support, but the integration with core.async's thread pool model was not there (so you were not completely incorrect). However, core.async later reimplemented go blocks using virtual threads when available. The improvements are still underway https://clojure.org/news/2025/10/01/async_virtual_threads
> take into account the host differences
Okay, this one is genuinely not that straightforward. The #? reader conditional in .cljc files is a clean, minimal mechanism. I don't really know any other language that can target completely different platforms from a single namespace as cleanly - even in Nodejs you can't in practice do it as nicely. Kotlin Multiplatform is probably the closest competitor - but its `expect/actual` mechanism requires separate source sets, separate files, and considerably more boilerplate. You're not writing in the same namespace; you're wiring together parallel declarations. Scala.js and GHCJS are essentially separate compilation targets with thinner sharing stories. But yes, it still can get complicated - different hosts have meaningfully different concurrency and I/O models, so it's rather "shared logic, host-specific edges" rather than "write once run anywhere". I still think Clojure handles this all far more elegantly than alternatives.
So pragmatically speaking, you're pointing at complexity at the implementation/runtime layer, while Clojure's complexity reduction happens at a different layer entirely - data model, immutability by default, simpler concurrency reasoning, REPL workflow. Those layers mostly don't interfere with each other. You mentioned real concerns at the platform engineering level, but they in practice don't touch what Clojure is actually trying to simplify. Someone writing Clojure code never experiences invokedynamic problems one way or the other.
The complexity would be to grow like Common Lisp, instead it is up to Clojure folks to write Java, C#, JavaScript code, therein lies the complexity.