Rename src/jolt -> stdlib (the runtime-loaded layer; jolt-core stays the seed-baked layer) and update the loader / emit-image / doc paths. Drop dead code: the spike/ experiments, the duplicate clojuredocs-export.edn (json moves to tools/), the Janet-era jolt.http binding, and the orphaned persistent_vector.clj whose ns/path didn't even match. Strip porting residue from comments and docstrings across host/chez, jolt-core, stdlib, tests, and docs: internal issue ids, "Phase N" markers, and the "vs Janet" historical exposition, leaving present-tense descriptions and the real JVM-Clojure semantic contrasts. Same pass over the corpus suite labels. The seed is unchanged (docstrings/comments aren't emitted), so the self-host fixpoint and corpus are untouched. Port tools/spec_coverage.py off the dead janet probe to bin/joltc and regenerate coverage.md; drop the dead :host/janet rule from certify.clj and regenerate the conformance profile. Add docs/host-interop.md (the JVM shims and how to register your own host class from a library) and a writing-style note in CLAUDE.md. Stabilize the four racy concurrency corpus cases (future-cancel and agent send/send-off): give the future a sleeping body and the agent a slow action, so cancel reliably catches an in-flight future and deref reliably reads the pre-update snapshot. They certify deterministically now, so drop their :flaky allowlist entries and the orphaned legend.
8.1 KiB
Host interop and JVM standard-library shims
Jolt runs on Chez Scheme, not the JVM, so there are no real Java classes behind
interop forms. Instead the runtime ships shims for the slice of the JVM standard
library that portable Clojure code reaches for, so libraries written against
clojure.core and common java.* classes run unchanged. The Clojure interop
syntax works against these shims:
(Math/sqrt 2) ; static call
Math/PI ; static field
(StringBuilder.) ; constructor
(.append sb "x") ; instance method
(instance? String "hi") ; class token
A class token (String, java.util.UUID, …) resolves to a name; there is no
reflection and no class hierarchy. (class x) returns the JVM class name for the
scalar/collection types Clojure programs compare against ("java.lang.Long",
"java.lang.String", and so on).
What's shimmed
This is the surface today, not the whole JVM. Methods not listed generally aren't implemented; a few are accepted but no-ops (noted inline).
Numbers and language
java.lang.Math—sqrtcbrtpowexploglog10floorceilroundabsmaxminsincostanasinacosatansignumrandom; fieldsPI,E. (clojure.mathmirrors these as functions.)Long/Integer—parseLong/parseInt/valueOf(optional radix),MAX_VALUE,MIN_VALUE;(Integer. x).Double/Float—parseDouble,valueOf,toString,isNaN,isInfinite, the*_VALUE/*_INFINITY/NaNfields;(Double. s).Boolean—parseBoolean,TRUE,FALSE.Character—isUpperCaseisLowerCaseisDigitisWhitespace(ASCII).- Boxed-number methods — every number answers
.intValue.longValue.doubleValue.floatValue.byteValue.shortValue.toString.hashCode(integer projections wrap modulo their width, as on the JVM). java.lang.System—currentTimeMillisnanoTimeexitgetPropertysetPropertyclearPropertygetPropertiesgetenv.java.lang.Thread—sleep(real),yield/interrupted(no-ops),currentThread.java.lang.Object—(Object.)as a fresh-identity sentinel;.toString.hashCode.equals.getClasswork on any value.java.lang.Class—forName.
Strings and text
java.lang.Stringstatics —valueOf,format(theclojure.core/formatengine;String/formatwith a leading locale is accepted). Instance methods go throughclojure.string/ the native string ops.StringBuilder—appendtoStringlengthcharAtsetLength.java.text.NumberFormat—getInstancegetNumberInstancegetIntegerInstance;.format,.setGroupingUsed,.setMinimum/MaximumFractionDigits.java.util.StringTokenizer—hasMoreTokenscountTokensnextToken.java.util.regex.Pattern—compile(withPattern/MULTILINE),quote;.split,.pattern. (#"…"literals andclojure.stringregex fns are the usual entry points.)
Collections (mutable)
java.util.ArrayList—addgetsetsizeisEmptyremoveclearcontainstoArrayiterator.java.util.HashMap—putgetgetOrDefaultcontainsKeycontainsValuesizeisEmptyremoveclearputAllkeySetvaluesentrySet.
I/O
java.io.File—(File. path)/(File. parent child);getPathgetNamegetAbsolutePathgetCanonicalPathtoURItoURLexistsisDirectoryisFilelistFilesgetParent.java.io.StringReader/StringWriter/PushbackReader— theread/readLine/mark/reset/unread/write/append/toStringsurface the reader andwith-out-strrely on.java.lang.ClassLoader—getSystemClassLoader,.getResource,.getResourceAsStream(resolved against the source roots).
Time and date
java.util.Date—(Date.)/(Date. ms);getTimetoInstanttoLocalDate(Time)beforeafterequalstoString(RFC 3339).java.time—Instant(now,ofEpochMilli,toEpochMilli,atZone),LocalDateTime,ZoneId,DateTimeFormatter(ofPattern,ISO_LOCAL_*, localized styles),FormatStyle.java.text.SimpleDateFormat—(SimpleDateFormat. pattern);parseformattoPatternapplyPattern(setTimeZone/setLenientaccepted but ignored — formatting is UTC).java.util.TimeZone/java.util.Locale— constructed and passed through; only UTC is honored for formatting.
Net, encoding, misc
java.net.URL—(URL. spec);toStringtoExternalFormgetProtocolgetPathgetFile.java.net.URI— full component accessors (getSchemegetHostgetPortgetPathgetQuerygetFragment, raw variants,isAbsolute).java.util.Base64—getEncoder/getDecoderwithencode,encodeToString,decode.java.nio.charset.Charset—forName.java.util.UUID—randomUUID,fromString;(UUID. s).- Exceptions —
ThrowableExceptionRuntimeExceptionIllegalArgumentExceptionIllegalStateExceptionIOExceptionNumberFormatExceptionArithmeticExceptionNullPointerExceptionClassCastExceptionIndexOutOfBoundsExceptionFileNotFoundExceptionUnsupportedOperationExceptionand the common network exceptions, each with the(E.)/(E. msg)/(E. msg cause)/(E. cause)constructors.
What's deliberately absent: STM (clojure.lang.LockingTransaction/isRunning
returns false), reflection, gen-class/proxy of Java classes, and
BigDecimal.
Adding your own shim from a library
The built-in shims above are baked into the seed. A library or project can
register its own host classes at load time — no seed re-mint, no host edits.
Put the registration calls at the top level of a namespace your code requires.
Four functions (in clojure.core) plus the tagged-table seam (in jolt.host)
cover it.
__register-class-ctor! makes (Name. …) work; __register-class-statics!
makes Name/field and (Name/method …) work; __register-class-methods!
attaches instance methods to a tagged value; __register-instance-check! teaches
instance? about your class. Method and static names are strings (they match
the literal name in the interop form).
A stateful object is a tagged table — jolt.host/tagged-table creates one,
ref-put!/ref-get set and read its fields. Read the tag back with
jolt.host/ref-get (or test it with jolt.host/table?); a plain get /
keyword lookup deliberately can't see a wrapper's own :jolt/type.
(ns mylib.greeter
(:require [jolt.host :as host]))
;; (Greeter. name) -> a tagged value carrying its name
(__register-class-ctor! "Greeter"
(fn [name] (-> (host/tagged-table :greeter)
(host/ref-put! :name name))))
;; (.hello g) -> instance method, keyed by the literal method name
(__register-class-methods! :greeter
{"hello" (fn [self] (str "hi " (host/ref-get self :name)))})
;; Greeter/VERSION (field) and (Greeter/make x) (static method)
(__register-class-statics! "Greeter"
{"VERSION" "1.0"
"make" (fn [name] (Greeter. name))})
;; (instance? Greeter x)
(__register-instance-check!
(fn [class-name v]
(when (= class-name "Greeter")
(and (host/table? v) (= :greeter (host/ref-get v :jolt/type))))))
(.hello (Greeter. "ada")) ;=> "hi ada"
Greeter/VERSION ;=> "1.0"
(.hello (Greeter/make "bob")) ;=> "hi bob"
(instance? Greeter (Greeter. "x")) ;=> true
An instance-check predicate returns true/false to decide, or nil to defer
to the next registered check and the built-ins — so several libraries can
register checks without clobbering each other. This is the mechanism jolt's
HTTP client library uses to emulate java.net.URL and HttpURLConnection so
clj-http-lite runs unchanged.
Extending a built-in class instead (adding a method to core's String shim,
say) means editing the relevant host/chez/*.ss file and running make remint
— see building-and-deps.md.