Chez Phase 1 (increment 3e): throw/try/catch/finally + ex-info

Emit :throw as jolt-throw (Scheme raise of the raw jolt value, matching the
Janet compiled back end's (error v) — no envelope, so catch binds it directly).
Emit :try as guard (catch; the class is dropped in the IR, so it's catch-all)
plus dynamic-wind for finally. ex-info is a native-op building the tagged jolt
map {:jolt/type :jolt/ex-info :message :data :cause}, so the ex-data/ex-message/
ex-cause tier fns read it over jolt-get for free.

Prelude emit reach 303 -> 334/355 (:throw and :try gaps close). Subset probe
619 -> 632/632 compiled, 0 divergences (throw/try/ex-info pull 13 corpus cases
into the subset). emit-test 94/94 (added 11 throw/try/ex-info cases + uncaught
exits non-zero). Full gate green.
This commit is contained in:
Yogthos 2026-06-17 17:10:38 -04:00
parent 8f26433469
commit ffa122440a
5 changed files with 87 additions and 12 deletions

View file

@ -48,11 +48,12 @@ compile-time signal) and are counted "out of subset", not as divergences.
JOLT_CHEZ_CORPUS=1 janet test/chez/run-corpus-chez.janet
Baseline after inc 3c (multi-arity + variadic fns, jolt-gret): **619/619 compiled
cases pass**, 0 divergences; 2036/2655 out of subset (await clojure.core on Chez).
`emit-fn` now lowers multi-arity fns to a Scheme `case-lambda` and variadic fns to
a rest-arg lambda (the Scheme rest list is coerced to a jolt seq, nil when empty),
which is the gating lift for emitting the clojure.core tiers as a prelude.
Baseline after inc 3e (throw/try + ex-info): **632/632 compiled cases pass**, 0
divergences; 2023/2655 out of subset (await clojure.core on Chez). Inc 3c
(multi-arity + variadic fns) brought this to 619/619 — `emit-fn` lowers
multi-arity fns to a Scheme `case-lambda` and variadic fns to a rest-arg lambda
(the Scheme rest list is coerced to a jolt seq, nil when empty); inc 3e's
throw/try/ex-info support pulled 13 more corpus cases into the subset.
## Phase 1 — clojure.core prelude emission (inc 3d, jolt-ocvi)
The `-e`-capable jolt-chez path: emit the clojure.core tiers
@ -69,10 +70,14 @@ 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: **303/355 non-macro core forms emit** to Scheme. Remaining 52 gaps:
`:throw` (29, the big one — needs an exception model), `:quote` (8, needs RT
symbols/lists), `:try` (2), Java host interop `.write`/`.isDirectory` (6, io tier),
`letfn` (4), `declare`/def-no-init (2), one edge (`parse-uuid`). Each is a clean
Baseline after inc 3e (throw/try + ex-info): **334/355 non-macro core forms emit**
to Scheme (was 303 at inc 3d). `:throw` lowers to `jolt-throw` (Scheme `raise` of
the raw jolt value, like the Janet compiled back end); `:try` lowers to `guard`
(catch, class dropped → catch-all) + `dynamic-wind` (finally); `ex-info` is a
native-op building the tagged jolt map, so the ex-data/ex-message/ex-cause tier
fns read it over `get` for free. Remaining 21 gaps: `:quote` (8, needs RT
symbols/lists), Java host interop `.write`/`.isDirectory` (6, io tier), `letfn`
(4), `declare`/def-no-init (2), one edge (`parse-uuid`). Each is a clean
next-increment target. The probe has a regression floor (raise it as gaps close).
Prior, inc 3b (seq tier + dynamic IFn, jolt-5pso): 595/595 compiled, 0 divergences,

View file

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

View file

@ -207,6 +207,35 @@
(ok (string "arity: " src) (and (= code 0) (= out want))
(string "chez=" out " janet=" want " | " err))))
# 3i) throw / try / catch / finally + ex-info (inc 3e). Value parity vs the CLI
# oracle for caught throws; an uncaught throw must exit non-zero.
(each src [# jolt catch syntax is (catch Class binding body); the class is dropped
# in the IR (catch-all). catch binds the thrown value raw.
"(try (throw 42) (catch Exception e e))"
"(try (+ 1 (throw 7)) (catch Exception e (* e 10)))"
# finally runs and its value is discarded (try returns the body value)
"(try 5 (finally 99))"
"(try (throw 3) (catch Exception e (+ e 1)) (finally 99))"
# body value passes through when nothing throws
"(try (+ 2 3) (catch Exception e :nope))"
# ex-info builds a real jolt map: read message/data via get (native-op)
"(get (ex-info \"boom\" {:a 1}) :message)"
"(get (ex-info \"boom\" {:a 1}) :data)"
"(try (throw (ex-info \"boom\" {:a 1})) (catch Exception e (get e :message)))"
"(try (throw (ex-info \"boom\" {:a 7})) (catch Exception e (get (get e :data) :a)))"
# nested try: inner rethrows, outer catches
"(try (try (throw 1) (catch Exception e (throw (+ e 1)))) (catch Exception e (* e 100)))"]
(let [[code out err] (d/run-on-chez ctx src)
want (cli-oracle src)]
(ok (string "throw/try: " src) (and (= code 0) (= out want))
(string "chez=" out " janet=" want " | " err))))
# an uncaught throw aborts the program (non-zero exit) — matches the corpus
# `:throws` semantics (interpret/compile both bail).
(let [[code out err] (d/run-on-chez ctx "(throw (ex-info \"unhandled\" {}))")]
(ok "throw: uncaught exits non-zero" (not= code 0)
(string "code=" code " out=" out)))
# 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.