Chez Phase 1 (increment 3h): host-interop method-call emit

(.method target arg*) now analyzes to a :host-call IR node instead of
punting at analyze. The Chez back end lowers it to a jolt-host-call
dispatch for the methods the RT shims (.write -> port display,
.isDirectory -> file-directory?, .listFiles -> directory-list); any
other method stays out of subset (clean emit-time reject, so it can't
read as a compiled-but-broken corpus divergence). The Janet back end
punts ALL :host-call to the interpreter, same shape as letfn: compiles
on Chez, interprets on Janet, zero change to the main language.

Closes the io tier's print-method defmethods and file-seq: prelude emit
reach 348 -> 354/355 (50-io 20/20). The one remaining gap is the regex
literal in parse-uuid (needs a regex engine on Chez; deferred).

emit-test 122/122; Chez subset 672/672, 0 divergences; full gate green.
This commit is contained in:
Yogthos 2026-06-17 18:58:44 -04:00
parent 0f7d2753a8
commit b1cdfd1c9b
9 changed files with 96 additions and 13 deletions

View file

@ -283,6 +283,10 @@
(defn- stdlib-var? [n]
(and (= :var (get n :op)) (string/has-prefix? "clojure." (or (get n :ns) ""))))
# 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})
# 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.
(def- cmp1-ops {"<" true ">" true "<=" true ">=" true})
@ -365,6 +369,18 @@
:throw (string "(jolt-throw " (emit (get node :expr)) ")")
:try (emit-try node)
:quote (emit-quoted (get node :form))
# host interop (jolt-0kf5): (.method target arg*) -> (jolt-host-call "method"
# target arg*). Only the methods the RT dispatcher (rt.ss) actually shims are
# IN the subset; any other method is out of subset (a clean emit-time reject,
# like an unimplemented stdlib fn), so it doesn't masquerade as a compiled-but-
# broken divergence. The Janet back end punts ALL :host-call to the interpreter.
:host-call (let [m (get node :method)]
(unless (get supported-host-methods m)
(errorf "emit: unsupported host method `.%s` (no Chez shim yet)" m))
(let [target (emit (get node :target))
args (map emit (vv (get node :args)))]
(string "(jolt-host-call " (string/format "%j" m) " "
target (if (empty? args) "" (string " " (string/join args " "))) ")")))
:fn (emit-fn node)
# (def name) with no init (declare): reserve the var cell (declare-var!
# doesn't clobber an existing root) so a forward reference resolves.

View file

