core: staged-bootstrap kernel tier; move second/peek/subvec/mapv/update to Clojure

First fractal turn of the multi-stage bootstrap (jolt-tzo). The Clojure part of
clojure.core is now loaded in ordered tiers under jolt-core/clojure/core/. The
kernel tier (00-kernel: second/peek/subvec/mapv/update) holds the structural fns
the self-hosted compiler itself uses; in compile mode it is bootstrap-compiled
into clojure.core BEFORE the analyzer is built, so the analyzer binds those names
to the Clojure definitions instead of a not-yet-defined forward ref. That removes
the circularity that previously forced these to stay in Janet — the five core-*
primitives and their init-core! entries are gone.

Mechanism: backend/bootstrap-load-source (generalized from compile-load) builds a
source string into a target ns via the bootstrap; rebuild-compiler! recompiles
the self-hosted compiler against the current core (the rail for future turns,
exercised by the new fixpoint test). api/load-core-overlay! walks the ordered
tiers, bootstrap-loading kernel tiers and self-hosting the rest.

Also fixes a latent evaluator bug surfaced by the move: a fn rebinds current-ns
to its defining ns while it runs and restores on normal return, but a fn that
THREW unwound past its own restore, leaking the ns. try now restores the
try-entry ns on the catch/finally path, so referred-symbol resolution survives a
caught error (this was breaking is/testing in the suite harness after any thrown
assertion). Net battery +3 (3913 -> 3916); conformance 218/218 in all three
modes; binary builds and runs the embedded tiers.
This commit is contained in:
Yogthos 2026-06-06 13:15:17 -04:00
parent a6f0f8a6fe
commit 28ef15ea4b
9 changed files with 233 additions and 117 deletions

View file

