jolt/docs/host-interop.md
Yogthos 8a877662dc Regex: accept Java-compatible char-class dash and (X+)* quantifier
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
2026-06-26 17:35:08 -04:00

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: the java.* 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.core overlay) 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/instjava.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.Mathsqrt cbrt pow exp log log10 floor ceil round abs max min sin cos tan asin acos atan signum random; fields PI, E. (clojure.math mirrors these as functions.)
  • Long / IntegerparseLong/parseInt/valueOf (optional radix), MAX_VALUE, MIN_VALUE; (Integer. x).
  • Double / FloatparseDouble, valueOf, toString, isNaN, isInfinite, the *_VALUE/*_INFINITY/NaN fields; (Double. s).
  • BooleanparseBoolean, TRUE, FALSE.
  • CharacterisUpperCase isLowerCase isDigit isWhitespace (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.SystemcurrentTimeMillis nanoTime exit getProperty setProperty clearProperty getProperties getenv gc (a full Chez collection — clears weak references and fires their queues).
  • java.lang.Thread — real OS threads over Chez fork-thread, sharing the one heap (a captured atom/var is shared): (Thread. thunk) + start / join / run / isAlive; plus sleep (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 (.get then returns nil) and a guardian enqueues the reference on its ReferenceQueue (poll). Chez has no reference softer than weak, so a SoftReference clears 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 .getClass work on any value.
  • java.lang.ClassforName (throws a catchable ClassNotFoundException for 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 yields Runnable and Callable, the check core.memoize uses to validate a memoizable argument.

Strings and text

  • java.lang.String statics — valueOf, format (the clojure.core/format engine; String/format with a leading locale is accepted). Instance methods go through clojure.string / the native string ops.
  • StringBuilderappend toString length charAt setLength.
  • java.text.NumberFormatgetInstance getNumberInstance getIntegerInstance; .format, .setGroupingUsed, .setMinimum/MaximumFractionDigits.
  • java.util.StringTokenizerhasMoreTokens countTokens nextToken.
  • java.util.regex.Patterncompile (with Pattern/MULTILINE), quote; .split, .pattern. (#"…" literals and clojure.string regex fns are the usual entry points.)

Collections (mutable)

  • java.util.ArrayListadd get set size isEmpty remove clear contains toArray iterator.
  • java.util.HashMap / java.util.concurrent.ConcurrentHashMapput get getOrDefault containsKey containsValue size isEmpty remove clear putAll keySet values entrySet; clojure.core's get / 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", .isAbsolute false); a relative path resolves against JOLT_PWD only when the filesystem is touched. Methods: getPath getName getParent getParentFile getAbsolutePath getAbsoluteFile getCanonicalPath getCanonicalFile toURI toURL exists isDirectory isFile isAbsolute isHidden length lastModified canRead canWrite canExecute list listFiles mkdir mkdirs delete createNewFile renameTo compareTo equals hashCode. Statics: File/separator File/separatorChar File/pathSeparator File/createTempFile File/listRoots.
  • Byte streamsFileInputStream / FileOutputStream (over a path/File, append arg), 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 streamsFileReader / InputStreamReader (read a byte stream as UTF-8), FileWriter / OutputStreamWriter, BufferedReader (readLine, lines) / BufferedWriter (newLine), StringReader / StringWriter / PushbackReader.
  • clojure.java.iofile as-file reader writer input-stream output-stream copy (byte-exact for byte sources) make-parents delete-file resource as-url. slurp/spit/line-seq/with-open work over all of the above.
  • java.lang.ClassLoadergetSystemClassLoader, .getResource, .getResourceAsStream (resolved against the source roots).

Time and date

  • java.util.Date(Date.) / (Date. ms); getTime toInstant toLocalDate(Time) before after equals toString (RFC 3339).
  • java.timeInstant (now, ofEpochMilli, toEpochMilli, atZone), LocalDateTime, ZoneId, DateTimeFormatter (ofPattern, ISO_LOCAL_*, localized styles), FormatStyle.
  • java.text.SimpleDateFormat(SimpleDateFormat. pattern); parse format toPattern applyPattern (setTimeZone/setLenient accepted 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); toString toExternalForm getProtocol getPath getFile.
  • java.net.URI — full component accessors (getScheme getHost getPort getPath getQuery getFragment, raw variants, isAbsolute).
  • java.util.Base64getEncoder/getDecoder with encode, encodeToString, decode.
  • java.nio.charset.CharsetforName.
  • java.util.UUIDrandomUUID, fromString; (UUID. s).
  • ExceptionsThrowable Exception RuntimeException IllegalArgumentException IllegalStateException IOException NumberFormatException ArithmeticException NullPointerException ClassCastException IndexOutOfBoundsException FileNotFoundException UnsupportedOperationException Error AssertionError and the common network exceptions, each with the (E.) / (E. msg) / (E. msg cause) / (E. cause) constructors. try dispatches its catch clauses by class in order, respecting the exception supertype hierarchy ((catch Exception e …) catches a RuntimeException but not an Error); a thrown value matching no clause re-throws. An untyped host condition (e.g. from (/ 1 0)) is caught by a RuntimeException/Exception/Throwable clause.

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 tablejolt.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.