@ -39,6 +39,20 @@
jolt-kw-data data
jolt-kw-cause (if (null? more) jolt-nil (car more))))
;; --- host interop (jolt-0kf5) ------------------------------------------------
;; (.method target arg*) lowers to (jolt-host-call "method" target arg*). JVM
;; interop has no general Chez analog, but the few methods jolt-core's io tier
;; calls map onto Chez equivalents: a writer's .write is a port display; a File's
;; .isDirectory / .listFiles work over a path string (Chez has no File type, so
;; file-seq's File branch is unreachable here — these keep the forms honest). An
;; unsupported method raises rather than silently returning nil.
(define (jolt-host-call method target . args)
(cond
((string=? method "write") (display (car args) target) jolt-nil)
((string=? method "isDirectory") (if (file-directory? target) #t #f))
((string=? method "listFiles") (list->cseq (directory-list target)))
(else (error 'jolt-host-call (string-append "unsupported host method: ." method)))))
;; --- var cells: late-bound global roots (Clojure vars) -----------------------
;; A var is a mutable cell keyed by "ns/name". A `:def` sets the root; a `:var`
;; reference reads it at use time (late binding), so a forward/mutually-recursive

View file

@ -276,6 +276,23 @@
(uncompilable (str "var of non-var " (form-sym-name sym)))))
(uncompilable (str "special form " op))))
;; Host interop method call (jolt-0kf5). `(.method target arg*)` — a head that
;; starts with "." but not ".-" (field access stays punted). Analyzes to a
;; :host-call node; the Janet back end punts it at emit (no interop model -> the
;; interpreter runs it), the Chez back end lowers it to a jolt-host-call dispatch.
(defn- method-head? [nm]
(and (> (count nm) 1)
(= "." (subs nm 0 1))
(not (= "-" (subs nm 1 2)))))
(defn- analyze-host-call [ctx hname items env]
(when (< (count items) 2)
(throw (str "Malformed member expression, expecting (.method target ...): " hname)))
{:op :host-call
:method (subs hname 1)
:target (analyze ctx (nth items 1) env)
:args (mapv #(analyze ctx % env) (drop 2 items))})
(defn- analyze-symbol [ctx form env]
(let [nm (form-sym-name form) ns (form-sym-ns form)]
(cond
@ -311,6 +328,8 @@
(cond
(and hname (not shadowed) (contains? handled hname))
(analyze-special ctx hname items env)
(and hname (not shadowed) (method-head? hname))
(analyze-host-call ctx hname items env)
(and hname (not shadowed) (form-special? hname))
(uncompilable (str "special form " hname))
(and (form-sym? head) (not shadowed) (form-macro? ctx head))

View file

@ -101,6 +101,8 @@
(= op :fn) (assoc node :arities (mapv (fn [a] (assoc a :body (f (get a :body))))
(get node :arities)))
(= op :def) (assoc node :init (f (get node :init)))
(= op :host-call) (assoc node :target (f (get node :target))
:args (mapv f (get node :args)))
;; :catch-body / :finally are optional; recurse them only when PRESENT.
;; Assoc'ing them nil-when-absent would turn the node into a phm (jolt's
;; nil-valued-key representation) and force backend densification — so we

View file

@ -777,6 +777,10 @@
:map (emit-map ctx node)
:set (emit-set ctx node)
:quote ['quote (node :form)]
# host interop (.method target ...): the back end doesn't model interop —
# punt to the interpreter, exactly as the analyzer used to before producing
# a :host-call node (the Chez back end lowers it instead).
:host-call (error "jolt/uncompilable: host method call")
(error (string "backend: unhandled op " (node :op))))))
(defn emit-ir

View file

@ -72,16 +72,19 @@ catalogs the gaps; macros are skipped (analyze-time only, not a runtime value):
JOLT_CHEZ_PRELUDE=1 janet test/chez/core-prelude-probe.janet
Baseline after inc 3f (quoted literals): **342/355 non-macro core forms emit** to
Scheme (was 334 at inc 3e, 303 at inc 3d). A `:quote` node reconstructs the raw
reader form as RT constructor calls — symbol → `jolt-symbol`, list → `jolt-list`,
vector → `jolt-vector`, map/set → `jolt-hash-map`/`jolt-hash-set`, scalars via
`emit-const`. Prior: `:throw``jolt-throw` (Scheme `raise` of the raw jolt
value); `:try``guard` (catch, class dropped → catch-all) + `dynamic-wind`
(finally); `ex-info` native-op building the tagged jolt map, so the tier
ex-data/ex-message/ex-cause read it over `get` for free. Remaining 13 gaps: Java
host interop `.write`/`.isDirectory` (6, io tier), `letfn` (4), `declare`/
def-no-init (2), one edge (`parse-uuid`). The probe has a regression floor.
Baseline after inc 3h (host-interop method calls): **354/355 non-macro core forms
emit** to Scheme (was 348 at inc 3g, 342 at inc 3f). A `.method` call now analyzes
to a `:host-call` IR node; the Chez emitter lowers it to a `jolt-host-call`
dispatch for the methods the RT shims — `.write` → port `display`, `.isDirectory`
`file-directory?`, `.listFiles``directory-list` — closing the io tier's
print-method defmethods and `file-seq` (now 20/20). Any other method is out of
subset (a clean emit-time reject, so it can't masquerade as a compiled-but-broken
divergence); the Janet back end punts ALL `:host-call` to the interpreter. Prior
incs: `:quote` reconstructs the raw reader form as RT constructors; `:throw`
`jolt-throw`, `:try``guard` + `dynamic-wind`, `ex-info` native-op; `letfn`
`letrec*`; `declare`/def-no-init → a reserved var cell. Remaining 1 gap: the regex
literal in `parse-uuid` (needs a regex engine on Chez — see jolt issue). The probe
has a regression floor (354).
Prior, inc 3b (seq tier + dynamic IFn, jolt-5pso): 595/595 compiled, 0 divergences,
2060/2655 out of subset. The seq tier brought up a list/lazy-seq type with

View file

@ -98,7 +98,7 @@
# Regression floor (raise it as new IR ops / RT shims land, like the suite
# baseline). Fails if prelude emit reach drops below the recorded baseline.
(def reach-floor 348)
(def reach-floor 354)
(when (< compiled reach-floor)
(printf "REGRESSION: prelude emit reach %d < floor %d" compiled reach-floor)
(os/exit 1))

View file

@ -281,6 +281,25 @@
(ok (string "letfn/declare: " src) (and (= code 0) (= out want))
(string "chez=" out " janet=" want " | " err))))
# 3l) host interop method calls (inc 3h). (.method target arg*) analyzes to a
# :host-call IR node and lowers to a jolt-host-call dispatch. The Janet back end
# PUNTS these (no interop model -> interpreter); the Chez RT shims the methods
# jolt-core's io tier uses: .write -> display to a port, .isDirectory ->
# file-directory?, .listFiles -> directory-list. Interop has no portable oracle
# (the Janet host models it differently), so these are emit-shape checks plus one
# deterministic runtime probe (the root "/" is always a directory).
(each [label src needle]
[["emit .write -> jolt-host-call" "(fn [w x] (.write w x))" "jolt-host-call"]
["emit .write keeps method name" "(fn [w x] (.write w x))" "\"write\""]
["emit .isDirectory -> jolt-host-call" "(fn [f] (.isDirectory f))" "isDirectory"]
["emit .listFiles -> jolt-host-call" "(fn [f] (.listFiles f))" "listFiles"]]
(let [scm (protect (emit/emit (backend/analyze-form ctx (in (r/parse-next src) 0))))]
(ok label (and (scm 0) (string/find needle (scm 1))) (string/format "%p" scm))))
(let [[code out err] (d/run-on-chez ctx "(.isDirectory \"/\")")]
(ok "runtime .isDirectory \"/\" = true" (and (= code 0) (= out "true"))
(string "chez=" out " | " err)))
# 3h) prelude mode (inc 3d): emitting clojure.core ITSELF, a core->core ref must
# lower to a runtime var-deref instead of being rejected as "out of subset".
# `frequencies` is a core fn but not a native-op, so it exercises the switch.

View file

@ -92,7 +92,11 @@
# gains a letrec lowering.
# eval — compile-and-run entry (also loader stateful-head?)
# . / new / Foo. / — thin host-interop heads the back end doesn't model
# .method
# .method — analyzes to a :host-call IR node now (inc 3h), but the
# Janet back end punts it at emit (no interop model). The
# Chez back end DOES lower it (jolt-host-call). Same shape as
# letfn: compiles on Chez, interprets on Janet.
# .-field — field access stays punted at analyze (form-special?)
# gen-class, monitor-enter, monitor-exit — JVM-compat stubs
# Growing this list is a REGRESSION: a new punt means the compiler lost
# coverage. Shrinking it (e.g. letfn via letrec IR) is progress — move the
@ -102,7 +106,9 @@
"(set! *warn-on-reflection* true)"
"(letfn [(f [n] (g n)) (g [n] (f n))] (f 1))"
"(eval (quote (+ 1 2)))"
"(.method obj)"
# .method analyzes to :host-call now; a resolvable target ("x") makes it punt
# at EMIT (the interop reason), not at analyze (an unresolved target).
"(.toUpperCase \"x\")"
"(.-field obj)"
"(new Foo 1)"
"(Foo. 1)"