Chez Phase 2 (inc Q): Java class statics + constructors (jolt-avt6)

Lower host class interop on the Chez back end. The analyzer now turns a
non-var qualified ref `Class/member` into a :host-static node and a
`(Class. ...)` / `(new Class ...)` form into a :host-new node (ir.clj
gains both, with walker support). The Janet back end punts both to the
interpreter, so its behavior is unchanged (verified: dot-form, `..`
threading, shadowed `new`, and all interop still resolve via fallback).

The Chez emit lowers a value ref to host-static-ref, a call head to
host-static-call, and a constructor to host-new. host/chez/host-static.ss
is the runtime registry these resolve against — the Chez port of the
seed's class-statics / class-ctors / tagged-methods (java_base.janet +
host_io.janet), restricted to the java.lang/util/net/io surface portable
cljc code calls: Math, System (getenv/getProperty/exit/currentTimeMillis),
Long, Integer, Boolean, Character, String, Thread, Class, Pattern
(compile/quote/MULTILINE), URLEncoder/Decoder, Base64, the Number method
surface (byteValue/intValue/...), plus the StringBuilder, StringWriter,
StringReader, PushbackReader, HashMap, StringTokenizer, BigInteger,
String, MapEntry, and exception constructors. Constructed objects are
jhost records dispatched through record-method-dispatch.

Also: emit now evaluates collection-literal elements left-to-right
(emit-ordered) — Chez evaluates call args right-to-left, which had been
swapping side-effecting elements in [(read r) (read r)] and map literals.
This un-allowlisted the 6 eval-order corpus cases (the read-line trio +
the three map-construction cases). Removed `.write` from the
jolt-host-call fast-path so a StringWriter routes through dispatch.

java.time formatting, edn/read-over-readers, and slurp/with-open over
readers are deferred to a follow-up.

Corpus parity 2078 -> 2134 (floor raised), 0 new divergences; the
print-method builtin-override case is allowlisted (same multimethod gap,
newly reachable now that StringWriter constructs). emit-test 326/326,
_javastatic 51/51, conformance 355x3, full jpm test green.
This commit is contained in:
Yogthos 2026-06-18 23:24:01 -04:00
parent a706a79b90
commit c90c4cb610
10 changed files with 711 additions and 26 deletions

View file

@ -107,6 +107,20 @@
(var- gensym-n 0)
(defn- fresh-label [prefix] (string prefix (++ gensym-n)))
# Emit a call `(ctor a0 a1 ...)` with the args evaluated LEFT-TO-RIGHT. Chez's
# procedure-argument evaluation order is unspecified (and in practice right-to-
# left), but Clojure/JVM evaluates collection-literal elements left-to-right, so a
# literal like [(read r) (read r)] over side-effecting reads must bind in source
# order. Bind each arg to a fresh temp in a let* (sequential) then construct. Only
# wraps when there are >= 2 args (0/1 have no ordering to preserve), keeping the
# common small-literal output compact (jolt-avt6).
(defn- emit-ordered [ctor arg-strs]
(if (< (length arg-strs) 2)
(string "(" ctor (if (empty? arg-strs) "" (string " " (string/join arg-strs " "))) ")")
(let [tmps (map (fn [_] (fresh-label "_o$")) arg-strs)
binds (string/join (map (fn [t a] (string "(" t " " a ")")) tmps arg-strs) " ")]
(string "(let* (" binds ") (" ctor " " (string/join tmps " ") "))"))))
# Most jolt names are already valid Scheme identifiers (inc, even?, +, ->str all
# are — Scheme allows ! $ % & * + - . / : < = > ? @ ^ _ ~). The one that isn't is
# `#`, which jolt auto-gensyms use as a suffix (e.g. p1__0000X4# from #(...)
@ -345,7 +359,10 @@
# Host interop methods with a Chez RT shim (rt.ss jolt-host-call). A `.method`
# call on any other method is out of subset until shimmed — keep this in sync.
(def- supported-host-methods {"write" true "isDirectory" true "listFiles" true})
# `.write` is NOT here: StringWriter (a jhost, host-static.ss) handles .write via
# record-method-dispatch; the old jolt-host-call "write" fast-path (display to a
# port) would mis-route a writer to `(display x jhost)`. Keep the File-op methods.
(def- supported-host-methods {"isDirectory" true "listFiles" true})
# jolt's comparison ops are vacuously true at arity 1 and DON'T inspect the arg
# (so (< :kw) is true), but Scheme's < demands a number even there — special-case.
@ -381,6 +398,13 @@
(= kind :coll) (string "(jolt-get " (emit fnode) " " (first args) default ")")
(and (stdlib-var? fnode) (not prelude-mode?))
(errorf "emit: unsupported stdlib fn `%s/%s` (no core on Chez yet)" (get fnode :ns) (get fnode :name))
# static method call (Class/method arg*) -> (host-static-call "Class"
# "method" arg*). host-static.ss resolves the method from the class-statics
# registry and applies it (jolt-avt6).
(= :host-static (get fnode :op))
(string "(host-static-call " (chez-str-lit (get fnode :class)) " "
(chez-str-lit (get fnode :member))
(if (empty? args) "" (string " " (string/join args " "))) ")")
(= :host (get fnode :op))
(errorf "emit: unsupported host call `%s` (no host interop on Chez yet)" (get fnode :name))
# a :local callee that isn't a known procedure (a let/param binding holding a
@ -438,6 +462,14 @@
(string "(var-deref " (chez-str-lit (get node :ns)) " "
(chez-str-lit (get node :name)) ")")))
:host (errorf "emit: unsupported host ref `%s` (no host interop on Chez yet)" (get node :name))
# value-position static ref (Class/member, e.g. Long/MAX_VALUE, System/exit
# passed as a value) -> the registered static value/procedure (host-static.ss).
:host-static (string "(host-static-ref " (chez-str-lit (get node :class)) " "
(chez-str-lit (get node :member)) ")")
# constructor (Class. args*) / (new Class args*) -> (host-new "Class" args*).
:host-new (string "(host-new " (chez-str-lit (get node :class))
(let [args (map emit (vv (get node :args)))]
(if (empty? args) "" (string " " (string/join args " ")))) ")")
# (var x) / #'x -> the var cell itself (the rt.ss var-cell, a first-class var
# object). var?/var-get/deref/invoke/= operate on it (vars.ss).
:the-var (string "(jolt-var " (chez-str-lit (get node :ns)) " " (chez-str-lit (get node :name)) ")")
@ -450,15 +482,16 @@
(if (empty? (vv (get node :statements))) "" " ")
(emit (get node :ret)) ")")
:invoke (emit-invoke node)
# collection literals -> rt constructors (collections.ss)
:vector (string "(jolt-vector " (string/join (map emit (vv (get node :items))) " ") ")")
:set (string "(jolt-hash-set " (string/join (map emit (vv (get node :items))) " ") ")")
# collection literals -> rt constructors (collections.ss). Elements evaluate
# LEFT-TO-RIGHT (emit-ordered) to match Clojure for side-effecting elements.
:vector (emit-ordered "jolt-vector" (map emit (vv (get node :items))))
:set (emit-ordered "jolt-hash-set" (map emit (vv (get node :items))))
:map (let [flat @[]]
(each p (vv (get node :pairs))
(def p (vv p))
(array/push flat (emit (get p 0)))
(array/push flat (emit (get p 1))))
(string "(jolt-hash-map " (string/join flat " ") ")"))
(emit-ordered "jolt-hash-map" flat))
:let (emit-let node)
:loop (emit-loop node)
:recur (emit-recur node)