irregex rejected two patterns the JVM accepts, which blocked library loads: - [\w-_] errored with bad char-set because a - after a shorthand class was read as a range start. Java reads it as a literal hyphen. Preprocess the pattern to escape such a dash. - (X+)* errored with duplicate repetition because sre-repeater? recurses through submatch, treating a quantified group like a dangling a**. Override it to a bare leading * / + check, matching the JVM (which only rejects the dangling case). Both in regex.ss (runtime). Unblocks cuerdas (was load-fail, now 292 passing) and aws-api config-test. Also documents the host/chez/java source-layering rule in host-interop.md. jolt-l8so
13 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).
Source layering: JVM-specific code lives in the java layer
Keep anything JVM-specific in host/chez/java/. The rest of the runtime stays
JVM-free, and the compiler in jolt-core/ is JVM-free by construction.
host/chez/java/holds the JVM model: thejava.*mirrors, the class tokens and class hierarchy,(class x)/(type x)/instance?, exception classes, the interop dispatch for.method/Class/static/(Class.). If a value or name only means something because the JVM has it, it belongs here.- The rest of
host/chez/is the host-neutral runtime — the value model (values.ss,collections.ss,seq.ss), reader, vars, multimethods, meta. It speaks jolt's own taxonomy (:string,:vector,:jolt/inst), never JVM class names. jolt-core/(the Clojure compiler +clojure.coreoverlay) emits and reasons in that taxonomy only. The JVM mapping happens after, in the java layer.
The worked example is type. The core layer (natives-meta.ss) computes the
keyword taxonomy and binds it as __type-tag — that's what print-method and the
reader dispatch on, with no JVM in scope. The java layer (java/host-class.ss)
then rebinds the public clojure.core/type to Clojure's (or (:type meta) (class x)), mapping :jolt/inst → java.util.Date and so on, right next to (class …). So the compiler keeps emitting :jolt/inst; the java layer remaps it.
When you add interop behaviour, prefer registering it through the generic hooks a
java-layer file already uses — register-class-arm! for (class x),
register-instance-check-arm! for instance?, register-eq-arm! for value
equality — rather than threading a JVM concept back into a host-neutral file. A
new java.* shim is a new file under host/chez/java/ loaded from rt.ss, not a
branch added to collections.ss or seq.ss.
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—currentTimeMillisnanoTimeexitgetPropertysetPropertyclearPropertygetPropertiesgetenvgc(a full Chez collection — clears weak references and fires their queues).java.lang.Thread— real OS threads over Chezfork-thread, sharing the one heap (a captured atom/var is shared):(Thread. thunk)+start/join/run/isAlive; plussleep(real),yield/interrupted/interrupt(no-ops),currentThread.java.util.concurrent.CountDownLatch—(CountDownLatch. n)+countDown/await/getCount, a real counting barrier (mutex + condition).java.lang.ref.SoftReference/WeakReference+ReferenceQueue— genuine GC reclamation: the referent is held through a Chez weak pair, so the collector reclaims it once unreachable (.getthen returns nil) and a guardian enqueues the reference on itsReferenceQueue(poll). Chez has no reference softer than weak, so aSoftReferenceclears on unreachability, not memory pressure — eager, but real eviction (core.cache's SoftCache).java.lang.Object—(Object.)as a fresh-identity sentinel;.toString.hashCode.equals.getClasswork on any value.java.lang.Class—forName(throws a catchableClassNotFoundExceptionfor a class jolt can't back, so(try (Class/forName "opt.Dep") (catch …))dependency probes work). There is no reflection, but a few common interfaces carry a modeled ancestry so(supers c)/(ancestors c)answer like the JVM — e.g.(ancestors (class f))for a function yieldsRunnableandCallable, the checkcore.memoizeuses to validate a memoizable argument.
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/java.util.concurrent.ConcurrentHashMap—putgetgetOrDefaultcontainsKeycontainsValuesizeisEmptyremoveclearputAllkeySetvaluesentrySet;clojure.core'sget/count/contains?also read them. (One shared heap, so the plain mutable map serves the concurrent one.)
I/O
java.io.File—(File. path)/(File. parent child). A File keeps the path as given ((.getPath (File. "rel"))is"rel",.isAbsolutefalse); a relative path resolves againstJOLT_PWDonly when the filesystem is touched. Methods:getPathgetNamegetParentgetParentFilegetAbsolutePathgetAbsoluteFilegetCanonicalPathgetCanonicalFiletoURItoURLexistsisDirectoryisFileisAbsoluteisHiddenlengthlastModifiedcanReadcanWritecanExecutelistlistFilesmkdirmkdirsdeletecreateNewFilerenameTocompareToequalshashCode. Statics:File/separatorFile/separatorCharFile/pathSeparatorFile/createTempFileFile/listRoots.- Byte streams —
FileInputStream/FileOutputStream(over a path/File,appendarg),ByteArrayInputStream/ByteArrayOutputStream(toByteArray/toString/size/reset),BufferedInputStream/BufferedOutputStream.read/read(byte[]),write(int)/write(byte[]),flush,close. Each is a Chez binary port underneath. - Char streams —
FileReader/InputStreamReader(read a byte stream as UTF-8),FileWriter/OutputStreamWriter,BufferedReader(readLine,lines) /BufferedWriter(newLine),StringReader/StringWriter/PushbackReader. clojure.java.io—fileas-filereaderwriterinput-streamoutput-streamcopy(byte-exact for byte sources)make-parentsdelete-fileresourceas-url.slurp/spit/line-seq/with-openwork over all of the above.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 —
ThrowableExceptionRuntimeExceptionIllegalArgumentExceptionIllegalStateExceptionIOExceptionNumberFormatExceptionArithmeticExceptionNullPointerExceptionClassCastExceptionIndexOutOfBoundsExceptionFileNotFoundExceptionUnsupportedOperationExceptionErrorAssertionErrorand the common network exceptions, each with the(E.)/(E. msg)/(E. msg cause)/(E. cause)constructors.trydispatches itscatchclauses by class in order, respecting the exception supertype hierarchy ((catch Exception e …)catches aRuntimeExceptionbut not anError); a thrown value matching no clause re-throws. An untyped host condition (e.g. from(/ 1 0)) is caught by aRuntimeException/Exception/Throwableclause.
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.