@ -1,31 +0,0 @@
;; The Clojure portion of clojure.core. Loaded into the clojure.core namespace at
;; init (api/init), AFTER the Janet primitives are interned by core/init-core!,
;; and compiled by the self-hosted pipeline (analyzer -> IR -> Janet back end).
;;
;; This is the Phase 4 kernel-shrink seam: fns expressible in plain Clojure on top
;; of the remaining Janet primitives move here from core.janet, one at a time,
;; each compiled by the prior stage. Anything here must depend only on core vars
;; already interned by init-core! (and on other overlay fns defined above it).
;;
;; Safe-to-move rule: a fn can move here only if it is (1) NOT in
;; compiler/core-renames (that map emits core-X Janet symbols directly), (2) has
;; no internal Janet callers of its core-X binding, and (3) is NOT used by the
;; self-hosted compiler itself (jolt-core/jolt/*.clj) — the compiler has to
;; compile this overlay, so anything it calls must already exist as a Janet
;; primitive. (That last rule is why `second`, used by analyzer.clj, stays in
;; Janet even though it has no Janet callers.)
(defn ffirst [coll] (first (first coll)))
(defn nfirst [coll] (next (first coll)))
(defn fnext [coll] (first (next coll)))
(defn nnext [coll] (next (next coll)))
;; Canonical Clojure defs: pure first/next/loop/recur, no Janet realize-for-iteration.
(defn last [s]
(if (next s) (recur (next s)) (first s)))
(defn butlast [s]
(loop [ret [] s s]
(if (next s)
(recur (conj ret (first s)) (next s))
(seq ret))))

View file

@ -0,0 +1,46 @@
;; clojure.core — kernel tier (stage just above the Janet seed).
;;
;; These are the structural fns the self-hosted compiler itself uses
;; (jolt.analyzer): second/peek/subvec/mapv/update. Because the compiler must be
;; able to compile the *rest* of clojure.core, anything it calls has to exist
;; before it is built. So this tier is loaded FIRST and, in compile mode, is
;; bootstrap-compiled directly into clojure.core (not routed through the
;; self-hosted pipeline, which would need these to already exist — the
;; circularity that previously forced `second` to stay in Janet). With this tier
;; in place the analyzer is built against the Clojure definitions and the Janet
;; primitives are gone.
;;
;; Constraint: depend only on core-renames primitives (first/next/nth/count/conj/
;; vec/map/apply/assoc/get/…, all hardwired to the Janet seed) and on each other.
(defn second [coll] (first (next coll)))
(defn peek [coll]
(cond
(nil? coll) nil
;; vectors (incl. jolt's eager seq results): last element; lists/seqs: first.
(vector? coll) (if (zero? (count coll)) nil (nth coll (dec (count coll))))
(seq? coll) (first coll)
:else (throw (str "peek not supported on: " coll))))
(defn subvec
([v start] (subvec v start (count v)))
([v start end]
(when (not (vector? v)) (throw (str "subvec requires a vector")))
;; Clojure coerces indices with (int ...): NaN -> 0, floats/ratios truncate
;; toward zero ((quot x 1)); non-numbers throw. Only then range-check.
(let [coerce (fn [x]
(cond
(not (number? x)) (throw (str "subvec index must be a number"))
(not= x x) 0
:else (quot x 1)))
s (coerce start)
e (coerce end)]
(when (or (< s 0) (< e s) (< (count v) e))
(throw (str "subvec index out of range: " s " " e)))
(loop [i s acc []]
(if (< i e) (recur (inc i) (conj acc (nth v i))) acc)))))
(defn mapv [f & colls] (vec (apply map f colls)))
(defn update [m k f & args] (assoc m k (apply f (get m k) args)))

View file

@ -0,0 +1,24 @@
;; clojure.core — seq tier. Pure-Clojure leaf sequence fns on top of the kernel
;; tier (00-kernel) and the Janet seed. Loaded after the kernel tier; in compile
;; mode these self-host through the now-built analyzer (interpreted otherwise).
;;
;; Migration rule for adding fns here: the fn must (1) NOT be in
;; compiler/core-renames (that map emits core-X Janet symbols directly), (2) have
;; no internal Janet callers of its core-X binding, and (3) NOT be used by the
;; self-hosted compiler (jolt-core/jolt/*.clj). Compiler-facing structural fns go
;; in the kernel tier (00-kernel) instead — see its header.
(defn ffirst [coll] (first (first coll)))
(defn nfirst [coll] (next (first coll)))
(defn fnext [coll] (first (next coll)))
(defn nnext [coll] (next (next coll)))
;; Canonical Clojure defs: pure first/next/loop/recur, no Janet realize-for-iteration.
(defn last [s]
(if (next s) (recur (next s)) (first s)))
(defn butlast [s]
(loop [ret [] s s]
(if (next s)
(recur (conj ret (first s)) (next s))
(seq ret))))

View file

@ -10,6 +10,7 @@
(use ./compiler) (use ./compiler)
(use ./loader) (use ./loader)
(use ./async) (use ./async)
(import ./backend :as backend)
(import ./stdlib_embed :as stdlib-embed) (import ./stdlib_embed :as stdlib-embed)
(import ./host_iface :as host) (import ./host_iface :as host)
@ -27,20 +28,38 @@
x)) x))
(defn- load-core-overlay! # Ordered clojure.core tiers (embedded jolt-core/clojure/core/NN-*.clj). Each tier
"Load the Clojure portion of clojure.core (embedded jolt-core/clojure/core.clj) # may reference only the Janet seed + earlier tiers. A :kernel tier holds the
into the clojure.core namespace, routed through eval-toplevel like any other # structural fns the self-hosted compiler itself uses (second/peek/subvec/mapv/
source (compiled when :compile?, interpreted otherwise)." # update); in compile mode it must be bootstrap-compiled into clojure.core BEFORE
[ctx] # the analyzer is built (the analyzer depends on it), so it bypasses the
(when-let [src (get stdlib-embed/sources "clojure.core")] # self-hosted pipeline. Non-kernel tiers route through eval-toplevel like any
(def saved (ctx-current-ns ctx)) # source (compiled when :compile?, interpreted otherwise — the analyzer, built
(ctx-set-current-ns ctx "clojure.core") # lazily on the first such form, sees the kernel tier already in place).
(def- core-tiers
[{:ns "clojure.core.00-kernel" :kernel true}
{:ns "clojure.core.10-seq" :kernel false}])
(defn- eval-overlay-source [ctx src]
(var s src) (var s src)
(while (> (length (string/trim s)) 0) (while (> (length (string/trim s)) 0)
(def [form rest] (parse-next s)) (def [form rest] (parse-next s))
(set s rest) (set s rest)
(when (not (nil? form)) (eval-toplevel ctx form))) (when (not (nil? form)) (eval-toplevel ctx form))))
(ctx-set-current-ns ctx saved)))
(defn- load-core-overlay!
"Load the Clojure portion of clojure.core in dependency-ordered tiers. See
core-tiers and jolt-core/clojure/core/."
[ctx]
(def compile? (get (ctx :env) :compile?))
(def saved (ctx-current-ns ctx))
(ctx-set-current-ns ctx "clojure.core")
(each tier core-tiers
(when-let [src (get stdlib-embed/sources (tier :ns))]
(if (and compile? (tier :kernel))
(backend/bootstrap-load-source ctx "clojure.core" src)
(eval-overlay-source ctx src))))
(ctx-set-current-ns ctx saved))
(defn init (defn init
"Create a new Jolt evaluation context. "Create a new Jolt evaluation context.

View file

@ -209,15 +209,16 @@
# --- pipeline wiring (the self-hosted compile path) --- # --- pipeline wiring (the self-hosted compile path) ---
# Compile-load a jolt-core namespace via the bootstrap so it runs as native # Bootstrap-compile a source string into target-ns: each form is compiled via the
# bytecode. The analyzer uses unqualified referred names (jolt.host form-* + the # bootstrap (native Janet) compiler and its defs interned in target-ns. This is
# IR ctors), so the bootstrap's plain :var path compiles it. Stateful forms (the # the stage-1 builder — it runs BEFORE the self-hosted analyzer exists, so it's
# ns/require) fall back to the interpreter. Source from the embedded stdlib map. # how both the compiler namespaces (jolt.ir/jolt.analyzer) and the clojure.core
(defn- compile-load [ctx ns-name] # kernel tier (the structural fns the analyzer itself calls) get built. The
(def src (get (get (ctx :env) :embedded-sources @{}) ns-name)) # analyzer uses unqualified referred names (jolt.host form-* + IR ctors), so the
(when src # bootstrap's plain :var path compiles it; stateful forms fall back to interp.
(defn bootstrap-load-source [ctx target-ns src]
(def saved (ctx-current-ns ctx)) (def saved (ctx-current-ns ctx))
(ctx-set-current-ns ctx ns-name) (ctx-set-current-ns ctx target-ns)
(var s src) (var s src)
(while (> (length (string/trim s)) 0) (while (> (length (string/trim s)) 0)
(def parsed (r/parse-next s)) (def parsed (r/parse-next s))
@ -229,12 +230,32 @@
# definition rather than killing the whole load. # definition rather than killing the whole load.
(def r (protect (comp/eval-compiled (comp/compile-ast f ctx) ctx))) (def r (protect (comp/eval-compiled (comp/compile-ast f ctx) ctx)))
(unless (r 0) (eval-form ctx @{} f)))) (unless (r 0) (eval-form ctx @{} f))))
(ctx-set-current-ns ctx saved))) (ctx-set-current-ns ctx saved))
# Compile-load an embedded jolt-core namespace by name (source from the stdlib map).
(defn- compile-load [ctx ns-name]
(def src (get (get (ctx :env) :embedded-sources @{}) ns-name))
(when src (bootstrap-load-source ctx ns-name src)))
# Build the self-hosted compiler (IR ctors + analyzer) via the bootstrap. The
# analyzer's references to clojure.core fns it uses (second/peek/subvec/mapv/
# update) resolve to whatever is interned in clojure.core at this point — so the
# kernel tier must already be loaded (see api/load-core-overlay!).
(defn- build-compiler! [ctx]
(compile-load ctx "jolt.ir")
(compile-load ctx "jolt.analyzer"))
(defn- ensure-analyzer [ctx] (defn- ensure-analyzer [ctx]
(when (= 0 (length ((ctx-find-ns ctx "jolt.analyzer") :mappings))) (when (= 0 (length ((ctx-find-ns ctx "jolt.analyzer") :mappings)))
(compile-load ctx "jolt.ir") (build-compiler! ctx)))
(compile-load ctx "jolt.analyzer")))
(defn rebuild-compiler!
"Recompile the self-hosted compiler (jolt.ir + jolt.analyzer) against the
CURRENT clojure.core. The fractal turn: once a core tier supplies Clojure
definitions the compiler itself uses, rebuilding makes the compiler run on
them. Idempotent; re-interns the compiler namespaces over the existing cells."
[ctx]
(build-compiler! ctx))
(defn analyze-form (defn analyze-form
"Run the portable Clojure analyzer (jolt.analyzer/analyze) on a reader form, "Run the portable Clojure analyzer (jolt.analyzer/analyze) on a reader form,

View file

@ -939,11 +939,11 @@
dropped (array/slice c (min n (length c)))] dropped (array/slice c (min n (length c)))]
(if (jvec? coll) (make-vec dropped) dropped)))))) (if (jvec? coll) (make-vec dropped) dropped))))))
(defn core-second [coll] (core-first (core-rest coll))) # ffirst/nfirst/fnext/nnext/last/butlast (seq tier) and second/peek/subvec/mapv/
# ffirst / nfirst / fnext / nnext / last / butlast now live in the Clojure overlay # update (kernel tier) now live in the Clojure clojure.core tiers under
# (jolt-core/clojure/core.clj). second stays here — the self-hosted compiler # jolt-core/clojure/core/. The kernel tier is bootstrap-compiled before the
# (analyzer.clj) calls it, so it must exist as a Janet primitive before the # self-hosted analyzer is built, so the structural fns the analyzer uses come
# overlay (which the compiler compiles) loads. # from Clojure, not Janet — see api/load-core-overlay! and core/00-kernel.clj.
(defn core-drop-last [a & rest] (defn core-drop-last [a & rest]
(let [n (if (= 0 (length rest)) 1 a) (let [n (if (= 0 (length rest)) 1 a)
@ -1277,17 +1277,9 @@
(indexed? m) (do (var i 0) (each x m (set acc (f acc i x)) (++ i)))) (indexed? m) (do (var i 0) (each x m (set acc (f acc i x)) (++ i))))
acc) acc)
# peek/pop are defined only on stacks (vectors -> last end, lists -> front); # pop is defined only on stacks (vectors -> last end, lists -> front); Clojure
# Clojure throws on sets/maps/seqs/strings/scalars. # throws on sets/maps/seqs/strings/scalars. (peek lives in the Clojure kernel
(defn core-peek [coll] # tier — core/00-kernel.clj.)
(cond
(nil? coll) nil
(plist? coll) (if (pl-empty? coll) nil (pl-first coll)) # list: first
(pvec? coll) (if (= 0 (pv-count coll)) nil (pv-nth coll (- (pv-count coll) 1))) # vector: last
(tuple? coll) (if (= 0 (length coll)) nil (in coll (- (length coll) 1))) # vector: last
(array? coll) (if (= 0 (length coll)) nil (in coll 0)) # list: first
(error (string "peek not supported on " (type coll)))))
(defn core-pop [coll] (defn core-pop [coll]
(cond (cond
(nil? coll) nil (nil? coll) nil
@ -1297,22 +1289,7 @@
(array? coll) (if (= 0 (length coll)) (error "Can't pop empty list") (array/slice coll 1)) (array? coll) (if (= 0 (length coll)) (error "Can't pop empty list") (array/slice coll 1))
(error (string "pop not supported on " (type coll))))) (error (string "pop not supported on " (type coll)))))
# Clojure coerces subvec indices with (int ...): floats truncate and NaN -> 0; # subvec lives in the Clojure kernel tier — core/00-kernel.clj.
# only non-numbers and out-of-range values throw.
(defn- subvec-idx [x]
(cond
(not (number? x)) (error "subvec index must be a number")
(not= x x) 0 # NaN -> 0
(math/trunc x)))
(defn core-subvec [v start &opt end]
(when (not (or (pvec? v) (tuple? v) (array? v)))
(error (string "subvec requires a vector, got " (type v))))
(let [a (vview v)
s (subvec-idx start)
e (if (nil? end) (length a) (subvec-idx end))]
(when (not (and (>= s 0) (<= s e) (<= e (length a))))
(error (string "subvec indices out of range: " s " " e " (length " (length a) ")")))
(make-vec (tuple/slice a s e))))
(defn core-trampoline [f & args] (defn core-trampoline [f & args]
(var result (apply f args)) (var result (apply f args))
@ -2758,11 +2735,8 @@
(let [nm (core-get ns :name)] (let [nm (core-get ns :name)]
(if nm {:jolt/type :symbol :ns nil :name (string nm)} nil))) (if nm {:jolt/type :symbol :ns nil :name (string nm)} nil)))
# update — works on both structs and tables # update lives in the Clojure kernel tier — core/00-kernel.clj. update-in stays
(defn core-update [m k f & args] # (it's recursive and has internal callers).
(def f (as-fn f))
(core-assoc m k (apply f (core-get m k) args)))
(defn- ks-rest [ks] (defn- ks-rest [ks]
(if (tuple? ks) (tuple/slice ks 1) (array/slice ks 1))) (if (tuple? ks) (tuple/slice ks 1) (array/slice ks 1)))
@ -3102,15 +3076,7 @@
(let [r @[]] (each x (realize-for-iteration coll) (when (truthy? (pred x)) (array/push r x))) (let [r @[]] (each x (realize-for-iteration coll) (when (truthy? (pred x)) (array/push r x)))
(make-vec r))) (make-vec r)))
(defn core-mapv [f & colls] # mapv lives in the Clojure kernel tier — core/00-kernel.clj.
(def f (as-fn f))
(let [r @[]]
(if (= 1 (length colls))
(each x (realize-for-iteration (colls 0)) (array/push r (f x)))
(let [cs (map realize-for-iteration colls)
n (min ;(map length cs))]
(var i 0) (while (< i n) (array/push r (apply f (map (fn [c] (in c i)) cs))) (++ i))))
(make-vec r)))
(defn- td-interpose [sep] (defn- td-interpose [sep]
(fn [rf] (fn [rf]
@ -3699,9 +3665,7 @@
"map-indexed" core-map-indexed "map-indexed" core-map-indexed
"cycle" core-cycle "cycle" core-cycle
"reduce-kv" core-reduce-kv "reduce-kv" core-reduce-kv
"peek" core-peek
"pop" core-pop "pop" core-pop
"subvec" core-subvec
"trampoline" core-trampoline "trampoline" core-trampoline
"format" core-format "format" core-format
"letfn" core-letfn "letfn" core-letfn
@ -3726,7 +3690,6 @@
"remove" core-remove "remove" core-remove
"reduce" core-reduce "reduce" core-reduce
"apply" core-apply "apply" core-apply
"second" core-second
"doall" core-doall "doall" core-doall
"dorun" core-dorun "dorun" core-dorun
"run!" core-run! "run!" core-run!
@ -3829,7 +3792,6 @@
"nthrest" core-nthrest "nthrest" core-nthrest
"nthnext" core-nthnext "nthnext" core-nthnext
"filterv" core-filterv "filterv" core-filterv
"mapv" core-mapv
"empty" core-empty "empty" core-empty
"not-empty" core-not-empty "not-empty" core-not-empty
"rseq" core-rseq "rseq" core-rseq
@ -4117,7 +4079,6 @@
"comment" core-comment "comment" core-comment
"resolve" core-resolve "resolve" core-resolve
"ns-name" core-ns-name "ns-name" core-ns-name
"update" core-update
"update-in" core-update-in "update-in" core-update-in
"assoc-in" core-assoc-in "assoc-in" core-assoc-in
"fnil" core-fnil "fnil" core-fnil

View file

@ -929,7 +929,14 @@
(error {:jolt/type :jolt/exception :value val})) (error {:jolt/type :jolt/exception :value val}))
"try" (let [body-form (in form 1) "try" (let [body-form (in form 1)
clauses (tuple/slice form 2) clauses (tuple/slice form 2)
n (length clauses)] n (length clauses)
# current-ns is dynamic state. The interpreter rebinds it to a
# fn's defining ns while that fn runs and restores it on normal
# return, but a fn that THROWS unwinds past its own restore — so
# the ns can leak. try is the unwind boundary: restore the ns that
# was current at try entry before running catch/finally, so caught
# code (and the harness's is/thrown?) sees the right namespace.
try-ns (ctx-current-ns ctx)]
(var catch-sym nil) (var catch-sym nil)
(var catch-body nil) (var catch-body nil)
(var finally-body nil) (var finally-body nil)
@ -952,6 +959,7 @@
(try (try
(eval-form ctx bindings body-form) (eval-form ctx bindings body-form)
([err] ([err]
(ctx-set-current-ns ctx try-ns)
(var new-bindings @{}) (var new-bindings @{})
(table/setproto new-bindings bindings) (table/setproto new-bindings bindings)
# bind the originally-thrown value (unwrap the :jolt/exception # bind the originally-thrown value (unwrap the :jolt/exception
@ -974,6 +982,7 @@
(run-finally finally-body) (run-finally finally-body)
result) result)
([err] ([err]
(ctx-set-current-ns ctx try-ns)
(run-finally finally-body) (run-finally finally-body)
(error err))) (error err)))
(eval-form ctx bindings body-form)))) (eval-form ctx bindings body-form))))

View file

@ -25,7 +25,11 @@
# running thread (Janet OS threads can't be interrupted), so `(deref (future # running thread (Janet OS threads can't be interrupted), so `(deref (future
# (sleep 1)))` re-raises the unresolved-`Thread/sleep` error — a documented # (sleep 1)))` re-raises the unresolved-`Thread/sleep` error — a documented
# platform gap, not a regression in any previously-working behavior. # platform gap, not a regression in any previously-working behavior.
(def baseline-pass 3913) # Raised 3913 -> 3916 with the staged-bootstrap kernel tier: the evaluator now
# restores current-ns when a fn throws across a try boundary (a leaked ns used to
# break referred-symbol resolution after a caught error), and subvec's index
# coercion (NaN/float/ratio) is faithful — net +3.
(def baseline-pass 3916)
# A file is "clean" when it ran with zero failures AND zero errors. # A file is "clean" when it ran with zero failures AND zero errors.
(def baseline-clean-files 45) (def baseline-clean-files 45)
# Per-file wall-clock budget (seconds). Normal files finish in well under 1s; # Per-file wall-clock budget (seconds). Normal files finish in well under 1s;

View file

@ -0,0 +1,63 @@
# Staged-bootstrap soundness (jolt-vcx, under epic jolt-tzo).
#
# The self-hosted compiler's structural deps — second/peek/subvec/mapv/update —
# now come from the Clojure kernel tier (jolt-core/clojure/core/00-kernel.clj),
# bootstrap-compiled into clojure.core BEFORE the analyzer is built. This pins
# the two properties that make that safe:
#
# 1. Compile mode: the analyzer (which itself calls second/peek/subvec/mapv)
# compiles analyzer-exercising forms correctly — the exact case that broke
# when `second` was a plain overlay fn: (first {:a 1}) / (key (first ...)).
# 2. Bootstrap FIXPOINT: rebuilding the compiler (rebuild-compiler!) against the
# now Clojure-defined core still yields a correct compiler. This is the
# soundness gate for every future fractal turn (S2 -> S3).
(use ../../src/jolt/api)
(import ../../src/jolt/backend :as backend)
(var failures 0)
# Each probe is a jolt boolean expression; compared with jolt's own `=`.
(def probes
["(= 2 (second [1 2 3]))"
"(= nil (second [1]))"
"(= 3 (peek [1 2 3]))"
"(= 1 (peek (list 1 2 3)))"
"(= nil (peek []))"
"(= [2 3] (subvec [1 2 3 4 5] 1 3))"
"(= [3 4 5] (subvec [1 2 3 4 5] 2))"
"(= [2 3 4] (mapv inc [1 2 3]))"
"(= [11 22 33] (mapv + [1 2 3] [10 20 30]))"
"(= {:a 2} (update {:a 1} :a inc))"
"(= {:a 1 :b 1} (update {:a 1} :b (fnil inc 0)))"
# Regression: these run the analyzer's own second/map-pair path in compile mode.
"(= [:a 1] (first {:a 1}))"
"(= :a (key (first {:a 1})))"
"(= 1 (val (first {:a 1})))"
"(= 3 (let [[a b] [1 2]] (+ a b)))"
"(= 3 (loop [i 0 acc 0] (if (< i 3) (recur (inc i) (+ acc i)) acc)))"])
(defn- run-probes [ctx label]
(each prog probes
(def got (protect (eval-string ctx prog)))
(unless (and (got 0) (= (got 1) true))
(++ failures)
(printf "FAIL [%s] %s => %s" label prog
(if (got 0) (string/format "%q" (got 1)) (string "ERR:" (got 1)))))))
# Interpret mode: kernel tier interpreted, no analyzer involved.
(run-probes (init {}) "interpret")
# Compile mode: kernel tier bootstrap-compiled, analyzer built against it.
(def cctx (init {:compile? true}))
(run-probes cctx "compile")
# Fixpoint: rebuild the compiler against the current (Clojure-defined) core and
# re-run. A correct compiler recompiled on the language it just defined stays
# correct.
(backend/rebuild-compiler! cctx)
(run-probes cctx "compile+rebuilt")
(if (pos? failures)
(do (printf "staged-bootstrap: %d failure(s)" failures) (os/exit 1))
(print "staged-bootstrap: all probes passed (interpret, compile, compile+rebuilt)"))