diff --git a/host/chez/emit.janet b/host/chez/emit.janet index f0e37ea..7c56fb3 100644 --- a/host/chez/emit.janet +++ b/host/chez/emit.janet @@ -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. diff --git a/host/chez/rt.ss b/host/chez/rt.ss index a54f889..d356961 100644 --- a/host/chez/rt.ss +++ b/host/chez/rt.ss @@ -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 diff --git a/jolt-core/jolt/analyzer.clj b/jolt-core/jolt/analyzer.clj index 37d87c1..7e1660d 100644 --- a/jolt-core/jolt/analyzer.clj +++ b/jolt-core/jolt/analyzer.clj @@ -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)) diff --git a/jolt-core/jolt/ir.clj b/jolt-core/jolt/ir.clj index 0ba5cb3..dfa8f46 100644 --- a/jolt-core/jolt/ir.clj +++ b/jolt-core/jolt/ir.clj @@ -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 diff --git a/src/jolt/backend.janet b/src/jolt/backend.janet index cf97407..e699845 100644 --- a/src/jolt/backend.janet +++ b/src/jolt/backend.janet @@ -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 diff --git a/test/chez/README.md b/test/chez/README.md index 7d77b89..8c5fb5e 100644 --- a/test/chez/README.md +++ b/test/chez/README.md @@ -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 diff --git a/test/chez/core-prelude-probe.janet b/test/chez/core-prelude-probe.janet index 914a98a..ae3f986 100644 --- a/test/chez/core-prelude-probe.janet +++ b/test/chez/core-prelude-probe.janet @@ -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)) diff --git a/test/chez/emit-test.janet b/test/chez/emit-test.janet index 0312788..4d87830 100644 --- a/test/chez/emit-test.janet +++ b/test/chez/emit-test.janet @@ -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. diff --git a/test/integration/fallback-zero-test.janet b/test/integration/fallback-zero-test.janet index 410aaa1..bdf40b4 100644 --- a/test/integration/fallback-zero-test.janet +++ b/test/integration/fallback-zero-test.janet @@ -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)"