Delete the Janet host — Chez is the sole substrate

Remove the Janet seed (src/jolt/*.janet: reader, value layer, vars/ns, the
tree-walking interpreter, the Janet backend, the optimizing compiler), the
Janet->Scheme cross-compiler (host/chez/{driver,emit,jolt-chez}.janet),
bin/jolt-chez, the jpm build (project.janet) and the Janet test runner
(run-tests.janet), plus the entire Janet test suite. jolt now builds and runs
on Chez alone: bin/joltc off the checked-in seed, bootstrap.ss to rebuild it.

The portable Clojure stays: jolt-core/**, host/chez/**.ss, and the stdlib +
tooling under src/jolt/clojure + src/jolt/jolt (read by the seed build, no
Janet). The gate is 'make test' (self-host, corpus, unit, cli smoke, certify).
Drop the sci and clojure-test-suite submodules (used only by deleted Janet
integration tests); irregex stays.

Filesystem corpus/unit cases that probed project.janet now probe README.md.

jolt-cf1q.6
This commit is contained in:
Yogthos 2026-06-21 11:29:03 -04:00
parent 5c1fdfc336
commit 58d03d67be
221 changed files with 16 additions and 29925 deletions

View file

@ -1,648 +0,0 @@
# Phase 1 (jolt-cf1q.2) — live-analyzer -> Chez driver.
#
# Boots a real jolt ctx, runs the EXISTING Janet-hosted analyzer on actual
# Clojure source to produce host-neutral IR, feeds that IR to the Scheme emitter
# (emit.janet), and assembles a runnable Chez program. This is the Option-2
# backend swap end to end: same front end, Scheme back end, run on Chez.
#
# Analysis still happens on Janet here (the analyzer is portable Clojure but not
# yet bootstrapped onto Chez — that's Phase 2); EXECUTION happens on Chez. The
# point of this increment is to validate that the real IR the analyzer emits
# compiles to correct, fast Scheme.
(import ../../src/jolt/api :as api)
(import ../../src/jolt/backend :as backend)
(import ../../src/jolt/reader :as r)
(import ../../src/jolt/evaluator :as evlr)
(import ../../src/jolt/types_ctx :as tctx)
(import ../../src/jolt/types_ns :as tns)
(import ../../src/jolt/types_var :as tvar)
(import ./emit :as emit)
# Chez Phase 3 (jolt-duot): the IR->Scheme emitter is now the PORTABLE Clojure
# jolt.backend-scheme (jolt-core), not emit.janet. It's loaded into the ctx and
# called from here the same way the analyzer is. emit.janet stays only as the
# program-string wrapper (emit/program) until program assembly ports to Clojure
# with compile-from-source. This is the step that takes the emitter off Janet.
(defn- ensure-clj-emitter [ctx]
(def env (ctx :env))
(unless (get env :clj-emit-fn)
(def src (get (get env :embedded-sources @{}) "jolt.backend-scheme"))
(assert src "jolt.backend-scheme not embedded (check stdlib_embed)")
(backend/bootstrap-load-source ctx "jolt.backend-scheme" src)
(def ns (tctx/ctx-find-ns ctx "jolt.backend-scheme"))
(put env :clj-emit-fn (tvar/var-get (tns/ns-find ns "emit")))
(put env :clj-set-prelude-fn (tvar/var-get (tns/ns-find ns "set-prelude-mode!"))))
ctx)
# Emit IR -> Scheme via the Clojure emitter (returns a Janet string).
(defn- cemit [ctx ir] (string ((get (ctx :env) :clj-emit-fn) ir)))
(defn- cset-prelude! [ctx on] ((get (ctx :env) :clj-set-prelude-fn) on))
# Public: emit IR -> Scheme via the portable Clojure emitter (jolt.backend-scheme).
# The single seam tests use so emit.janet's emit fn is no longer exercised.
(defn scheme-emit [ctx ir] (ensure-clj-emitter ctx) (cemit ctx ir))
(defn chez-available?
"True when a `chez` binary is on PATH — lets the chez tests skip cleanly on
hosts without it (CI without Chez), like the clojure-test-suite skips when its
corpus dir is absent."
[]
(def r (protect (let [p (os/spawn ["chez" "--version"] :p {:out :pipe :err :pipe})]
(ev/read (p :out) 1024)
(ev/read (p :err) 1024)
(os/proc-wait p))))
(and (r 0) (zero? (r 1))))
(defn make-ctx []
"A compile-mode jolt ctx (the analyzer pipeline is only built under :compile?).
Late-bind unresolved symbols: the Chez back end has no interpreter to punt to,
so a forward reference to a runtime-interned var (defmulti/defmethod's setup
call) lowers to a var-deref instead of failing to compile (jolt-9ls5)."
(def ctx (api/init {:compile? true}))
(put (get ctx :env) :late-bind-unresolved? true)
(ensure-clj-emitter ctx)
ctx)
(defn- parse-all [src]
(def out @[])
(var s src)
(while (> (length (string/trim s)) 0)
(def parsed (r/parse-next s))
(set s (in parsed 1))
(def f (in parsed 0))
(unless (nil? f) (array/push out f)))
out)
(defn compile-program
"Compile a Clojure program string to a runnable Chez program. Every top-level
form is analyzed to real IR and emitted to Scheme; all but the LAST form are
treated as defs (also interned in the ctx so later forms resolve their vars),
and the last form is the expression whose value the program prints."
[ctx src]
(ensure-clj-emitter ctx)
(def forms (parse-all src))
(assert (> (length forms) 0) "compile-program: empty program")
(def n (length forms))
(def def-scm @[])
(for i 0 (- n 1)
(def f (in forms i))
# emit the def, then intern it (interpreted) so a later form's reference to
# this var resolves to a :var node rather than an unresolved symbol.
(array/push def-scm (cemit ctx (backend/analyze-form ctx f)))
(evlr/eval-form ctx @{} f))
(def final-scm (cemit ctx (backend/analyze-form ctx (in forms (- n 1)))))
(emit/program def-scm final-scm))
# Drain a pipe to EOF. A single (ev/read pipe N) can return BEFORE the child has
# flushed everything — a program with a stdout side effect (newline/print) flushes
# in two writes, and the first ev/read sometimes catches only the first chunk, so
# the trailing real value is lost (intermittent gate divergence). Loop until EOF.
(defn- drain [pipe]
(def b @"")
(var c (ev/read pipe 0x10000))
(while c (buffer/push b c) (set c (ev/read pipe 0x10000)))
(string b))
(defn run-on-chez
"Compile `src` and run it on Chez; returns [exit-code stdout stderr]."
[ctx src &opt scheme-out]
(def prog (compile-program ctx src))
(def path (or scheme-out "/tmp/chez-jolt-prog.ss"))
(spit path prog)
(def proc (os/spawn ["chez" "--script" path] :p {:out :pipe :err :pipe}))
(def out (drain (proc :out)))
(def err (drain (proc :err)))
(def code (os/proc-wait proc))
[code (string/trim out) (string/trim err)])
# --- clojure.core prelude assembly (jolt-9ziu) --------------------------------
# The -e-capable jolt-chez path: emit EVERY non-macro clojure.core form across
# the dependency-ordered tiers as a def-var! in prelude mode, concatenated into
# a Scheme prelude loaded before the user expression. var-deref then resolves any
# core fn at runtime from the prelude's own def-var! cells. Macros are skipped
# (analyze-time only — the Janet analyzer expands them before emit, so they have
# no runtime value). Each form is wrapped in a tolerant load guard so a form that
# fails to LOAD (currently only the Phase-2 multimethod defmulti/defmethod
# print-method forms) doesn't break the rest of the prelude; it logs to stderr
# and becomes a lazy gap rather than a hard prelude failure.
(def core-tier-files
["00-syntax" "00-kernel" "10-seq" "20-coll" "25-sorted" "30-macros" "40-lazy" "50-io"])
# stdlib namespaces (beyond clojure.core) emitted into the prelude as their own
# def-var! tier. Each is pure Clojure over clojure.core + host natives, so the
# same analyze->emit pipeline lowers it; an aliased ref resolves via var-deref at
# runtime once the alias is registered (the driver pre-evals requires). jolt-nfca.
(def stdlib-ns-files
[["clojure.string" "src/jolt/clojure/string.clj"]
["clojure.walk" "src/jolt/clojure/walk.clj"]
# clojure.template requires clojure.walk (apply-template over postwalk-replace)
# — must follow it so the alias resolves at emit time.
["clojure.template" "src/jolt/clojure/template.clj"]
# clojure.edn requires clojure.string; read-string/__read-tagged are the
# reader.ss seams. The reader-arity's drain-reader is Janet-coupled (janet/type)
# so it's a lazy gap on Chez — read-string/edn->value are the live path. jolt-r8ku.
["clojure.edn" "src/jolt/clojure/edn.clj"]
# clojure.set / clojure.pprint: pure Clojure over core. set = relational ops
# (union/intersection/difference/join/index/...); pprint = the minimal jolt
# shim (pprint -> prn + recognized dispatch vars, with-pprint-dispatch macro).
# jolt-j5vg, clojure.pprint Phase-2 parity.
["clojure.set" "src/jolt/clojure/set.clj"]
["clojure.pprint" "src/jolt/clojure/pprint.clj"]])
(defn- sym-name [x]
(when (and (struct? x) (= :symbol (get x :jolt/type))) (get x :name)))
(defn- macro-form? [f]
(and (indexed? f) (> (length f) 0)
(let [h (sym-name (in f 0))] (and h (or (= h "defmacro") (= h "definline"))))))
# Extract [name-string fn-form] from (defmacro NAME ...rest): the macro's expander
# as a bare (fn ...rest), docstring/attr-map stripped. Mirrors eval_special.janet/
# eval-defmacro's parsing — bare name (no metadata on the name in core/stdlib),
# optional docstring, optional attr-map, then a params vector + body (single arity)
# OR arity clauses. Uses the `fn` MACRO (not fn*) so a destructured macro arglist
# desugars before lowering, like api/macro-compile-hook.
#
# We emit the BARE fn (not (def NAME ...)) on purpose: analyzing a def would
# host-intern! NAME in the Janet build ctx as a non-macro nil-root stub, and that
# stub makes a later (require '[stdlib-ns]) skip loading the REAL macro — so the
# Janet-hosted analyzer (the parity oracle) would treat e.g. with-pprint-dispatch
# as a fn and return its unexpanded template. The caller wraps the emitted lambda
# in def-var! manually, so NAME is never interned and require still works (jolt-r9lm).
(defn- defmacro->fn [f]
(def name-sym (in f 1))
(def after-name (tuple/slice f 2))
(def a1 (if (and (> (length after-name) 0) (string? (first after-name)))
(tuple/slice after-name 1) after-name))
(def after-meta (if (and (> (length a1) 0) (struct? (first a1))
(not= :symbol (get (first a1) :jolt/type)))
(tuple/slice a1 1) a1))
(def fn-sym {:jolt/type :symbol :ns nil :name "fn"})
[(sym-name name-sym) (array fn-sym ;after-meta)])
# Cross-compile one top-level form to its guard-wrapped Scheme string, or nil if it
# doesn't emit (out of subset). A defmacro emits as (def-var! ns name <expander fn>)
# plus (mark-macro! ns name) so the on-Chez analyzer expands it (jolt-r9lm). The
# caller handles ns forms (alias registration only) before calling this.
(defn- emit-form-scheme [ctx ns-name f]
(defn- jts [x] (string/format "%j" x))
(if (macro-form? f)
(let [[nm fn-form] (defmacro->fn f)]
(when nm
(def res (protect (cemit ctx (backend/analyze-form ctx fn-form))))
(when (res 0)
(string "(guard (e (#t #f))\n (def-var! " (jts ns-name) " " (jts nm) "\n "
(res 1) ")\n (mark-macro! " (jts ns-name) " " (jts nm) "))"))))
(let [res (protect (cemit ctx (backend/analyze-form ctx f)))]
(when (res 0)
(string "(guard (e (#t #f))\n " (res 1) ")")))))
(defn- form-label [f]
(if (and (indexed? f) (> (length f) 1))
(let [h (or (sym-name (in f 0)) "?") n (sym-name (in f 1))] (if n (string h " " n) h))
"?"))
(defn- require-head? [f]
(and (indexed? f) (> (length f) 0)
(let [h (sym-name (in f 0))] (and h (or (= h "require") (= h "use"))))))
(defn- scan-eval-requires! [ctx form]
"Recursively eval any (require ...)/(use ...) sub-form against the ctx so the
alias registers + the aliased ns loads BEFORE the AOT analyzer resolves its
qualified refs — the whole user form is analyzed up front, before any require
would run at eval time (jolt-nfca). Failures are swallowed (the ref then stays
an emit-err, the prior behavior)."
(when (indexed? form)
(if (require-head? form)
(protect (api/eval-one ctx form))
(each sub form (scan-eval-requires! ctx sub)))))
(defn emit-core-prelude
"Assemble the clojure.core prelude as a Scheme string. `ctx` must be a
compile-mode ctx; its current ns is set to clojure.core for the duration.
Returns [scheme emitted total skipped-load-guards-unknown]; `scheme` is the
joined, guard-wrapped def-var! forms (no rt.ss load — add that at program
assembly via emit/program or program-with-prelude)."
[ctx &opt core-dir]
(default core-dir "jolt-core/clojure/core/")
(ensure-clj-emitter ctx)
(cset-prelude! ctx true)
(def prev-ns (tctx/ctx-current-ns ctx))
(tctx/ctx-set-current-ns ctx "clojure.core")
(def out @[])
(var total 0) (var emitted 0)
(defn- emit-ns-forms [ns-name src]
(tctx/create-ns ctx ns-name)
(tctx/ctx-set-current-ns ctx ns-name)
(each f (parse-all src)
# Register any aliases this ns depends on before analyzing its forms, so an
# aliased ref (e.g. clojure.template's walk/postwalk-replace) resolves at emit
# time instead of lowering to an "Unknown class walk" host-static. The ns
# form's :require is a keyword-headed clause that scan-eval-requires! (matching
# the `require`/`use` symbol heads) doesn't catch, so eval the ns form whole.
(def ns-form? (and (indexed? f) (> (length f) 0) (= "ns" (sym-name (in f 0)))))
(if ns-form? (protect (api/eval-one ctx f)) (scan-eval-requires! ctx f))
# Skip emitting ns forms: their only role here is alias registration, and a
# runtime ns-switch would leak into the prelude's trailing *ns* (the def-var!s
# already carry explicit ns names). Macros ARE emitted now (jolt-r9lm): each
# defmacro becomes a def of its expander fn + (mark-macro! ns name) so the
# on-Chez analyzer (inc6b) can expand it — previously skipped (the Janet
# analyzer expanded them at analyze time, before they reached the prelude).
# Tolerant load guard (inside emit-form-scheme): a form that fails to LOAD
# (the 8 Phase-2 multimethod print-method forms in 50-io) is swallowed so it
# doesn't break the rest of the prelude — it becomes a lazy gap.
(unless ns-form?
(++ total)
(def scm (emit-form-scheme ctx ns-name f))
(when scm (++ emitted) (array/push out scm)))))
(each tf core-tier-files
(emit-ns-forms "clojure.core" (slurp (string core-dir tf ".clj"))))
# stdlib namespaces beyond clojure.core that are pure Clojure over core/host
# natives — emitted as their own def-var! tier so an aliased ref (e.g. s/split
# after (require '[clojure.string :as s])) resolves at runtime (jolt-nfca).
(each [ns-name path] stdlib-ns-files
(emit-ns-forms ns-name (slurp path)))
(tctx/ctx-set-current-ns ctx prev-ns)
(cset-prelude! ctx false)
[(string/join out "\n") emitted total])
# --- analyzer/emitter cross-compile (jolt-hs9n, the zero-Janet spine) ---------
# Phase 3 inc6: cross-compile the PORTABLE compiler (jolt.ir + jolt.analyzer +
# jolt.backend-scheme) to Scheme def-var! forms so analyze->IR->emit runs ON CHEZ.
# Same emit pipeline as the core prelude, but for jolt-core/jolt/* namespaces
# rather than clojure.core: jolt.* refs lower to var-deref (the prelude-mode gate
# only rejects clojure.* refs), clojure.core refs resolve from the loaded prelude,
# and the jolt.host form-*/resolve-global/... refs resolve from host-contract.ss.
(defn- emit-ns-forms-list
"Cross-compile one namespace's source to a list of guard-wrapped def-var! Scheme
strings (prelude mode must already be ON). Registers the ns' requires/aliases in
ctx first so cross-ns refs resolve at emit time; skips ns + macro forms (macros
are analyze-time only, already expanded at their use sites)."
[ctx ns-name src]
(tctx/create-ns ctx ns-name)
(tctx/ctx-set-current-ns ctx ns-name)
(def out @[])
(each f (parse-all src)
(def ns-form? (and (indexed? f) (> (length f) 0) (= "ns" (sym-name (in f 0)))))
(if ns-form? (protect (api/eval-one ctx f)) (scan-eval-requires! ctx f))
# The compiler namespaces define no macros, but route through the shared helper
# anyway (a defmacro would emit as a def + mark-macro!, jolt-r9lm).
(unless ns-form?
(def scm (emit-form-scheme ctx ns-name f))
(when scm (array/push out scm))))
out)
(def compiler-ns-files
[["jolt.ir" "jolt-core/jolt/ir.clj"]
["jolt.analyzer" "jolt-core/jolt/analyzer.clj"]
["jolt.backend-scheme" "jolt-core/jolt/backend_scheme.clj"]])
(defn emit-compiler-image
"Cross-compile the analyzer pipeline (jolt.ir + jolt.analyzer +
jolt.backend-scheme) to a Scheme string of prelude-mode def-var! forms — the
analyze->IR->emit spine running ON CHEZ (jolt-hs9n). Load AFTER rt.ss +
host-contract.ss + the core prelude. Returns [scheme total]."
[ctx]
(ensure-clj-emitter ctx)
# ensure-analyzer is lazy; a trivial analyze builds jolt.ir/jolt.analyzer/
# jolt.passes in the Janet ctx so their vars resolve while we emit their source.
(protect (backend/analyze-form ctx (in (r/parse-next "nil") 0)))
(cset-prelude! ctx true)
(def prev-ns (tctx/ctx-current-ns ctx))
(def out @[])
(each [ns-name path] compiler-ns-files
(array/concat out (emit-ns-forms-list ctx ns-name (slurp path))))
(tctx/ctx-set-current-ns ctx prev-ns)
(cset-prelude! ctx false)
[(string/join out "\n") (length out)])
(defn ensure-compiler-image
"Build (once) and return the path to the cross-compiled compiler image — the
jolt.ir/jolt.analyzer/jolt.backend-scheme def-var! forms (jolt-hs9n). Cached on
disk keyed by the same fingerprint scheme as the prelude; pass an explicit path
to control caching from the test harness."
[ctx path]
(unless (os/stat path)
(def [img _] (emit-compiler-image ctx))
(spit path img))
path)
(defn program-zero-janet
"Assemble a fully self-hosted Chez program: rt.ss + the core prelude +
host-contract.ss + the cross-compiled compiler image + compile-eval.ss, then
compile AND eval `src` ON CHEZ (read->analyze->emit->eval, no Janet). The
zero-Janet spine (jolt-hs9n)."
[prelude-path image-path src ns]
(string
"(import (chezscheme))\n"
"(load \"host/chez/rt.ss\")\n"
"(set-chez-ns! \"clojure.core\")\n"
"(load " (string/format "%j" prelude-path) ")\n"
"(load \"host/chez/post-prelude.ss\")\n"
"(set-chez-ns! \"user\")\n"
"(load \"host/chez/host-contract.ss\")\n"
"(load " (string/format "%j" image-path) ")\n"
"(load \"host/chez/compile-eval.ss\")\n"
"(printf \"~a\\n\" (jolt-final-str (jolt-compile-eval "
(string/format "%j" src) " " (string/format "%j" ns) ")))\n"))
(defn eval-zero-janet
"Compile+run `src` through the ON-CHEZ analyzer/emitter (zero Janet). Needs a
prebuilt core prelude (`prelude-path`) and compiler image (`image-path`).
Returns [code stdout stderr]."
[prelude-path image-path src &opt ns scheme-out]
(default ns "user")
(def prog (program-zero-janet prelude-path image-path src ns))
(def path (or scheme-out (string "/tmp/jolt-zero-janet-" (os/getpid) ".ss")))
(spit path prog)
(def proc (os/spawn ["chez" "--script" path] :p {:out :pipe :err :pipe}))
(def out (drain (proc :out)))
(def err (drain (proc :err)))
(def code (os/proc-wait proc))
[code (string/trim out) (string/trim err)])
# --- self-hosting fixpoint (jolt-cf1q.4 inc8) ---------------------------------
# emit-compiler-image (above) builds stage1: the Janet analyzer/emitter
# cross-compiles the compiler sources to a Scheme def-var! image. To prove the
# ON-CHEZ compiler reproduces itself we recompile the SAME sources WITH the loaded
# image (emit-image.ss's jolt-emit-image runs analyze->emit on Chez): feeding it
# stage1 yields stage2, feeding it stage2 yields stage3, and stage2 == stage3
# byte-for-byte is the fixpoint (self-hosting-bootstrap-research §4).
(defn program-emit-image
"A Chez program that loads the zero-Janet runtime + the compiler `image-path`,
then re-emits the compiler image (or, with emit-fn \"jolt-emit-prelude\", the
clojure.core prelude) ON CHEZ and writes it to `out-path`. Running this with
image = stageN produces stage(N+1)."
[prelude-path image-path out-path &opt emit-fn]
(default emit-fn "jolt-emit-image")
(string
"(import (chezscheme))\n"
"(load \"host/chez/rt.ss\")\n"
"(set-chez-ns! \"clojure.core\")\n"
"(load " (string/format "%j" prelude-path) ")\n"
"(load \"host/chez/post-prelude.ss\")\n"
"(set-chez-ns! \"user\")\n"
"(load \"host/chez/host-contract.ss\")\n"
"(load " (string/format "%j" image-path) ")\n"
"(load \"host/chez/compile-eval.ss\")\n"
"(load \"host/chez/emit-image.ss\")\n"
"(let ((p (open-output-file " (string/format "%j" out-path) " 'replace)))\n"
" (put-string p (" emit-fn ")) (close-port p))\n"))
(defn emit-image-on-chez
"Re-emit the compiler image on Chez: load `image-path` (stageN) and write the
re-emitted image (stage N+1) to `out-path`. Each runs in a fresh chez process so
gensym/state start clean (essential for a byte-stable fixpoint). emit-fn selects
jolt-emit-image (the compiler) or jolt-emit-prelude (clojure.core). Returns
[code stderr]."
[prelude-path image-path out-path &opt emit-fn]
(def prog (program-emit-image prelude-path image-path out-path emit-fn))
(def path (string "/tmp/jolt-emit-image-" (os/getpid) "-" (hash out-path) ".ss"))
(spit path prog)
(def proc (os/spawn ["chez" "--script" path] :p {:out :pipe :err :pipe}))
(def out (drain (proc :out)))
(def err (drain (proc :err)))
(def code (os/proc-wait proc))
[code (string/trim (string out err))])
# --- pure-Chez self-build (jolt-9phg, inc9a) ----------------------------------
# host/chez/bootstrap.ss rebuilds the prelude + compiler image from source ON
# CHEZ given a seed (prelude, image) pair. run-bootstrap drives ONE bootstrap.ss
# pass (no Janet in the compile path — Janet only spawns chez). mint-chez-seed
# iterates it from the Janet seed to the joint fixpoint and writes the checked-in
# bootstrap seed under host/chez/seed/.
(defn run-bootstrap
"Run one pure-Chez bootstrap pass: load (seed-prelude, seed-image), rebuild the
prelude + image from source on Chez, write them to (out-prelude, out-image).
Returns [code stdout stderr]. The compilation is 100% Chez; Janet only spawns
the process."
[seed-prelude seed-image out-prelude out-image]
(def proc (os/spawn ["chez" "--script" "host/chez/bootstrap.ss"
seed-prelude seed-image out-prelude out-image]
:p {:out :pipe :err :pipe}))
(def out (drain (proc :out)))
(def err (drain (proc :err)))
(def code (os/proc-wait proc))
[code (string/trim out) (string/trim err)])
(defn mint-chez-seed*
"Mint the checked-in bootstrap seed. Takes the Janet-emitted starting pair
(janet-prelude + janet-image, e.g. from jolt-chez/ensure-prelude +
ensure-compiler-image) and iterates bootstrap.ss to the joint byte-fixpoint, then
writes the converged pair to seed-prelude/seed-image. Run once (and whenever the
seed sources change) to refresh the checked-in seed. Returns iteration count."
[janet-prelude janet-image seed-prelude seed-image &opt max-iter]
(default max-iter 8)
(defn b= [a b] (= (string (slurp a)) (string (slurp b))))
(def tmp (or (os/getenv "TMPDIR") "/tmp"))
(var cur-pre janet-prelude)
(var cur-img janet-image)
(var converged false)
(var iters 0)
(for i 0 max-iter
(def npre (string tmp "/mint-pre-" i ".ss"))
(def nimg (string tmp "/mint-img-" i ".ss"))
(def [code _ err] (run-bootstrap cur-pre cur-img npre nimg))
(unless (zero? code) (errorf "bootstrap pass %d failed: %s" i err))
(set iters (inc i))
# A pass is a fixpoint once its output equals its input AND the input is no
# longer the Janet seed (the Janet prelude/image differ only in gensym ids).
(when (and (not= cur-pre janet-prelude)
(b= cur-pre npre) (b= cur-img nimg))
(set converged true)
(set cur-pre npre) (set cur-img nimg)
(break))
(set cur-pre npre) (set cur-img nimg))
(unless converged (errorf "seed did not converge in %d iterations" max-iter))
(os/mkdir (string/slice seed-prelude 0 (last (string/find-all "/" seed-prelude))))
(spit seed-prelude (slurp cur-pre))
(spit seed-image (slurp cur-img))
iters)
# --- batched zero-Janet corpus runner (jolt-qjr0, inc7) -----------------------
# eval-zero-janet spawns a fresh chez per case, each reloading rt.ss + the prelude
# (~282KB) + the compiler image (~89KB) from source — ~0.5s of pure reload per
# case, the entire cost. This runs ALL cases in ONE chez process: load the runtime
# once, then loop. Each case is guarded (errors isolated) and the user namespace is
# reset between cases (var-table keys added by a case are removed, *ns* restored) so
# there is no state leakage vs the per-process path. ~10-30x faster.
(defn program-corpus-zero-janet
"A Chez program that loads the zero-Janet runtime once, then runs every case in
`cases-tsv` (label<TAB>src per line) through jolt-compile-eval, printing one
result line per case: PASS<TAB>label | DIVERGE<TAB>label<TAB>value |
CRASH<TAB>label<TAB>message."
[prelude-path image-path cases-tsv]
(string
"(import (chezscheme))\n"
"(load \"host/chez/rt.ss\")\n"
"(set-chez-ns! \"clojure.core\")\n"
"(load " (string/format "%j" prelude-path) ")\n"
"(load \"host/chez/post-prelude.ss\")\n"
"(set-chez-ns! \"user\")\n"
"(load \"host/chez/host-contract.ss\")\n"
"(load " (string/format "%j" image-path) ")\n"
"(load \"host/chez/compile-eval.ss\")\n"
# Snapshot mutable global state after setup so each case sees a clean world (as
# if it ran in its own process): (1) var-table keys a case ADDS (its defs) are
# removed; (2) a base cell whose ROOT a case mutated (e.g. in-ns rebinds
# clojure.core/*ns*) is restored; (3) the ns + type registries are pruned back to
# their base keys. Without this, *ns*/find-ns/all-ns/satisfies? leak across cases.
"(define zj-base (let ((h (make-hashtable string-hash string=?)))\n"
" (vector-for-each (lambda (k) (hashtable-set! h k #t)) (hashtable-keys var-table)) h))\n"
"(define zj-roots '())\n"
"(vector-for-each (lambda (k) (let ((c (hashtable-ref var-table k #f)))\n"
" (when c (set! zj-roots (cons (cons c (var-cell-root c)) zj-roots)))))\n"
" (hashtable-keys var-table))\n"
"(define (zj-snap ht) (let ((h (make-hashtable string-hash string=?)))\n"
" (vector-for-each (lambda (k) (hashtable-set! h k #t)) (hashtable-keys ht)) h))\n"
"(define (zj-prune! ht base) (vector-for-each\n"
" (lambda (k) (unless (hashtable-ref base k #f) (hashtable-delete! ht k))) (hashtable-keys ht)))\n"
"(define zj-ns-base (zj-snap ns-registry))\n"
"(define zj-type-base (zj-snap type-registry))\n"
# global-hierarchy is a core atom whose CONTENTS `derive` mutates (its var root
# stays the same atom object, so the root-restore above misses it). Reset its
# contents to a fresh hierarchy each case.
"(define zj-ghier (var-cell-lookup \"clojure.core\" \"global-hierarchy\"))\n"
"(define (zj-reset!)\n"
" (vector-for-each (lambda (k) (unless (hashtable-ref zj-base k #f) (hashtable-delete! var-table k)))\n"
" (hashtable-keys var-table))\n"
" (for-each (lambda (cr) (unless (eq? (var-cell-root (car cr)) (cdr cr))\n"
" (var-cell-root-set! (car cr) (cdr cr)))) zj-roots)\n"
" (zj-prune! ns-registry zj-ns-base)\n"
" (zj-prune! type-registry zj-type-base)\n"
" (hashtable-clear! ns-alias-table)\n"
" (hashtable-clear! ns-refer-table)\n"
" (when zj-ghier (jolt-invoke (var-deref \"clojure.core\" \"reset!\")\n"
" (var-cell-root zj-ghier) (jolt-invoke (var-deref \"clojure.core\" \"make-hierarchy\"))))\n"
" (set-chez-ns! \"user\"))\n"
"(define kw-message (keyword #f \"message\"))\n"
"(define (zj-err->str e)\n"
" (cond ((and (pmap? e) (string? (jolt-get e kw-message))) (jolt-get e kw-message))\n"
" ((condition? e) (call-with-string-output-port (lambda (p) (display-condition e p))))\n"
" ((string? e) e)\n"
" (else (call-with-string-output-port (lambda (p) (write e p))))))\n"
"(define (zj-clean s)\n" # strip tabs/newlines from a message so it stays one TSV line
" (list->string (map (lambda (c) (if (or (char=? c #\\tab) (char=? c #\\newline)) #\\space c))\n"
" (string->list s))))\n"
# cases are stored one-per-line with \\n / \\t / \\\\ escaped (a source may be
# multi-line — e.g. a ;comment\\n inside a map literal); unescape before eval.
"(define (zj-unescape s)\n"
" (let ((out (open-output-string)) (n (string-length s)))\n"
" (let loop ((i 0))\n"
" (if (>= i n) (get-output-string out)\n"
" (let ((c (string-ref s i)))\n"
" (if (and (char=? c #\\\\) (< (+ i 1) n))\n"
" (let ((d (string-ref s (+ i 1))))\n"
" (write-char (cond ((char=? d #\\n) #\\newline) ((char=? d #\\t) #\\tab) (else d)) out)\n"
" (loop (+ i 2)))\n"
" (begin (write-char c out) (loop (+ i 1)))))))))\n"
# ACTUAL is compiled+eval'd as its OWN top-level program (jolt-compile-eval
# unrolls a top-level do), so a macro defined earlier in the program is usable
# later (runtime defmacro) — matching certify.clj's eval-isolated. Then compare
# to EXPECTED with =. (Wrapping in (= E A) would nest ACTUAL's do; wrapping A in
# (eval (quote A)) would quote a map literal and lose its source eval-order.)
"(define (zj-run label e-esc a-esc)\n"
" (define esrc (zj-unescape e-esc))\n"
" (define asrc (zj-unescape a-esc))\n"
" (guard (e (#t (printf \"CRASH\\t~a\\t~a\\n\" label (zj-clean (zj-err->str e)))))\n"
" (let* ((av (jolt-compile-eval asrc \"user\")) (ev (jolt-compile-eval esrc \"user\")))\n"
" (if (jolt= ev av)\n"
" (printf \"PASS\\t~a\\n\" label)\n"
" (printf \"DIVERGE\\t~a\\t~a\\n\" label (zj-clean (jolt-final-str av))))))\n"
" (zj-reset!))\n"
"(define (zj-tab s from)\n"
" (let loop ((i from)) (cond ((>= i (string-length s)) #f)\n"
" ((char=? (string-ref s i) #\\tab) i) (else (loop (+ i 1))))))\n"
"(let ((p (open-input-file " (string/format "%j" cases-tsv) ")))\n"
" (let loop ()\n"
" (let ((line (get-line p)))\n"
" (unless (eof-object? line)\n"
" (let* ((t1 (zj-tab line 0)) (t2 (and t1 (zj-tab line (+ t1 1)))))\n"
" (when (and t1 t2)\n"
" (zj-run (substring line 0 t1) (substring line (+ t1 1) t2)\n"
" (substring line (+ t2 1) (string-length line)))))\n"
" (loop)))))\n"))
(defn eval-corpus-zero-janet
"Run all `cases` ([label src] pairs) through the ON-CHEZ analyzer in ONE chez
process. Returns a struct mapping label -> [:pass] | [:diverge value] |
[:crash message]. Vastly faster than per-case eval-zero-janet (single runtime
load); use eval-zero-janet to isolate a single case for debugging."
[prelude-path image-path cases &opt scheme-out cases-out]
(def tsv-path (or cases-out (string "/tmp/jolt-zj-cases-" (os/getpid) ".tsv")))
(def buf @"")
# escape so each case is one TSV line even if its source is multi-line; the
# runner's zj-unescape reverses it. Backslash first, then newline/tab.
(defn- tsv-esc [s]
(->> s (string/replace-all "\\" "\\\\") (string/replace-all "\n" "\\n")
(string/replace-all "\t" "\\t")))
(each [label e a] cases (buffer/push buf label "\t" (tsv-esc e) "\t" (tsv-esc a) "\n"))
(spit tsv-path buf)
(def prog (program-corpus-zero-janet prelude-path image-path tsv-path))
(def path (or scheme-out (string "/tmp/jolt-zj-runner-" (os/getpid) ".ss")))
(spit path prog)
(def proc (os/spawn ["chez" "--script" path] :p {:out :pipe :err :pipe}))
(def out (drain (proc :out)))
(def err (drain (proc :err)))
(def code (os/proc-wait proc))
(def res @{})
(each line (string/split "\n" (string/trim out))
(when (> (length line) 0)
(def parts (string/split "\t" line))
(def status (in parts 0))
(def label (get parts 1 ""))
(cond
(= status "PASS") (put res label [:pass])
(= status "DIVERGE") (put res label [:diverge (get parts 2 "")])
(= status "CRASH") (put res label [:crash (get parts 2 "")]))))
# If chez died mid-run (e.g. an uncatchable error), surface what we have + stderr.
{:results res :code code :stderr (string/trim err) :count (length res)})
(defn program-with-prelude
"Assemble a runnable Chez program that loads rt.ss, loads the assembled core
prelude from `prelude-path` (a file written once), then prints `final-scm`."
[prelude-path final-scm]
(string
"(import (chezscheme))\n"
"(load \"host/chez/rt.ss\")\n"
# the prelude's defmultis (print-method/print-dup) must land in clojure.core,
# not the default user ns (jolt-9ls5); set the multimethod current-ns around
# the prelude load, then restore it to user for the program form.
"(set-chez-ns! \"clojure.core\")\n"
"(load " (string/format "%j" prelude-path) ")\n"
# native-wins overrides for overlay predicates that read :jolt/type (char?,
# atom?) — must load AFTER the prelude's own def-var! to take effect.
"(load \"host/chez/post-prelude.ss\")\n"
"(set-chez-ns! \"user\")\n"
"(printf \"~a\\n\" (jolt-final-str " final-scm "))\n"))
(defn eval-e-with-prelude
"Run a single user expression `src` on Chez with the full clojure.core prelude
(loaded from `prelude-path`). Emits `src` in prelude mode so any core ref
resolves via var-deref. Returns [code stdout stderr], or [:emit-err msg \"\"]
if the user form itself can't be emitted."
[ctx src prelude-path &opt scheme-out]
(ensure-clj-emitter ctx)
(cset-prelude! ctx true)
(def form (in (r/parse-next src) 0))
(scan-eval-requires! ctx form)
(def res (protect (cemit ctx (backend/analyze-form ctx form))))
(cset-prelude! ctx false)
(if (not (res 0))
[:emit-err (string (res 1)) ""]
(let [prog (program-with-prelude prelude-path (res 1))
# PID-unique default so concurrent processes (a foreground -e while the
# parity gate runs) never read each other's half-written program file.
path (or scheme-out (string "/tmp/jolt-chez-e-" (os/getpid) ".ss"))]
(spit path prog)
(def proc (os/spawn ["chez" "--script" path] :p {:out :pipe :err :pipe}))
(def out (drain (proc :out)))
(def err (drain (proc :err)))
(def code (os/proc-wait proc))
[code (string/trim out) (string/trim err)])))

View file

@ -1,552 +0,0 @@
# Phase 1 — jolt IR -> Chez Scheme emitter (jolt-cf1q.2).
#
# The new back end: consumes the SAME host-neutral IR (jolt.ir, see
# jolt-core/jolt/ir.clj) the live analyzer produces and the Janet backend
# consumes, but emits Scheme source text instead of Janet. `host/compile` (Chez
# `eval`) turns that into a procedure. Covers the pure-functional + numeric
# subset (const/local/var/host/if/do/let/fn/invoke/def/loop/recur) — enough to
# run fib/mandelbrot-shaped code through the REAL analyzer.
#
# IR access mirrors the Janet backend: live IR fields are jolt VALUES — vectors
# are persistent (pv), and a nil-valued node densifies to a phm. `nn`/`vv` below
# normalize both into Janet structs/arrays, so the same code drives hand-built
# IR (the unit tests) and live analyzer output (the driver).
(import ../../src/jolt/pv :as pv)
(import ../../src/jolt/phm :as phm)
# Normalize a node (phm -> struct) and a vector field (pvec -> array view); both
# pass plain Janet values through untouched, so hand-built IR still works.
(defn- nn [n] (if (phm/phm? n) (phm/phm-to-struct n) n))
(defn- vv [x] (if (pv/pvec? x) (pv/pv->array x) x))
# Hot clojure.core primitives lowered to native Scheme, mirroring the Janet
# backend's native-ops (documented numbers-only relaxation). `=` is the
# exactness-aware jolt= from values.ss; inc/dec/not are rt shims; mod/rem/quot
# map to Scheme's (correct: Scheme has all three, unlike Janet which lacked quot).
(def- native-ops
{"+" "+" "-" "-" "*" "*" "/" "/"
"<" "<" ">" ">" "<=" "<=" ">=" ">="
"=" "jolt=" "inc" "jolt-inc" "dec" "jolt-dec" "not" "jolt-not"
"min" "min" "max" "max"
"mod" "modulo" "rem" "remainder" "quot" "quotient"
# persistent-collection leaf ops (jolt-wgbz) -> rt prims in collections.ss
"vector" "jolt-vector" "hash-map" "jolt-hash-map" "hash-set" "jolt-hash-set"
"conj" "jolt-conj" "get" "jolt-get" "nth" "jolt-nth" "count" "jolt-count"
"assoc" "jolt-assoc" "dissoc" "jolt-dissoc" "contains?" "jolt-contains?"
"empty?" "jolt-empty?" "peek" "jolt-peek" "pop" "jolt-pop"
# seq tier (jolt-5pso) -> rt prims in seq.ss
"first" "jolt-first" "rest" "jolt-rest" "next" "jolt-next" "seq" "jolt-seq"
"cons" "jolt-cons" "list" "jolt-list" "reverse" "jolt-reverse" "last" "jolt-last"
"map" "jolt-map" "filter" "jolt-filter" "remove" "jolt-remove"
"reduce" "jolt-reduce" "into" "jolt-into" "concat" "jolt-concat" "apply" "jolt-apply"
"range" "jolt-range" "take" "jolt-take" "drop" "jolt-drop"
"keys" "jolt-keys" "vals" "jolt-vals"
"even?" "jolt-even?" "odd?" "jolt-odd?" "pos?" "jolt-pos?" "neg?" "jolt-neg?"
"zero?" "jolt-zero?" "identity" "jolt-identity"
# exceptions (jolt-vcsl): ex-info builds the tagged map; ex-data/ex-message/
# ex-cause are pure-over-get Clojure tier fns (no native-op needed).
"ex-info" "jolt-ex-info"})
# Value-position resolution for a clojure.core ref passed AS A VALUE (to map /
# filter / reduce / apply). Each native-op already names a usable Scheme
# procedure; arithmetic is the exception — Scheme's +/-/*// return EXACT results
# for exact/zero-arg inputs, breaking the all-double model in higher-order use,
# so value-position arithmetic routes to the flonum-coercing rt wrappers.
(def- core-value-procs
(merge native-ops {"+" "jolt-add" "-" "jolt-sub" "*" "jolt-mul" "/" "jolt-div"}))
# Per-op arity gate: only lower when the Scheme prim and the jolt fn agree at
# this arity. Ops absent from the table are variadic (arith/compare/=, the
# collection constructors, conj/assoc/dissoc) and legal at any arity.
(def- op-arity
{"inc" |(= $ 1) "dec" |(= $ 1) "not" |(= $ 1)
"count" |(= $ 1) "empty?" |(= $ 1) "peek" |(= $ 1) "pop" |(= $ 1)
"mod" |(= $ 2) "rem" |(= $ 2) "quot" |(= $ 2) "contains?" |(= $ 2)
"get" |(or (= $ 2) (= $ 3)) "nth" |(or (= $ 2) (= $ 3))
"assoc" |(and (>= $ 3) (odd? $)) "dissoc" |(>= $ 1) "conj" |(>= $ 1)
# seq tier arities the shims support
"first" |(= $ 1) "rest" |(= $ 1) "next" |(= $ 1) "seq" |(= $ 1)
"reverse" |(= $ 1) "last" |(= $ 1) "keys" |(= $ 1) "vals" |(= $ 1)
"even?" |(= $ 1) "odd?" |(= $ 1) "pos?" |(= $ 1) "neg?" |(= $ 1)
"zero?" |(= $ 1) "identity" |(= $ 1)
"cons" |(= $ 2) "filter" |(= $ 2) "remove" |(= $ 2) "into" |(= $ 2)
"take" |(= $ 2) "drop" |(= $ 2) "map" |(>= $ 2) "apply" |(>= $ 2)
"reduce" |(or (= $ 2) (= $ 3)) "range" |(and (>= $ 0) (<= $ 3))
"ex-info" |(or (= $ 2) (= $ 3))})
# If fnode is a clojure.core (or host) ref to a native-op primitive, return the
# Scheme op string — only at an arity where the Scheme op and the jolt fn agree.
(defn- native-op [fnode nargs]
(def nm (case (get fnode :op)
:var (when (= "clojure.core" (get fnode :ns)) (get fnode :name))
:host (get fnode :name)
nil))
(def op (and nm (get native-ops nm)))
(def arity-ok (get op-arity nm))
(cond
(nil? op) nil
(and arity-ok (not (arity-ok nargs))) nil
op))
# PRELUDE MODE (inc 3d). The default (subset) mode rejects any clojure.core ref
# that isn't a native-op — a clean "out of subset" signal for user-facing `-e`.
# When emitting clojure.core ITSELF as a Scheme prelude, core fns reference each
# other constantly; those refs must lower to `var-deref` (resolved at runtime
# from the prelude's own def-var! forms) instead of being rejected. Host interop
# (:host) and unhandled IR ops still error in both modes — those are the real
# gaps that need a hand-written RT shim.
(var- prelude-mode? false)
(defn set-prelude-mode! [on] (set prelude-mode? on))
(var- recur-target nil)
# Munged local names known to hold a procedure (a named fn's self-recursion name).
# Calls to these stay DIRECT; any other :local callee routes through jolt-invoke
# (dynamic IFn dispatch) — keeps the fib self-call off the invoke fallback.
(def- known-procs @{})
(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 #(...)
# shorthand) — `#` starts a datum in Scheme, so replace it with `_`.
(defn- munge [name] (string/replace-all "#" "_" name))
(var emit nil) # forward declaration (mutual recursion with the helpers below)
# A Chez string literal (jolt-x0os). Janet's %j renders a non-ASCII char as raw
# UTF-8 bytes (\xC3\xA9) and a control char / DEL as \xHH with NO terminating
# semicolon — both forms Chez's reader rejects ("invalid character \ in string
# hex escape"). Emit a Chez string where every byte outside printable ASCII
# becomes a codepoint hex escape \x<cp>; (UTF-8 decoded for multibyte) and the
# named escapes (\n \t \r \" \\) match what both readers accept. For pure
# printable ASCII this is byte-identical to %j.
(defn- utf8-cp [s i]
# decode the UTF-8 sequence starting at byte i -> [codepoint byte-length]
(def b (in s i))
(cond
(< b 0x80) [b 1]
(= (band b 0xE0) 0xC0) [(bor (blshift (band b 0x1F) 6)
(band (in s (+ i 1)) 0x3F)) 2]
(= (band b 0xF0) 0xE0) [(bor (blshift (band b 0x0F) 12)
(blshift (band (in s (+ i 1)) 0x3F) 6)
(band (in s (+ i 2)) 0x3F)) 3]
(= (band b 0xF8) 0xF0) [(bor (blshift (band b 0x07) 18)
(blshift (band (in s (+ i 1)) 0x3F) 12)
(blshift (band (in s (+ i 2)) 0x3F) 6)
(band (in s (+ i 3)) 0x3F)) 4]
[b 1])) # malformed lead byte: pass through one byte
(defn- chez-str-lit [s]
(def out @"\"")
(def n (length s))
(var i 0)
(while (< i n)
(def b (in s i))
(cond
(= b 0x22) (do (buffer/push-string out "\\\"") (++ i))
(= b 0x5C) (do (buffer/push-string out "\\\\") (++ i))
(= b 0x0A) (do (buffer/push-string out "\\n") (++ i))
(= b 0x09) (do (buffer/push-string out "\\t") (++ i))
(= b 0x0D) (do (buffer/push-string out "\\r") (++ i))
(and (>= b 0x20) (< b 0x7F)) (do (buffer/push-byte out b) (++ i))
(< b 0x80) (do (buffer/push-string out (string/format "\\x%x;" b)) (++ i))
(let [[cp len] (utf8-cp s i)]
(buffer/push-string out (string/format "\\x%x;" cp))
(+= i len))))
(buffer/push-string out "\"")
(string out))
(defn- emit-const [v]
(cond
(nil? v) "jolt-nil"
(boolean? v) (if v "#t" "#f")
# jolt models every number as a double (no ratios/bignums; see reader.janet).
# Emit flonums so arithmetic matches the Janet host and Chez doesn't fall into
# exploding exact rationals (mandelbrot). Integer-valued -> append ".0".
# ##Inf/##-Inf/##NaN: Janet stringifies these as inf/-inf/nan, which are
# unbound symbols in Chez — emit Chez's flonum literals instead.
(number? v) (cond
(= v math/inf) "+inf.0"
(= v (- math/inf)) "-inf.0"
(not= v v) "+nan.0"
(let [s (string v)]
(if (or (string/find "." s) (string/find "e" s)) s (string s ".0"))))
(string? v) (chez-str-lit v) # quoted+escaped string literal
# keyword literal -> (keyword ns name); ns is everything before the first "/"
(keyword? v) (let [s (string v) idx (string/find "/" s)]
(if (and idx (> idx 0))
(string "(keyword " (chez-str-lit (string/slice s 0 idx)) " "
(chez-str-lit (string/slice s (inc idx))) ")")
(string "(keyword #f " (chez-str-lit s) ")")))
# jolt char value {:ch <codepoint> :jolt/type :jolt/char}
(and (struct? v) (= :jolt/char (get v :jolt/type)))
(string "(integer->char " (get v :ch) ")")
(errorf "emit-const: unsupported literal %p" v)))
# Quoted literals (jolt-u8j7). A :quote node's :form is the RAW reader form (a
# Janet value): scalars are Janet natives, a symbol is {:jolt/type :symbol …}, a
# list is an array, a vector a tuple, a map a struct/phm, a set a tagged struct.
# Reconstruct each as the matching Chez RT constructor — the runtime value of a
# quote is just that literal data (the interpreter returns the reader form
# verbatim; the Janet backend Janet-quotes it; here we rebuild it on the RT).
(var emit-quoted nil)
(defn- emit-quoted-map [m]
(def flat @[])
(eachp [k v] m (array/push flat (emit-quoted k)) (array/push flat (emit-quoted v)))
(string "(jolt-hash-map " (string/join flat " ") ")"))
(set emit-quoted (fn emit-quoted [form]
(cond
# scalars emit-const already lowers (nil/bool/number/string/keyword/char)
(or (nil? form) (boolean? form) (number? form) (string? form) (keyword? form))
(emit-const form)
(and (struct? form) (= :symbol (get form :jolt/type)))
(let [ns (get form :ns)
m (get form :meta)]
(if (and m (not (nil? m)) (> (length m) 0))
# carry reader metadata (^:foo bar) onto the quoted symbol so (meta 'x) sees it
(string "(jolt-symbol/meta " (if ns (chez-str-lit ns) "#f") " "
(chez-str-lit (get form :name)) " " (emit-quoted m) ")")
(string "(jolt-symbol " (if ns (chez-str-lit ns) "#f") " "
(chez-str-lit (get form :name)) ")")))
(and (struct? form) (= :jolt/char (get form :jolt/type))) (emit-const form)
(and (struct? form) (= :jolt/set (get form :jolt/type)))
(string "(jolt-hash-set " (string/join (map emit-quoted (get form :value)) " ") ")")
(array? form) (string "(jolt-list " (string/join (map emit-quoted form) " ") ")")
(tuple? form) (string "(jolt-vector " (string/join (map emit-quoted form) " ") ")")
(phm/phm? form) (emit-quoted-map (phm/phm-to-struct form))
(or (struct? form) (table? form)) (emit-quoted-map form)
(errorf "emit-quoted: unsupported quoted form %p" form))))
# A def's :meta is a jolt map value (Janet struct/table or phm). Non-empty?
# (a plain def carries {} — keep it on the lean def-var! path).
(defn- jmeta-nonempty? [m]
(cond
(nil? m) false
(phm/phm? m) (> (length (phm/phm-to-struct m)) 0)
(or (struct? m) (table? m)) (> (length m) 0)
false))
(defn- emit-binding [b]
(def b (vv b))
(string "(" (munge (get b 0)) " " (emit (get b 1)) ")"))
# letfn lowers to a :let flagged :letrec (mutually-recursive named local fns):
# Scheme `letrec*` binds them so each sees its siblings (and itself), which a
# sequential let* can't. A plain let uses let* (Clojure let binds sequentially).
(defn- emit-let [node]
(def kw (if (get node :letrec) "letrec*" "let*"))
(string "(" kw " (" (string/join (map emit-binding (vv (get node :bindings))) " ") ") "
(emit (get node :body)) ")"))
(defn- emit-loop [node]
(def label (fresh-label "loop"))
(def pairs (map vv (vv (get node :bindings))))
(def names (map |(munge (get $ 0)) pairs))
# inits are evaluated in the OUTER scope (recur-target unchanged) and, like
# Clojure loop/let, SEQUENTIALLY — a later init sees earlier bindings. Scheme's
# named `let` binds in parallel, so wrap a sequential let* around the loop.
(def inits (map |(emit (get $ 1)) pairs))
(def seq-bs (string/join (map (fn [n i] (string "(" n " " i ")")) names inits) " "))
(def rebinds (string/join (map (fn [n] (string "(" n " " n ")")) names) " "))
(def prev recur-target)
(set recur-target label)
(def body (emit (get node :body)))
(set recur-target prev)
(string "(let* (" seq-bs ") (let " label " (" rebinds ") " body "))"))
(defn- emit-recur [node]
(unless recur-target (error "emit: recur outside a loop/fn target"))
(string "(" recur-target " " (string/join (map emit (vv (get node :args))) " ") ")"))
# One arity -> a Scheme lambda param-list + a named-let-wrapped body. The named
# let lets fn-level `recur` rebind this arity's params. A variadic arity takes a
# Scheme rest arg (proper list) and the let binding coerces it to a jolt seq
# (nil when empty — Clojure's rest semantics; list->cseq already does this); recur
# carries the rest seq directly, and the named let's init only runs on first
# entry, so the coercion isn't re-applied on a recur.
# try/catch/finally (jolt-vcsl). throw raises the jolt value RAW (jolt-throw =
# Scheme `raise`), mirroring the Janet COMPILED backend (which does `(error v)`,
# no :jolt/exception envelope) — so catch binds the value directly, no unwrap.
# catch lowers to `guard` with an `else` clause (catch-all: the IR drops the
# class), finally to `dynamic-wind`'s after-thunk (runs on success, catch, and
# escape — Clojure finally semantics). Both keys are optional on the node.
(defn- emit-try [node]
(def core
(if-let [cs (get node :catch-sym)]
(string "(guard (" (munge cs) " (else " (emit (get node :catch-body)) ")) "
(emit (get node :body)) ")")
(emit (get node :body))))
(if-let [fin (get node :finally)]
(string "(dynamic-wind (lambda () #f) (lambda () " core ") (lambda () " (emit fin) "))")
core))
(defn- emit-arity-clause [a]
(def params (map munge (vv (get a :params))))
(def restp (when-let [r (get a :rest)] (munge r)))
(def label (fresh-label "fnrec"))
(def prev recur-target)
(set recur-target label)
(def body (emit (get a :body)))
(set recur-target prev)
(def paramlist
(cond
# only a rest param: Scheme formals are the bare symbol, not `( . xs)`
(and restp (empty? params)) restp
restp (string "(" (string/join params " ") " . " restp ")")
(string "(" (string/join params " ") ")")))
(def binds
(if restp
[;(map (fn [p] (string "(" p " " p ")")) params)
(string "(" restp " (list->cseq " restp "))")]
(map (fn [p] (string "(" p " " p ")")) params)))
[paramlist (string "(let " label " (" (string/join binds " ") ") " body ")")])
(defn- emit-fn [node]
(def arities (map nn (vv (get node :arities))))
# a named fn binds its own name as a known-procedure local across ALL arities,
# so self-calls (to any arity) emit directly rather than via jolt-invoke; the
# case-lambda value dispatches on argument count.
(def self (when-let [nm (get node :name)] (munge nm)))
(def had-self (and self (get known-procs self)))
(when self (put known-procs self true))
# Restore known-procs even when a body is uncompilable: a throw mid-emit must
# not leak this fn's name into the module global, or a LATER case binding the
# same name to a keyword/coll would emit a direct call to a non-procedure
# (runtime crash). The corpus probe shares one emit state across all cases, so
# this leak is order-dependent and otherwise invisible in single-case tests.
(def clauses
(try (map emit-arity-clause arities)
([err fib]
(unless had-self (when self (put known-procs self nil)))
(propagate err fib))))
(unless had-self (when self (put known-procs self nil)))
(def lambda
(if (= 1 (length clauses))
(let [[pl body] (first clauses)] (string "(lambda " pl " " body ")"))
(string "(case-lambda "
(string/join (map (fn [c] (string "(" (get c 0) " " (get c 1) ")")) clauses) " ")
")")))
# A named fn (defn / (fn self [..])) references itself by name — the analyzer
# binds that name as a :local in the body. letrec makes the name visible to the
# lambda so self-calls resolve (recur stays a separate self-call to the arity).
(if-let [nm (get node :name)]
(let [m (munge nm)] (string "(letrec ((" m " " lambda ")) " m ")"))
lambda))
# The Clojure stdlib (clojure.core, clojure.math, clojure.string, …) and host
# interop (Math/sqrt etc.) have no implementation on Chez yet (Phase 2+). A
# reference to one — except a clojure.core call lowered to a native op — is
# genuinely uncompilable here. Reject it at emit time (a clean "out of subset"
# signal) rather than emitting a var-deref that resolves to nil and fails
# confusingly at runtime.
(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.
# `.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.
(def- cmp1-ops {"<" true ">" true "<=" true ">=" true})
# IFn dispatch for a LITERAL callee (Clojure's "value as fn"): a keyword looks
# itself up in its arg ((:k m) = (get m :k)); a map/set/vector literal looks up
# its arg ((m :k) = (get m :k)). This static lowering avoids the jolt-invoke
# dispatch overhead; the dynamic case (a local holding a keyword/coll/fn) routes
# through jolt-invoke in the emit-invoke fallback below.
(defn- ifn-kind [fnode]
(case (get fnode :op)
:const (when (keyword? (get fnode :val)) :keyword)
:map :coll :set :coll :vector :coll
nil))
(defn- emit-invoke [node]
(def fnode (nn (get node :fn)))
(def args (map emit (vv (get node :args))))
(def nop (native-op fnode (length args)))
(def kind (ifn-kind fnode))
(def default (if (> (length args) 1) (string " " (in args 1)) ""))
(cond
# zero-arg + / * : Scheme's identity is the EXACT 0 / 1, but jolt models every
# number as a double, so emit the flonum identity to keep (= 0 (+)) true.
(and nop (empty? args) (= nop "+")) "0.0"
(and nop (empty? args) (= nop "*")) "1.0"
(and nop (= 1 (length args)) (get cmp1-ops nop)) (string "(begin " (first args) " #t)")
nop (string "(" nop " " (string/join args " ") ")")
# (:k coll [default]) -> (jolt-get coll :k [default])
(= kind :keyword) (string "(jolt-get " (first args) " " (emit fnode) default ")")
# (coll k [default]) -> (jolt-get coll k [default])
(= 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
# keyword/coll/fn) -> dynamic IFn dispatch. Excludes the named-fn self-call.
(and (= :local (get fnode :op)) (not (get known-procs (munge (get fnode :name)))))
(string "(jolt-invoke " (emit fnode) " " (string/join args " ") ")")
# a late-bound :var call head can hold a plain procedure OR a non-applicable
# value the RT dispatches (a multimethod record, a keyword/coll IFn) — route it
# through jolt-invoke so all of those work. Transparent for a procedure
# (jolt-invoke just applies it); the hot self-recursive call is a :local
# known-proc above, so it stays a direct call.
(= :var (get fnode :op))
(string "(jolt-invoke " (emit fnode) " " (string/join args " ") ")")
# a computed callee (an :invoke / :if / :do expression) can yield ANY IFn —
# a procedure, but also a coll/keyword/multimethod (e.g. ((sorted-map …) k)).
# Route through jolt-invoke: transparent for a procedure, correct for the rest.
(string "(jolt-invoke " (emit fnode) " " (string/join args " ") ")")))
# Native-op Scheme procedures that return a genuine Scheme boolean (#t/#f), so an
# :if test built from them needs no jolt-truthy? wrapper (jolt-nkcb). Comparisons
# and `not` are #t/#f; the numeric/collection predicates bottom out in fx=?/>/etc.
(def- bool-returning-ops
{"<" true "<=" true ">" true ">=" true "jolt=" true "jolt-not" true
"jolt-even?" true "jolt-odd?" true "jolt-pos?" true "jolt-neg?" true
"jolt-zero?" true "jolt-empty?" true "jolt-contains?" true})
# Does this IR node emit to an expression that yields a Scheme boolean? Used to
# drop the redundant jolt-truthy? on an :if test — sound because jolt-truthy? of
# #t/#f is the identity. Conservative: only a boolean const or an :invoke that
# lowers to a bool-returning native op (any other shape keeps the wrapper).
(defn- returns-scheme-bool? [node]
(def node (nn node))
(cond
(and (= :const (get node :op)) (boolean? (get node :val))) true
(= :invoke (get node :op))
(let [nop (native-op (nn (get node :fn)) (length (vv (get node :args))))]
(truthy? (and nop (get bool-returning-ops nop))))
false))
(set emit (fn emit [node]
(def node (nn node))
(case (get node :op)
:const (emit-const (get node :val))
:local (munge (get node :name))
# late-bound var: read the cell's current root at use time. A value-position
# ref to a clojure.core fn the RT provides (e.g. passing `inc`/`even?`/`:k` to
# (map inc xs)) lowers to the RT procedure — native-ops names a real Scheme
# procedure for each. Any OTHER stdlib var (clojure.string, an unimplemented
# core fn) has no impl on Chez yet, so it's out of subset.
:var (let [core-proc (and (= "clojure.core" (get node :ns)) (get core-value-procs (get node :name)))]
(cond
core-proc core-proc
(and (stdlib-var? node) (not prelude-mode?))
(errorf "emit: unsupported stdlib ref `%s/%s` (no core on Chez yet)" (get node :ns) (get node :name))
(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)) ")")
:if (let [test (get node :test)
t (if (returns-scheme-bool? test) (emit test)
(string "(jolt-truthy? " (emit test) ")"))]
(string "(if " t " " (emit (get node :then)) " " (emit (get node :else)) ")"))
:do (string "(begin "
(string/join (map emit (vv (get node :statements))) " ")
(if (empty? (vv (get node :statements))) "" " ")
(emit (get node :ret)) ")")
:invoke (emit-invoke node)
# 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))))
(emit-ordered "jolt-hash-map" flat))
:let (emit-let node)
:loop (emit-loop node)
:recur (emit-recur node)
:throw (string "(jolt-throw " (emit (get node :expr)) ")")
:try (emit-try node)
:quote (emit-quoted (get node :form))
# regex literal #"…" -> a jolt-regex value (regex.ss compiles the source via
# the vendored irregex). chez-str-lit quotes+escapes the source; a backslash
# in the pattern becomes \\ in the Scheme string literal -> the 1-char
# backslash irregex expects (same escaping emit-const uses for strings).
:regex (string "(jolt-regex " (chez-str-lit (get node :source)) ")")
# #inst / #uuid literals -> a runtime inst / uuid value (inst-time.ss /
# natives-misc.ss). The source string round-trips through chez-str-lit.
:inst (string "(jolt-inst-from-string " (chez-str-lit (get node :source)) ")")
:uuid (string "(jolt-uuid-from-string " (chez-str-lit (get node :source)) ")")
# 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)
target (emit (get node :target))
args (map emit (vv (get node :args)))]
(if (get supported-host-methods m)
(string "(jolt-host-call " (chez-str-lit m) " "
target (if (empty? args) "" (string " " (string/join args " "))) ")")
# a non-shimmed method: dispatch at runtime by the target's type
# — a record/reify protocol method (jolt-jgoc). On a non-record
# host value this errors (was an emit-fail before, so no new
# divergence), but it lets (.protoMethod record …) compile.
(string "(record-method-dispatch " target " " (chez-str-lit m)
" (jolt-vector" (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.
# A def with non-empty reader metadata (^:private / ^Type tag / docstring ->
# {:doc}) lowers to def-var-with-meta! so (meta (var x)) sees it (jolt-zikh).
:def (cond
(get node :no-init)
(string "(declare-var! " (chez-str-lit (get node :ns)) " "
(chez-str-lit (get node :name)) ")")
(jmeta-nonempty? (get node :meta))
(string "(def-var-with-meta! " (chez-str-lit (get node :ns)) " "
(chez-str-lit (get node :name)) " " (emit (get node :init)) " "
(emit-quoted (get node :meta)) ")")
(string "(def-var! " (chez-str-lit (get node :ns)) " "
(chez-str-lit (get node :name)) " " (emit (get node :init)) ")"))
(errorf "emit: unhandled op %p" (get node :op)))))
# Wrap emitted top-level forms into a runnable Chez program: load the RT, then
# the def forms, then print `final` (an emitted Scheme expr string) via jolt's
# number/value printing.
(defn program [forms-scheme final]
(string
"(import (chezscheme))\n"
"(load \"host/chez/rt.ss\")\n"
(string/join forms-scheme "\n") "\n"
"(printf \"~a\\n\" (jolt-final-str " final "))\n"))

View file

@ -1,62 +0,0 @@
# -e-capable jolt-chez (jolt-9ziu): the Option-2 back end as a runnable CLI.
#
# Analysis runs on Janet (the portable analyzer); EXECUTION runs on Chez with the
# full clojure.core assembled as a Scheme prelude (driver/emit-core-prelude). The
# prelude is assembled once and cached on disk keyed by a fingerprint of the core
# sources + the Chez RT/emitter, so repeated invocations (e.g. the run-corpus.janet
# gate, one subprocess per case) reuse it.
#
# Usage (the run-corpus.janet boundary): jolt-chez -e "EXPR"
# Run from the repo root (the prelude loads host/chez/rt.ss by relative path).
(import ../../src/jolt/api :as api)
(import ./driver :as d)
(defn fingerprint []
# Hash the inputs that shape the prelude: the core tiers + the emitter + the
# Chez RT shims. Any change invalidates the cached prelude.
(def parts @[])
(each tf d/core-tier-files
(array/push parts (slurp (string "jolt-core/clojure/core/" tf ".clj"))))
(each f ["jolt-core/jolt/backend_scheme.clj" "src/jolt/host_iface.janet"
"host/chez/emit.janet" "host/chez/driver.janet" "host/chez/rt.ss"
"host/chez/values.ss" "host/chez/collections.ss" "host/chez/seq.ss"
"host/chez/atoms.ss" "host/chez/predicates.ss" "host/chez/regex.ss"
"host/chez/ns.ss" "host/chez/post-prelude.ss" "host/chez/natives-meta.ss"
"host/chez/natives-str.ss" "host/chez/records.ss"
"host/chez/host-class.ss" "host/chez/io.ss"
"host/chez/inst-time.ss" "host/chez/reader.ss" "host/chez/math.ss"
"host/chez/syntax-quote.ss"
"host/chez/host-static.ss" "host/chez/dot-forms.ss"
"src/jolt/clojure/string.clj" "src/jolt/clojure/walk.clj"
"src/jolt/clojure/template.clj" "src/jolt/clojure/edn.clj"
"src/jolt/clojure/set.clj" "src/jolt/clojure/pprint.clj"]
(array/push parts (slurp f)))
(string/slice (string (hash (string/join parts))) 0))
(defn ensure-prelude [ctx]
(def dir (or (os/getenv "JOLT_IMAGE_CACHE_DIR") (os/getenv "TMPDIR") "/tmp"))
(def path (string dir "/jolt-chez-prelude-" (fingerprint) ".ss"))
(unless (os/stat path)
(def [scm _ _] (d/emit-core-prelude ctx))
(spit path scm))
path)
(defn main [& argv]
# argv: [script "-e" EXPR]
(def args (drop 1 argv))
(unless (and (= (length args) 2) (= (first args) "-e"))
(eprint "usage: jolt-chez -e EXPR")
(os/exit 2))
(def src (in args 1))
(def ctx (api/init-cached {:compile? true}))
# late-bind unresolved symbols (no interpreter to punt to) so defmulti/defmethod
# forward references lower to a var-deref (jolt-9ls5), matching d/make-ctx.
(put (get ctx :env) :late-bind-unresolved? true)
(def prelude-path (ensure-prelude ctx))
(def [code out err] (d/eval-e-with-prelude ctx src prelude-path))
(when (= code :emit-err)
(eprint "jolt-chez: cannot compile: " out)
(os/exit 1))
(unless (= "" out) (print out))
(unless (= "" err) (eprint err))
(os/exit code))