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

6
.gitmodules vendored
View file

@ -1,9 +1,3 @@
[submodule "vendor/sci"]
path = vendor/sci
url = https://github.com/borkdude/sci.git
[submodule "vendor/clojure-test-suite"]
path = vendor/clojure-test-suite
url = https://github.com/jank-lang/clojure-test-suite.git
[submodule "vendor/irregex"] [submodule "vendor/irregex"]
path = vendor/irregex path = vendor/irregex
url = https://github.com/ashinn/irregex.git url = https://github.com/ashinn/irregex.git

View file

@ -4,16 +4,20 @@
# build step. `make test` is the full gate. `make remint` rebuilds the seed after a # build step. `make test` is the full gate. `make remint` rebuilds the seed after a
# source change. # source change.
.PHONY: test corpus unit smoke selfhost certify remint .PHONY: test values corpus unit smoke selfhost certify remint
# Full gate. Each step exits non-zero on failure, failing the target. # Full gate. Each step exits non-zero on failure, failing the target.
test: selfhost corpus unit smoke certify test: selfhost values corpus unit smoke certify
@echo "OK: all gates passed" @echo "OK: all gates passed"
# Self-host fixpoint: bootstrap.ss rebuild == checked-in seed. # Self-host fixpoint: bootstrap.ss rebuild == checked-in seed.
selfhost: selfhost:
@sh host/chez/selfcheck.sh @sh host/chez/selfcheck.sh
# Value-model unit tests (nil/truthiness/collections on Chez).
values:
@chez --script test/chez/values-test.ss
# Corpus conformance vs JVM-sourced expecteds (allowlist + floor). # Corpus conformance vs JVM-sourced expecteds (allowlist + floor).
corpus: corpus:
@chez --script host/chez/run-corpus.ss @chez --script host/chez/run-corpus.ss

View file

@ -1,28 +0,0 @@
# Dump the Janet that jolt's backend emits for the mandelbrot hot fns, so we can
# A/B it against bench/mandelbrot-hand.janet and localize the ~1.43x jolt-over-
# hand-Janet gap measured in the foundational-runtime spike (jolt-5vsp).
(import ../src/jolt/api :as api)
(import ../src/jolt/backend :as backend)
(import ../src/jolt/reader :as reader)
(def ctx (api/init-cached {:compile? true}))
(put (ctx :env) :direct-linking? true)
(put (ctx :env) :inline? true)
(api/eval-string ctx "(ns mandelbrot)")
(def count-point-src
"(defn count-point [cr ci cap] (loop [i 0 zr 0.0 zi 0.0] (if (or (>= i cap) (> (+ (* zr zr) (* zi zi)) 4.0)) i (recur (inc i) (+ (- (* zr zr) (* zi zi)) cr) (+ (* 2.0 (* zr zi)) ci)))))")
(def run-src
"(defn run [n] (let [cap 200 nd (* 1.0 n)] (loop [y 0 acc 0] (if (< y n) (let [ci (- (/ (* 2.0 y) nd) 1.0) row (loop [x 0 a 0] (if (< x n) (let [cr (- (/ (* 2.0 x) nd) 1.5)] (recur (inc x) (+ a (count-point cr ci cap)))) a))] (recur (inc y) (+ acc row))) acc))))")
(api/eval-string ctx count-point-src)
(api/eval-string ctx run-src)
(defn emit [src]
(string/format "%p" (backend/emit-ir ctx (backend/analyze-form ctx (reader/parse-string src)))))
(print "===== count-point emitted Janet =====")
(print (emit count-point-src))
(print "\n===== run emitted Janet =====")
(print (emit run-src))

View file

@ -1,49 +0,0 @@
# mandelbrot — hand-written Janet that MIRRORS jolt's loop-lowering: every loop/
# recur becomes a self-recursive local closure stored in a var and called once
# per iteration (see bench/dump-mandelbrot-emit.janet). If this lands at jolt's
# ~219ms (vs the while-loop mandelbrot-hand.janet's ~153ms), the ~1.43x jolt-
# over-hand-Janet gap is the recursive-closure loop lowering, not anything else.
# janet bench/mandelbrot-hand-rec.janet 200
(defn count-point [cr ci cap]
(var loopfn nil)
(set loopfn (fn [i zr zi]
(if (let [t (>= i cap)] (if t t (> (+ (* zr zr) (* zi zi)) 4)))
i
(loopfn (+ i 1)
(+ (- (* zr zr) (* zi zi)) cr)
(+ (* 2 (* zr zi)) ci)))))
(loopfn 0 0 0))
(defn run [n]
(def cap 200)
(def nd (* 1 n))
(var yloop nil)
(set yloop (fn [y acc]
(if (< y n)
(let [ci (- (/ (* 2 y) nd) 1)
row (do
(var xloop nil)
(set xloop (fn [x a]
(if (< x n)
(let [cr (- (/ (* 2 x) nd) 1.5)]
(xloop (+ x 1) (+ a (count-point cr ci cap))))
a)))
(xloop 0 0))]
(yloop (+ y 1) (+ acc row)))
acc)))
(yloop 0 0))
(defn main [& args]
(def n (if (> (length args) 1) (scan-number (get args 1)) 1000))
(repeat 2 (run (div n 2)))
(def times @[])
(var last-r 0)
(repeat 3
(def t0 (os/clock))
(def r (run n))
(array/push times (* 1000.0 (- (os/clock) t0)))
(set last-r r))
(printf "mandelbrot n %d result %d" n last-r)
(print "runs: " (string/join (map |(string (/ (math/round (* $ 10.0)) 10.0)) times) " "))
(printf "mean: %.1f ms" (/ (sum times) 3)))

View file

@ -1,53 +0,0 @@
# mandelbrot — hand-written idiomatic Janet, same nested loop as bench/mandelbrot.clj.
# This is the "optimal Janet" leg of the foundational-runtime spike (jolt-5vsp):
# comparing jolt-emitted Janet vs this vs JVM localizes the 15x compute floor —
# jolt-backend overhead vs the Janet VM's own floor.
#
# janet bench/mandelbrot-hand.janet 200
(defn count-point [cr ci cap]
(var i 0)
(var zr 0.0)
(var zi 0.0)
(while (and (< i cap) (<= (+ (* zr zr) (* zi zi)) 4.0))
(def nzr (+ (- (* zr zr) (* zi zi)) cr))
(def nzi (+ (* 2.0 (* zr zi)) ci))
(set zr nzr)
(set zi nzi)
(++ i))
i)
(defn run [n]
(def cap 200)
(def nd (* 1.0 n))
(var acc 0)
(var y 0)
(while (< y n)
(def ci (- (/ (* 2.0 y) nd) 1.0))
(var x 0)
(var a 0)
(while (< x n)
(def cr (- (/ (* 2.0 x) nd) 1.5))
(set a (+ a (count-point cr ci cap)))
(++ x))
(set acc (+ acc a))
(++ y))
acc)
(defn main [& args]
(def n (if (> (length args) 1) (scan-number (get args 1)) 1000))
# warmup
(repeat 2 (run (div n 2)))
(def runs 3)
(def times @[])
(var last-r 0)
(repeat runs
(def t0 (os/clock))
(def r (run n))
(def ms (* 1000.0 (- (os/clock) t0)))
(set last-r r)
(array/push times ms))
(def mean (/ (sum times) runs))
(printf "mandelbrot n %d result %d" n last-r)
(print "runs: " (string/join (map |(string (/ (math/round (* $ 10.0)) 10.0)) times) " "))
(printf "mean: %.1f ms" mean))

View file

@ -1,7 +0,0 @@
#!/bin/sh
# -e-capable jolt-chez launcher (jolt-9ziu). Runs from the repo root so the
# assembled prelude can load host/chez/rt.ss by relative path.
# JOLT_BIN=bin/jolt-chez janet test/chez/run-corpus.janet
root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
cd "$root" || exit 1
exec janet host/chez/jolt-chez.janet "$@"

View file

@ -1,165 +0,0 @@
# Handoff — removing Janet (Chez re-host, Phase 4 → Phase 5)
**Branch:** `spike/chez-bootstrap` · **LOCAL COMMITS ONLY — NEVER push.** This
overrides the session-close git-push protocol.
**Epic:** jolt-cf1q (Re-host Jolt on Chez, zero Janet). **Last session:** finished
self-hosting (inc8/inc9) + the portable conformance spec + fixed 4 certified bugs.
## Where we are
Done and gated (all local commits, latest `6abbea3`):
- **Self-hosting proven.** The on-Chez compiler reproduces itself (stage2==stage3,
prelude pstage3==pstage4). `bin/joltc` + `host/chez/bootstrap.ss` build and run
jolt with **zero Janet in the loop**, off the checked-in seed `host/chez/seed/`.
- **Correctness proven.** Both Chez corpus gates at **2534/2756**, 0 divergences.
The corpus is a JVM-certified host-neutral spec (`test/conformance/`, see SPEC.md):
2670 portable cases, 249 feature-gated.
**We are NOT ready to delete Janet.** Ripping it out now regresses real capability
(below). The remaining work is Phase 4 (runtime features + deployment/perf) → then
Phase 5 (jolt-cf1q.6) deletes `src/jolt/*.janet` + the Janet cross-compile.
## Hard rules (every batch)
1. **Never `git push`.** Local commits only on this branch.
2. **Gate discipline.** After any change run, with a real exit code:
- `jpm build && janet run-tests.janet` (full Janet gate; must end `All tests passed.`)
- `JOLT_CHEZ_ZEROJANET_CORPUS=1 janet test/chez/run-corpus-zero-janet.janet` (fast, ~2s)
- `JOLT_CHEZ_PRELUDE_CORPUS=1 janet test/chez/run-corpus-prelude.janet` (~18min; run ISOLATED — no concurrent chez work)
- `clojure -M test/conformance/certify.clj` (JVM cert; fails on new/stale divergence)
3. **If `clojure.core` or any seed source changes, re-mint the Chez seed**, or
`test/chez/bootstrap-test.janet` fails (rebuilt ≠ committed seed). Re-mint:
```janet
(import host/chez/driver :as d) (import host/chez/jolt-chez :as jc)
(def ctx (d/make-ctx))
(d/mint-chez-seed* (jc/ensure-prelude ctx)
(d/ensure-compiler-image ctx "/tmp/stage1.ss")
"host/chez/seed/prelude.ss" "host/chez/seed/image.ss")
```
(run from repo root via a throwaway `test/chez/_x.janet`). Raise the corpus
floors when parity rises.
4. **Use beads** (`bd ready`, `bd show <id>`, `bd update <id> --claim`, `bd close`).
`bd remember` for persistent notes — see memories `chez-phase3-progress`,
`conformance-spec`. Run `bd close` as its own command then verify (close-race).
## Verify current state (run these first next session)
```sh
git log --oneline -8
bin/joltc -e '(+ 1 2)' # => 3 (pure-Chez runtime works)
bin/joltc -e '(eval (quote (+ 1 2)))' # => FAILS today — that's blocker #1
```
## The blockers, in priority order
### 1. jolt-r8ku — runtime eval / load-string / defmacro on the Chez spine ⭐ DO FIRST
**Why first:** biggest functional gap, and mostly *wiring* — the compiler image is
already resident at runtime on Chez (`host/chez/compile-eval.ss` loads it). Turns
`joltc` from a one-shot `-e` into a real runtime/REPL. Unblocks ~the eval/load-string
corpus crashes too.
**Symptoms (tested):**
```
joltc -e '(eval (quote (+ 1 2)))' -> jolt/uncompilable: special form eval
joltc -e '(load-string "(+ 1 2)")' -> not a fn (jolt-nil)
joltc -e '(defmacro m [x] ...) (m 5)' -> jolt/uncompilable: special form defmacro
```
**Why they punt:** `eval`/`defmacro` are in the special-symbol list, so the analyzer
treats them as special forms and the `:else` arm punts:
- `host/chez/host-contract.ss:110` and `:183``hc-special-symbols` / `form-special?`
list (includes `eval`, `defmacro`).
- `jolt-core/jolt/analyzer.clj:216` `analyze-special` (`case op …`), `:279`
`(uncompilable (str "special form " op))`.
**Approach:**
- **eval / load-string are FUNCTIONS, not special forms.** Remove `eval` from the
special-symbol lists so it resolves as a normal var, then `def-var!` them in a Chez
`.ss` (e.g. a new `host/chez/eval.ss` loaded by `compile-eval.ss`):
`eval` = `(lambda (form) (jolt-compile-eval-form form (current-ns)))` — but
`jolt-compile-eval` (compile-eval.ss) takes a SOURCE STRING and reads it; add a
sibling that takes an already-read FORM and runs analyze→emit→eval on it.
`load-string` = read-all + eval each form.
- Mirror the Janet seed semantics: check `src/jolt/eval_runtime.janet` /
`eval_base.janet` for what `eval`/`load-string` do there (the contract to match).
- **defmacro must stay special** (it defines a macro). The prelude already handles
defmacro at build time (`host/chez/emit-image.ss``ei-defmacro->fn` emits a def of
the expander fn + `mark-macro!`). For RUNTIME user defmacro, add a `defmacro` branch
to `analyze-special` that lowers to the same shape: a `def` of the `(fn …)` expander
+ a runtime `mark-macro!` call, so the macro is usable in subsequent forms of the
same program. Re-analysis of later forms must see the macro flag (`hc-macro?` reads
`var-macro-table`).
- Watch `jolt-lpvi` (build-ctx vs eval-ctx isolation) — runtime eval defining vars
must land in the user ns, not leak into the compiler image ns.
**Verify:** `joltc -e` of all three above; add cases to `test/chez/spine-test.janet`;
the eval/load-string corpus crashes (660/661/2157/2158/… per the old chez-phase2
notes) should now run; re-run both corpus gates.
### 2. jolt-byjr — concurrency (future / promise / agent / pmap / pvalues / pcalls)
`joltc -e '(deref (future (+ 1 2)))'``Unknown class clojure.core`. The Janet
oracle uses isolated-heap SNAPSHOT futures (the `:concurrency/snapshot` feature in
`test/conformance/profile.edn`) which neither a sync shim nor raw Chez threads
reproduce exactly. **Decision needed:** real Chez threads vs a sync shim vs snapshot
emulation. The corpus snapshot cases are oracle-coupled (JVM gives shared-heap
answers, jolt gives snapshot) — already classified as a deliberate delta, so don't
chase JVM parity; match the *jolt* documented semantics. Revisit with a Chez
thread-safety review of the RT (`host/chez/*.ss` mutable tables: var-table,
ns-registry, atoms).
### 3. jolt-cf1q.7 — host/Java interop breadth
~167 runtime crashes in the corpus are host-coupled (`profile.edn`:
`:host/jvm-interop` 174, `:host/arrays` 12, `:host/janet` 16). Java class shims,
arrays, `bean`/`proxy`/`definterface`. **Apply `vendor-before-scratch`**: for
non-trivial interop (date/time, crypto, etc.) vendor a mature Chez lib rather than
hand-rolling. Many of these are intentionally non-portable — decide per case whether
Chez should implement it or it stays a host-gated feature. `janet.*` cases are N/A on
Chez (delete from corpus in Phase 5).
### 4. jolt-cf1q.5 — Phase 4: deployment & optimization modes
The optimizing compiler is **Janet-only**: inference, direct-linking, cgen-to-C,
whole-program, and **native binary builds** (`jpm build`, the ring-app example). Chez
emits Scheme and relies on Chez's own native compiler — perf is **unmeasured**. The
Phase 5 bead literally gates on *"perf confirmed."* Tasks: benchmark the Chez path
vs the Janet/JVM baselines; decide the Chez deployment story (Chez `compile-program`/
boot file vs script); decide which Janet optimization modes (if any) must be
reproduced. This is the largest unknown — scope it before committing to deletion.
### 5. Move the test oracle off Janet, then Phase 5 (jolt-cf1q.6) delete
- Today `build/jolt` (Janet) is the oracle for `test/chez/_*.janet`, and
`run-corpus-prelude` analyzes on Janet. The corpus is now JVM-certified, so the
oracle can shift to **spec-corpus + JVM**. Do this swap before deleting Janet.
- Tooling still Janet: `jolt-deps`, `nrepl-server`, uberscript/DCE — port or drop.
- **Then** Phase 5: delete `src/jolt/*.janet` (reader, value layer, vars/ns, the
tree-walking interpreter `evaluator.janet`, the Janet backend) AND
`host/chez/emit.janet` + the Janet cross-compile in `driver.janet`. The
tree-walking interpreter (the literal "drop the interpreter" goal) dies here.
## Sequencing
```
jolt-r8ku (eval/macros) ─┐
jolt-byjr (concurrency) ├─► jolt-cf1q.7 (host interop) ─► jolt-cf1q.5 (Phase 4 perf/deploy)
─┘ │
oracle → JVM/corpus ◄───────┘
jolt-cf1q.6 (Phase 5: DELETE Janet)
```
## Key file map
| Area | File |
|---|---|
| Pure-Chez runtime CLI | `bin/joltc``host/chez/cli.ss` |
| Zero-Janet compile spine | `host/chez/compile-eval.ss` (`jolt-compile-eval`) |
| Host contract (special syms, resolve, macro?) | `host/chez/host-contract.ss` |
| On-Chez image/prelude emitter | `host/chez/emit-image.ss` |
| Pure-Chez self-build + seed | `host/chez/bootstrap.ss`, `host/chez/seed/` |
| Portable analyzer / backend | `jolt-core/jolt/analyzer.clj`, `…/backend_scheme.clj` |
| clojure.core overlay | `jolt-core/clojure/core/NN-*.clj` |
| Janet seed (to delete in Phase 5) | `src/jolt/*.janet` (reader, evaluator, host_iface, …) |
| Chez RT shims | `host/chez/*.ss` |
| Conformance spec | `test/conformance/` (SPEC.md, certify.clj, profile.edn, known-divergences.edn) |
| Corpus + gates | `test/chez/corpus.edn`, `run-corpus-{zero-janet,prelude}.janet`, `extract-corpus.janet` |
| Self-host gates | `test/chez/{spine,fixpoint,bootstrap,cli}-test.janet` |

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))

View file

@ -1,17 +0,0 @@
(declare-project
:name "jolt"
:description "Clojure interpreter on Janet")
(declare-source
:source @["src"])
(declare-executable
:name "jolt"
:entry "src/jolt/main.janet")
# Deprecated shim kept for back-compat: deps.edn resolution is built into `jolt`
# now (the CLI front-end resolves into JOLT_PATH in-process; the runtime core
# stays deps-agnostic). Forwards to `jolt`; prefer `jolt -M:…` / `jolt path`.
(declare-executable
:name "jolt-deps"
:entry "src/jolt/deps_cli.janet")

View file

@ -1,100 +0,0 @@
#!/usr/bin/env janet
# Parallel test runner — the gate, faster.
#
# `jpm test` runs every file under test/ serially; this runs the same set across
# a pool of worker processes (each test file is an independent `janet FILE`).
# Build the executable FIRST (`jpm build`) so the binary-spawning tests don't
# skip: jpm build && janet run-tests.janet
#
# Worker count is the detected CPU count; override with JOLT_TEST_JOBS. Per-file
# stdout+stderr is captured to a temp file and only printed for failures (and,
# with -v / JOLT_TEST_VERBOSE, for everything). Exits non-zero if any file fails
# and prints the literal "All tests passed." on success (CI greps for it).
(defn- detect-jobs []
(or (when-let [j (os/getenv "JOLT_TEST_JOBS")] (scan-number j))
(try
(let [p (os/spawn ["sh" "-c" "nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null"]
:p {:out :pipe})
out (:read (p :out) :all)]
(os/proc-wait p)
(scan-number (string/trim (or out ""))))
([_] nil))
4))
(defn- find-tests [dir acc]
(each name (sorted (os/dir dir))
(def full (string dir "/" name))
(case (os/stat full :mode)
# support/ holds shared libraries (harness, shims), not standalone tests —
# jpm doesn't run them and neither do we.
:directory (unless (= name "support") (find-tests full acc))
:file (when (string/has-suffix? ".janet" name) (array/push acc full))))
acc)
(def verbose? (or (has-value? (dyn :args) "-v") (os/getenv "JOLT_TEST_VERBOSE")))
(def janet-bin (dyn :executable))
(def tmpdir (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-test-" (os/getpid)))
(os/mkdir tmpdir)
(def files (find-tests "test" @[]))
(def total (length files))
(def jobs (max 1 (min 16 (detect-jobs))))
(def results @[])
(var done 0)
(defn- run-one [file]
(def t0 (os/clock))
(def of (string tmpdir "/" (string/replace-all "/" "_" file) ".out"))
# let the shell own redirection — avoids pipe-buffer deadlock on chatty tests.
(def cmd (string/format "exec '%s' '%s' > '%s' 2>&1" janet-bin file of))
(def rc (os/proc-wait (os/spawn ["sh" "-c" cmd] :p)))
(def dt (- (os/clock) t0))
(array/push results {:file file :rc rc :out of :dt dt})
(++ done)
(printf "[%d/%d] %s %6.2fs %s%s" done total (if (zero? rc) "ok " "FAIL")
dt file (if (zero? rc) "" (string/format " (rc=%d)" rc)))
(flush))
# A :stop sentinel per worker signals end-of-queue — ev/chan-close discards
# buffered items (Janet 1.41), so we can't just close and drain.
(def q (ev/chan (+ total jobs)))
(each f files (ev/give q f))
(loop [_ :range [0 jobs]] (ev/give q :stop))
(defn- worker [&]
(forever
(def f (ev/take q))
(if (= f :stop) (break) (run-one f))))
(printf "running %d test files across %d workers...\n" total jobs)
(flush)
(def wall0 (os/clock))
(def super (ev/chan))
(loop [_ :range [0 jobs]] (ev/go worker nil super))
(loop [_ :range [0 jobs]] (ev/take super))
(def wall (- (os/clock) wall0))
(def failed (filter |(not (zero? ($ :rc))) results))
(def slow (->> results (sorted-by |(- ($ :dt))) (take 5)))
(print "\n--- slowest ---")
(each r slow (printf " %6.2fs %s" (r :dt) (r :file)))
(when verbose?
(each r results
(printf "\n===== %s (rc=%d, %.2fs) =====" (r :file) (r :rc) (r :dt))
(print (slurp (r :out)))))
(unless (empty? failed)
(print "\n--- FAILURES ---")
(each r failed
(printf "\n===== %s (rc=%d) =====" (r :file) (r :rc))
(print (slurp (r :out)))))
(printf "\n%d files, %d failed, %.1fs wall (%d workers)"
total (length failed) wall jobs)
(if (empty? failed)
(do (print "\nAll tests passed.") (os/exit 0))
(do (printf "\n%d test files FAILED." (length failed)) (os/exit 1)))

View file

@ -1,47 +0,0 @@
# AOT build/deploy demo (jolt-a7ds). Two phases, run as separate processes:
# janet spike/native/aot-demo.janet build # needs cc; compiles + writes manifest
# janet spike/native/aot-demo.janet deploy # run with cc REMOVED from PATH
# Proves the deploy target needs no C toolchain: it only loads the prebuilt .so.
(import ../../src/jolt/api :as api)
(import ../../src/jolt/cgen :as cgen)
(def cp "(defn count-point [cr ci cap] (loop [i 0 zr 0.0 zi 0.0] (if (or (>= i cap) (> (+ (* zr zr) (* zi zi)) 4.0)) i (recur (inc i) (+ (- (* zr zr) (* zi zi)) cr) (+ (* 2.0 (* zr zi)) ci)))))")
(def run "(defn run [n] (let [cap 200 nd (* 1.0 n)] (loop [y 0 acc 0] (if (< y n) (let [ci (- (/ (* 2.0 y) nd) 1.0) row (loop [x 0 a 0] (if (< x n) (let [cr (- (/ (* 2.0 x) nd) 1.5)] (recur (inc x) (+ a (count-point cr ci cap)))) a))] (recur (inc y) (+ acc row))) acc))))")
(def manifest (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-aot-demo.jdn"))
(def dir "spike/native/build/aot")
(defn setup [ctx]
(put (ctx :env) :direct-linking? true)
(api/eval-string ctx "(ns demo)"))
(def phase (get (dyn :args) 1))
(cond
(= phase "build")
(do
(def ctx (api/init-cached {:compile? true}))
(put (ctx :env) :cgen-collect? true)
(setup ctx)
(api/eval-string ctx cp)
(api/eval-string ctx run)
(def build (cgen/aot-build (get (ctx :env) :cgen-collected) {:dir dir}))
(cgen/write-manifest manifest build)
(printf "BUILD: cc-available? %p -> %s (%d fn)" (cgen/toolchain-available?)
(build :sopath) (length (build :entries))))
(= phase "deploy")
(do
(printf "DEPLOY: cc-available? %p (should be false with cc off PATH)"
(cgen/toolchain-available?))
(def ctx (api/init-cached {:compile? true}))
(setup ctx)
(put (ctx :env) :cgen-prebuilt (cgen/load-aot manifest))
(api/eval-string ctx cp)
(api/eval-string ctx run)
(def native? (cfunction? (api/eval-string ctx "count-point")))
(def t0 (os/clock))
(def total (api/eval-string ctx "(run 200)"))
(def ms (* 1000 (- (os/clock) t0)))
(printf "DEPLOY: count-point native? %p total %d (%s) %.1f ms"
native? total (if (= total 3288753) "OK" "MISMATCH") ms))
(eprint "usage: aot-demo.janet build|deploy"))

View file

@ -1,53 +0,0 @@
# Benchmark the native-C mandelbrot vs the spike's other legs (jolt-5vsp lever 1).
# janet spike/native/bench-native.janet 200
(import ./build/mandel :as mandel)
(defn bench [label f n]
(repeat 2 (f (div n 2)))
(def times @[])
(var last-r 0)
(repeat 3
(def t0 (os/clock))
(set last-r (f n))
(array/push times (* 1000.0 (- (os/clock) t0))))
(printf "%-28s n %d result %d mean %.2f ms"
label n last-r (/ (sum times) 3)))
# Leg A: whole run in native C (pure native-codegen ceiling).
(defn run-pure-c [n] (mandel/run-c n))
# Leg B: Janet `while` loop, but count-point is a native C cfunction called n^2
# times — measures the Janet->C boundary-crossing cost (the incremental hybrid).
(defn run-boundary [n]
(def cap 200)
(def nd (* 1.0 n))
(var acc 0)
(var y 0)
(while (< y n)
(def ci (- (/ (* 2.0 y) nd) 1.0))
(var x 0)
(var a 0)
(while (< x n)
(def cr (- (/ (* 2.0 x) nd) 1.5))
(set a (+ a (mandel/count-point-c cr ci cap)))
(++ x))
(set acc (+ acc a))
(++ y))
acc)
# Leg C: C run loop calling a Janet bytecode count-point back via janet_call n^2
# times — the reverse crossing (hot C fn -> cold bytecode helper).
(defn count-point-janet [cr ci cap]
(var i 0) (var zr 0.0) (var zi 0.0)
(while (and (< i cap) (<= (+ (* zr zr) (* zi zi)) 4.0))
(def nzr (+ (- (* zr zr) (* zi zi)) cr))
(def nzi (+ (* 2.0 (* zr zi)) ci))
(set zr nzr) (set zi nzi) (++ i))
i)
(defn run-callback [n] (mandel/run-callback n count-point-janet))
(defn main [& args]
(def n (if (> (length args) 1) (scan-number (get args 1)) 200))
(bench "native-C whole run" run-pure-c n)
(bench "Janet loop -> C count-point" run-boundary n)
(bench "C loop -> janet_call back" run-callback n))

View file

@ -1,15 +0,0 @@
# Dump the analyzed IR (not emitted Janet) for count-point, so the C emitter can
# be written against the real node shapes. jolt-ihdp.
(import ../../src/jolt/api :as api)
(import ../../src/jolt/backend :as backend)
(import ../../src/jolt/reader :as reader)
(def ctx (api/init-cached {:compile? true}))
(put (ctx :env) :direct-linking? true)
(put (ctx :env) :inline? true)
(api/eval-string ctx "(ns mandelbrot)")
(def src "(defn count-point [cr ci cap] (loop [i 0 zr 0.0 zi 0.0] (if (or (>= i cap) (> (+ (* zr zr) (* zi zi)) 4.0)) i (recur (inc i) (+ (- (* zr zr) (* zi zi)) cr) (+ (* 2.0 (* zr zi)) ci)))))")
(def ir (backend/analyze-form ctx (reader/parse-string src)))
(printf "%P" ir)

View file

@ -1,91 +0,0 @@
/* Native-C mandelbrot, exposed as a Janet module — the lever-1 ceiling probe for
* the foundational-runtime epic (jolt-5vsp). Measures:
* (1) mandel/run-c whole run in C (count_point inlined in C). The pure
* native-codegen ceiling: no Janet in the hot loop.
* (2) mandel/count-point-c just count_point exposed as a Janet cfunction, so a
* Janet `while` loop can call it n^2 times. Measures the
* Janet->C boundary-crossing cost the incremental
* hybrid (hot fn in C, caller still bytecode) pays this.
* Build: jpm --local build (project.janet declares the native module). */
#include <janet.h>
/* Pure C. cr/ci/cap are doubles; cap compared as int iteration count. */
static long count_point(double cr, double ci, long cap) {
long i = 0;
double zr = 0.0, zi = 0.0;
while (i < cap && (zr*zr + zi*zi) <= 4.0) {
double nzr = zr*zr - zi*zi + cr;
double nzi = 2.0*zr*zi + ci;
zr = nzr; zi = nzi;
i++;
}
return i;
}
static long run_c(long n) {
long cap = 200;
double nd = (double)n;
long acc = 0;
for (long y = 0; y < n; y++) {
double ci = (2.0*y)/nd - 1.0;
long a = 0;
for (long x = 0; x < n; x++) {
double cr = (2.0*x)/nd - 1.5;
a += count_point(cr, ci, cap);
}
acc += a;
}
return acc;
}
static Janet cfun_run_c(int32_t argc, Janet *argv) {
janet_fixarity(argc, 1);
long n = (long)janet_getinteger(argv, 0);
return janet_wrap_number((double)run_c(n));
}
/* count_point exposed for the Janet-loop-calls-C boundary test. */
static Janet cfun_count_point_c(int32_t argc, Janet *argv) {
janet_fixarity(argc, 3);
double cr = janet_getnumber(argv, 0);
double ci = janet_getnumber(argv, 1);
long cap = (long)janet_getinteger(argv, 2);
return janet_wrap_number((double)count_point(cr, ci, cap));
}
/* run loop in C, but count_point is a Janet function called back via janet_call
* n^2 times the reverse crossing: a C-compiled hot fn invoking a cold bytecode
* helper. Measures janet_call overhead (the cost the hybrid pays when native code
* calls back into the bytecode world). */
static Janet cfun_run_callback(int32_t argc, Janet *argv) {
janet_fixarity(argc, 2);
long n = (long)janet_getinteger(argv, 0);
JanetFunction *cp = janet_getfunction(argv, 1);
long cap = 200;
double nd = (double)n;
long acc = 0;
for (long y = 0; y < n; y++) {
double ci = (2.0*y)/nd - 1.0;
long a = 0;
for (long x = 0; x < n; x++) {
double cr = (2.0*x)/nd - 1.5;
Janet args[3] = { janet_wrap_number(cr), janet_wrap_number(ci),
janet_wrap_number((double)cap) };
Janet r = janet_call(cp, 3, args);
a += (long)janet_unwrap_number(r);
}
acc += a;
}
return janet_wrap_number((double)acc);
}
static const JanetReg cfuns[] = {
{"run-c", cfun_run_c, "(mandel/run-c n) whole mandelbrot run in native C."},
{"count-point-c", cfun_count_point_c, "(mandel/count-point-c cr ci cap) one point, native C."},
{"run-callback", cfun_run_callback, "(mandel/run-callback n count-point-fn) C loop calling a Janet fn back via janet_call."},
{NULL, NULL, NULL}
};
JANET_MODULE_ENTRY(JanetTable *env) {
janet_cfuns(env, "mandel", cfuns);
}

View file

@ -1,6 +0,0 @@
(declare-project
:name "mandel-native-spike")
(declare-native
:name "mandel"
:source ["mandel.c"])

View file

@ -1,47 +0,0 @@
# Ahead-of-time images for compiled namespaces.
#
# Compile-by-default turns each form into Janet bytecode at load time. AOT skips
# that work on subsequent runs by serializing a namespace's compiled vars to a
# bytecode image and loading them back.
#
# The trick is the marshal dictionary. A compiled jolt function closes over core
# fns (core-map, +, …) and var cells; those core fns are Janet cfunctions/closures
# that can't be marshaled by value. But the runtime env that holds them is baked
# into the binary and is byte-for-byte identical at save and load time, so we
# marshal *against* it: core fns are referenced by name, and only the user's
# bytecode plus its var cells are actually serialized.
(import ./backend :as backend) # backend/jolt-runtime-env
(use ./types)
# Forward dict (key -> value) for unmarshal; reverse (value -> key) for marshal.
# Built from the runtime env, which chains to the Janet boot env, so both core fns
# and Janet builtins resolve by name.
(defn- fwd-dict [] (env-lookup backend/jolt-runtime-env))
(defn- rev-dict [] (invert (env-lookup backend/jolt-runtime-env)))
(defn marshal-ns
"Marshal namespace `ns-name`'s var mappings to a byte buffer. The whole mappings
table is marshaled in one call so var cells shared between defs stay shared."
[ctx ns-name]
(marshal ((ctx-find-ns ctx ns-name) :mappings) (rev-dict)))
(defn unmarshal-ns!
"Install mappings produced by marshal-ns into `ns-name` in ctx, overwriting
same-named vars. Returns ns-name."
[ctx ns-name bytes]
(let [mappings (unmarshal bytes (fwd-dict))
ns (ctx-find-ns ctx ns-name)]
(each [sym v] (pairs mappings) (put (ns :mappings) sym v))
ns-name))
(defn save-ns
"Write an AOT image of compiled namespace `ns-name` to `path`."
[ctx ns-name path]
(spit path (marshal-ns ctx ns-name)))
(defn load-ns-image
"Read an AOT image written by save-ns back into ctx under `ns-name`. Skips
parse/analyze/emit/compile entirely — the bytecode is already built."
[ctx ns-name path]
(unmarshal-ns! ctx ns-name (slurp path)))

View file

@ -1,434 +0,0 @@
# Jolt Public API
# High-level interface for the Clojure-on-Janet interpreter.
(use ./types)
(use ./pv)
(use ./plist)
(use ./reader)
(use ./evaluator)
(use ./core)
(use ./loader)
(use ./async)
(import ./backend :as backend)
(import ./stdlib_embed :as stdlib-embed)
(import ./host_iface :as host)
(import ./config :as config)
# Host interop: JVM class/method shims + collection interop (.iterator/.nth/...)
# register into the evaluator at module load. See src/jolt/host_interop.janet.
(import ./host_interop)
# A defmacro expander compiles to a native fn (built as (fn args body...) and run
# through the self-hosted pipeline) so macro expansion is COMPILED code, zero runtime
# cost — instead of an interpreted closure, mirroring Clojure (macros are ordinary
# compiled fns). Wrapped in the `fn` MACRO (not the `fn*` primitive) so a destructured
# macro arglist — `[a & [b]]`, `[& {:keys [x]}]`, nested — desugars before lowering;
# raw fn* punts on a destructuring rest param. Returns nil when the analyzer isn't
# built yet (the early macros, expanded WHILE the analyzer is being bootstrapped) or
# the body isn't compilable; in that case defmacro keeps an interpreted closure, and
# backend/recompile-macros! replaces it with a compiled expander once the analyzer
# comes alive (staged bootstrap — the interpreter is a build-time crutch, gone by
# steady state).
(set macro-compile-hook
(fn [ctx args-form body]
(backend/try-compile-fn ctx
(array/concat @[{:jolt/type :symbol :ns nil :name "fn"} args-form] body))))
(defn normalize-pvecs
"Deep-convert any sequential (pvec/tuple/array) to a Janet tuple. Test helper
so Janet-level `=`/deep= can compare jolt collection results against Janet
tuple literals regardless of representation — mirroring Clojure, where vectors
and lists with the same elements are equal."
[x]
(cond
# lazy-seq: realize to a tuple (map/filter/take now return lazy seqs).
(and (table? x) (= (get x :jolt/type) :jolt/lazy-seq))
(tuple ;(map normalize-pvecs (realize-for-iteration x)))
(pvec? x) (tuple ;(map normalize-pvecs (pv->array x)))
(plist? x) (tuple ;(map normalize-pvecs (pl->array x)))
(tuple? x) (tuple ;(map normalize-pvecs x))
(array? x) (tuple ;(map normalize-pvecs x))
x))
# Ordered clojure.core tiers (embedded jolt-core/clojure/core/NN-*.clj). Each tier
# may reference only the Janet seed + earlier tiers. A :kernel tier holds the
# structural fns the self-hosted compiler itself uses (second/peek/subvec/mapv/
# update); in compile mode it must be bootstrap-compiled into clojure.core BEFORE
# the analyzer is built (the analyzer depends on it), so it bypasses the
# self-hosted pipeline. Non-kernel tiers route through eval-toplevel like any
# source (compiled when :compile?, interpreted otherwise — the analyzer, built
# lazily on the first such form, sees the kernel tier already in place).
(def- core-tiers
[{:ns "clojure.core.00-syntax" :kernel false}
{:ns "clojure.core.00-kernel" :kernel true}
{:ns "clojure.core.10-seq" :kernel false}
{:ns "clojure.core.20-coll" :kernel false}
{:ns "clojure.core.25-sorted" :kernel false}
{:ns "clojure.core.30-macros" :kernel false}
{:ns "clojure.core.40-lazy" :kernel false}
{:ns "clojure.core.50-io" :kernel false}])
(defn- eval-overlay-source [ctx src]
(var s src)
(while (> (length (string/trim s)) 0)
(def [form rest] (parse-next s))
(set s rest)
(when (not (nil? form)) (eval-toplevel ctx form))))
(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 env (ctx :env))
(def compile? (get env :compile?))
# Core compiles with direct-linking on when :aot-core? (so core->core calls
# are direct). The flag is restored to the user-code default afterward, so
# user/REPL code stays indirect and fully redefinable.
(def user-dl (get env :direct-linking?))
(def core-dl (get env :aot-core?))
(def saved (ctx-current-ns ctx))
(ctx-set-current-ns ctx "clojure.core")
# Gate the analyzer build until the kernel tier loads (see ensure-analyzer):
# present-and-false here means pre-kernel compiles fall back to the interpreter.
(put env :kernel-ready? false)
# Pre/at-kernel defns load interpreted in some mode (00-syntax always; the
# kernel tier too in interpret mode); stash their fn sources so the staged
# recompile pass (backend/recompile-defns!) can compile them once the
# analyzer is alive. Cleared after the kernel tier so later tiers and user
# code don't stash.
(put env :stash-defn-src? true)
(each tier core-tiers
(when-let [src (get stdlib-embed/sources (tier :ns))]
(put env :direct-linking? core-dl)
(if (and compile? (tier :kernel))
(backend/bootstrap-load-source ctx "clojure.core" src)
(eval-overlay-source ctx src))
# The self-hosted compiler depends on the kernel tier (second/peek/mapv/...).
# Mark it ready once that tier is in place so the analyzer can be built; a
# pre-kernel tier that triggers a compile (e.g. a defn in 00-syntax) instead
# falls back to the interpreter rather than building the analyzer against a
# half-loaded core (which would forward-ref the missing kernel fns to nil).
(when (tier :kernel)
(put env :kernel-ready? true)
(put env :stash-defn-src? false))))
(put env :direct-linking? user-dl)
(ctx-set-current-ns ctx saved)
# Stage 3 interpreted bootstrap: the analyzer was loaded INTERPRETED (no
# bootstrap compiler); have it compile itself + the kernel tier before the
# macro pass, so steady-state compilation runs compiled.
(when compile?
(backend/self-compile-compiler! ctx))
# Staged bootstrap: the early macros (00-syntax) were defined while the analyzer
# was still being built, so their expanders are interpreted closures. Now that the
# full overlay + analyzer are in place, recompile those expanders to native code —
# by steady state no macro expansion runs interpreted (no-op in interpreter mode).
(backend/ensure-macros-compiled! ctx)
# print-method's record hook: only wirable once the overlay multimethod
# exists (50-io tier), so it rides the end of overlay load.
(install-print-method-cb! ctx))
# clojure.math (Clojure 1.11) backed directly by Janet's math natives
# (jolt-h79). The vars hold plain Janet functions, so compiled calls
# direct-link — unlike Math/sqrt interop forms, which are in the frozen
# interpret-only punt set (~5us/call vs ~30ns here). Installed as a
# populated namespace, so (require '[clojure.math :as m]) is a no-op pass.
(defn- install-clojure-math! [ctx]
(def ns (ctx-find-ns ctx "clojure.math"))
(def fns
{"sqrt" math/sqrt "cbrt" math/cbrt "pow" math/pow
"exp" math/exp "expm1" math/expm1
"log" math/log "log10" math/log10 "log1p" math/log1p
"sin" math/sin "cos" math/cos "tan" math/tan
"asin" math/asin "acos" math/acos "atan" math/atan "atan2" math/atan2
"sinh" math/sinh "cosh" math/cosh "tanh" math/tanh
"floor" math/floor "ceil" math/ceil "rint" math/round
"round" (fn cm-round [x] (math/round x))
"signum" (fn cm-signum [x] (cond (< x 0) -1.0 (> x 0) 1.0 0.0))
"to-degrees" (fn cm-to-degrees [r] (/ (* r 180.0) math/pi))
"to-radians" (fn cm-to-radians [d] (/ (* d math/pi) 180.0))
"hypot" (fn cm-hypot [a b] (math/sqrt (+ (* a a) (* b b))))
"floor-div" (fn cm-floor-div [a b] (math/floor (/ a b)))
"floor-mod" (fn cm-floor-mod [a b] (mod a b))
"E" math/e "PI" math/pi})
(eachp [nm f] fns (ns-intern ns nm f))
# mark loaded so maybe-require-ns never goes looking for a source file
(def loaded (or (get (ctx :env) :loaded-namespaces)
(let [t @{}] (put (ctx :env) :loaded-namespaces t) t)))
(put loaded "clojure.math" true))
(defn init
"Create a new Jolt evaluation context.
opts may contain:
:namespaces — map of {ns-name → {sym → value, ...}, ...}
:mutable? — use Janet mutable data structures instead of persistent
:compile? — compile Clojure forms via the self-hosted pipeline (analyzer ->
IR -> Janet back end), falling back to the interpreter as needed
:paths — extra source roots to search for namespaces (after the stdlib)"
[&opt opts]
(default opts {})
(let [ctx (make-ctx opts)]
# The .clj stdlib (clojure.string, jolt.http, …) baked into the image at build
# time, so it loads from any directory; the loader falls back to this when a
# namespace isn't found on disk. (See stdlib-embed.)
(put (ctx :env) :embedded-sources stdlib-embed/sources)
# Extra source roots: opts :paths, then JOLT_PATH (colon-separated). These are
# searched after the stdlib so (require ...) finds deps.edn-resolved libs.
(let [roots (get (ctx :env) :source-paths)]
(each p (get opts :paths []) (array/push roots p))
(when-let [jp (os/getenv "JOLT_PATH")]
(each p (string/split ":" jp) (when (> (length p) 0) (array/push roots p)))))
# App source roots (jolt-87e): the project's own roots (opts :app-paths, then
# JOLT_APP_PATHS from jolt-deps). The whole-program inference fixpoint is
# scoped to namespaces loaded from these — deps load-infer per-ns instead.
# Empty => no app/dep distinction, so whole-program covers every namespace
# (a bare program run with no deps.edn keeps its old behavior).
(let [aps @[]]
(each p (get opts :app-paths []) (array/push aps p))
(when-let [jap (os/getenv "JOLT_APP_PATHS")]
(each p (string/split ":" jap) (when (> (length p) 0) (array/push aps p))))
(put (ctx :env) :app-source-paths aps))
# Collection representation (persistent vs mutable) is selected at BUILD time
# via JOLT_MUTABLE (see config.janet); init-core! registers vec/vector/conj/
# etc. that produce the mode-appropriate values, so nothing extra to load.
(init-core! ctx)
# clojure.core.async (channels + go blocks on Janet fibers); pre-populated
# so (require '[clojure.core.async ...]) finds it and applies :as/:refer.
(install-async! ctx)
# Host contract (ns jolt.host): the seam the portable jolt-core compiler calls.
(host/install! ctx)
(install-clojure-math! ctx)
# require/maybe-require-ns route loaded namespaces through the loader's
# compile-or-interpret eval-toplevel (the evaluator can't import the loader
# — that would be circular — so it reads this hook). Without it, required
# namespaces ran interpreted-only.
(put (ctx :env) :toplevel-eval eval-toplevel)
# Inter-procedural type-inference hook (jolt-767): the evaluator calls this
# after a unit finishes loading (optimization mode only). Installed here to
# avoid an evaluator->backend circular import.
(put (ctx :env) :infer-unit! backend/infer-unit!)
(put (ctx :env) :infer-program! backend/infer-program!) # jolt-t34 whole-program
# Stateful primitives as ctx-capturing clojure.core fns (protocol-dispatch,
# register-method, …) — so the protocol macros compile to plain invokes. Must
# precede the overlay (its defprotocol/extend-type expansions call these).
(install-stateful-fns! ctx)
# Clojure portion of clojure.core (jolt-core/clojure/core.clj): fns expressed
# in plain Clojure on top of the Janet primitives interned above. Loaded into
# clojure.core and compiled by the self-hosted pipeline (or interpreted when
# :compile? is off). Phase 4 kernel-shrink seam — see that file.
(load-core-overlay! ctx)
# load-string and eval as VALUES need the loader's compile-or-interpret
# routing, which lives above the evaluator — intern them here. (The eval
# special form still handles direct calls; this covers value position,
# e.g. (map eval forms).)
(let [core (ctx-find-ns ctx "clojure.core")]
(ns-intern core "load-string"
(fn [s]
(var cur s)
(var result nil)
(while (> (length (string/trim cur)) 0)
(def [form rest-src] (parse-next cur))
(set cur rest-src)
(when (not (nil? form)) (set result (eval-toplevel ctx form))))
result))
(ns-intern core "eval" (fn [form] (eval-toplevel ctx form))))
# Init is done: core + the self-hosted compiler are loaded with :inline? off
# (so they compiled exactly as before). Flip inlining on for subsequent
# user-code compilation iff user direct-linking is on (JOLT_DIRECT_LINK=1) —
# the inline pass only inlines targets that won't be redefined, the same
# safety the direct-linking flag asserts (jolt-87f).
(put (ctx :env) :inline? (if (get (ctx :env) :direct-linking?) true false))
# jolt-t34. Two shape gates:
# :shapes? — shape-recs are active. Records use declared-shape layout +
# bare-index reads here. ON wherever the inference that proves
# reads runs = direct-linking. JOLT_NO_SHAPE force-disables.
# :map-shapes? — also shape generic const-key MAP literals. Opt-in (JOLT_SHAPE)
# because shaping maps net-loses on unproven reads; records win.
(put (ctx :env) :shapes?
(and (get (ctx :env) :direct-linking?) (not (os/getenv "JOLT_NO_SHAPE"))))
(put (ctx :env) :map-shapes?
(and (os/getenv "JOLT_SHAPE") (not (os/getenv "JOLT_NO_SHAPE"))))
# Whole-program (Stalin) mode (jolt-t34): opt-in, closed-world. Defers the
# per-ns inference and runs one fixpoint over all units at the end (main, or a
# harness calling infer-program!). Needs direct-linking (the closed-world
# assumption); slow/memory-heavy builds are the documented trade-off.
(put (ctx :env) :whole-program?
(and (os/getenv "JOLT_WHOLE_PROGRAM") (get (ctx :env) :direct-linking?)))
ctx))
# --- Context snapshot/fork + disk image (AOT context image) ------------------
#
# init is expensive (~50 ms interpreted, ~900 ms compiled: tier loading, analyzer
# build, macro recompilation). For workloads that need MANY isolated contexts —
# the test harnesses build a fresh ctx per case — snapshot a fully-built ctx once
# and fork cheap deep copies (~2 ms) from it via Janet marshal/unmarshal. A fork
# shares nothing mutable with the original: defs, protocol extensions, hierarchy
# changes, atom states in one fork are invisible to the others.
#
# load-ctx-image/save-ctx-image add the disk layer (fork->validate->rewire load,
# atomic save) shared by init-cached and main's deps-image cache (jolt-q5ql) —
# the callers differ only in the validity predicate. They live here (not a
# separate module) because Janet's `use` doesn't transitively re-export, and the
# snapshot/fork they build on are already api's and consumed via (use ./api).
#
# The reverse-lookup dicts must be built from root-env (cfunctions and abstract
# values from the Janet runtime marshal by reference through them) BEFORE any ctx
# values exist in scope — module load time here, so user code can't leak into it.
(def- image-load-dict (env-lookup root-env))
(def- image-make-dict (invert image-load-dict))
(defn snapshot
"Marshal a fully-built context into a buffer that fork can cheaply clone.
Build the ctx (init), customize it if needed, then snapshot once."
[ctx]
(marshal ctx image-make-dict))
(defn fork
"A fresh, fully-isolated deep copy of a snapshotted context (~2 ms, vs
re-running init). (fork (snapshot ctx)) behaves exactly like ctx did at
snapshot time; mutations to a fork never affect the original or other forks."
[snap]
(unmarshal snap image-load-dict))
(defn load-ctx-image
"Load a marshaled ctx image from `path`: fork it, verify it really is a ctx (a
corrupt image can unmarshal to a non-ctx value), run the optional `valid?`
predicate (e.g. a source-mtime check), and replay the per-PROCESS module-state
wiring an image restore skips by calling `(rewire! ctx)` (the print-method hook
lives in module state, not the marshaled ctx). Returns the ctx, or nil if the
file is absent, unreadable, not a ctx, or fails `valid?`."
[path rewire! &opt valid?]
(when (os/stat path)
(def r (protect (fork (slurp path))))
(when (and (r 0) (ctx? (r 1)) (or (nil? valid?) (valid? (r 1))))
(def c (r 1))
(when rewire! (rewire! c))
c)))
(defn save-ctx-image
"Atomically write ctx's marshaled image to `path` (write a pid-tagged tmp file
then rename) so concurrent cold starts never observe a torn image. Best-effort:
a failed write/rename is swallowed (the next run just rebuilds)."
[ctx path]
(def tmp (string path "." (os/getpid) ".tmp"))
(when (protect (spit tmp (snapshot ctx)))
(protect (os/rename tmp path))))
# init in compile mode is ~2.4 s (tier loading, analyzer self-compile, macro
# recompilation) — paid by every PROCESS that builds a ctx from source, e.g.
# each `jpm test` file. init-cached pays it once: the built ctx is snapshotted
# to an image file and later processes unmarshal it (~tens of ms). Marshaling
# is against root-env (same dicts as snapshot/fork), so core cfunctions ride by
# name and everything jolt-level rides by value — a loaded image needs nothing
# from the baking process. The cache key fingerprints everything a fresh build
# would read: the embedded .clj stdlib (which includes jolt-core — the
# analyzer, IR, and core tiers), the .janet seed sources next to this module,
# the janet version, and the init opts. Any change rebuilds; a corrupt or
# unreadable image silently rebuilds. JOLT_NO_IMAGE_CACHE=1 disables.
# Captured at module load: in source mode this is .../src/jolt/api.janet, so
# the seed sources can be fingerprinted; nil or stale in a built binary, where
# the baked-at-build-time ctx makes init-cached pointless anyway.
(def- api-module-file (dyn :current-file))
(defn- src-dir []
(when api-module-file
(let [idxs (string/find-all "/" api-module-file)]
(when (not (empty? idxs))
(string/slice api-module-file 0 (last idxs))))))
# Every .janet seed file under `dir`, RECURSIVELY (src/jolt/ has subdirs now —
# interop/), keyed by repo-relative path so files in different dirs don't alias.
# Sorted for a stable fingerprint. Non-recursive (os/dir) would silently miss a
# subdir edit and serve a stale image — the map-nil-style footgun, for the cache.
(defn- janet-sources-under [dir]
(def acc @[])
(defn- walk [d prefix]
(each name (sorted (os/dir d))
(def full (string d "/" name))
(def rel (string prefix name))
(case (os/stat full :mode)
:directory (walk full (string rel "/"))
:file (when (string/has-suffix? ".janet" name) (array/push acc [rel full])))))
(walk dir "")
acc)
(defn- source-fingerprint
"Hash + total length of every source a fresh init depends on. Two numbers, so
a 32-bit hash collision alone can't alias two different source trees."
[]
(def buf @"")
(each k (sorted (keys stdlib-embed/sources))
(buffer/push buf k "\x00" (get stdlib-embed/sources k) "\x00"))
(def dir (src-dir))
(when dir
(each [rel full] (janet-sources-under dir)
(buffer/push buf rel "\x00" (slurp full) "\x00")))
[(hash (string buf)) (length buf)])
(defn- image-cache-path [opts]
(def dir (or (os/getenv "JOLT_IMAGE_CACHE_DIR")
(os/getenv "TMPDIR")
"/tmp"))
(def [h len] (source-fingerprint))
# Opts land in the key via their printed form; an opt that prints unstably
# (e.g. a closure in :namespaces) just degrades to a cache miss, never to a
# wrong hit. Every ctx-shaping env var rides along via config/ctx-cache-key —
# one canonical list shared with the deps-image key (jolt-q5ql), so a new knob
# can't be added to one key and forgotten in the other.
(def key (config/ctx-cache-key [:janet (string janet/version "-" janet/build) :opts opts]))
(string dir "/jolt-ctx-" (band h 0x7FFFFFFF) "-" len "-" (band (hash key) 0x7FFFFFFF) ".jimg"))
(defn init-cached
"init, but disk-cached: the first call builds the context and writes a
bytecode image; later calls (any process, same sources) load the image
instead of rebuilding. Same opts as init. JOLT_NO_IMAGE_CACHE=1 disables;
JOLT_IMAGE_CACHE_DIR overrides the cache directory (default TMPDIR)."
[&opt opts]
(default opts {})
(if (or (= "1" (os/getenv "JOLT_NO_IMAGE_CACHE")) (nil? (src-dir)))
(init opts)
(let [path (image-cache-path opts)]
# The source-fingerprint is already in the path, so any cached image at
# this path is valid — no extra predicate, just the per-process rewiring.
(or (load-ctx-image path install-print-method-cb!)
(let [ctx (init opts)]
(save-ctx-image ctx path)
ctx)))))
(defn eval-one
"Evaluate a single already-parsed form. Routing (compile when :compile? is set,
stateful forms interpret, interpreter fallback for forms the compiler can't
handle) lives in loader/eval-toplevel so load-ns and eval-one stay in sync."
[ctx form]
(eval-toplevel ctx form))
(defn eval-string
"Evaluate a Clojure source string in a Jolt context.
When :compile? is enabled, compiles to Janet and evaluates.
Macros are expanded at compile time.
Context-modifying forms (ns, defmacro, deftype, require, in-ns, defmulti, defmethod)
always use the interpreter."
[ctx s]
(eval-one ctx (parse-string s)))
(defn eval-string*
"Evaluate a Clojure source string with explicit bindings."
[ctx s bindings]
(let [form (parse-string s)]
(eval-form ctx bindings form)))
(defn load-string
"Evaluate all forms from a Clojure source string.
Uses parse-next to load every top-level form in sequence.
Returns the result of the last form evaluated."
[ctx s &opt file]
(default file "<eval>")
# record form positions so the checker can report file:line:col (jolt-fqy).
# The checker is on when JOLT_TYPE_CHECK selects it, OR by default in
# direct-link builds (where it piggybacks on inference for free).
(when (or (checker-enabled?) (get (ctx :env) :inline?))
(track-positions! true)
(put (ctx :env) :tc-source s)
(put (ctx :env) :tc-file file))
(eval-forms-positioned ctx (parse-all-positioned s file) file))

View file

@ -1,208 +0,0 @@
# clojure.core.async on Janet fibers.
#
# Janet fibers are stackful coroutines, so a `go` block is just "run the body in
# a fiber" — the body parks on a channel op by yielding to the event loop, and
# the whole interpreter call stack rides along on the fiber's stack. No CPS/state
# machine transform (unlike Clojure's `go` macro), so <! / >! work anywhere
# (inside try, nested fns, loops, …).
#
# A channel is a pair of Janet ev/chans wrapped in a tagged table: a `:ch` that
# carries values and a `:done` that is closed to signal channel close. A take is
# `(ev/select :ch :done)` — ev/select checks in order, so buffered values drain
# before the close signal is seen, giving Clojure's drain-then-nil semantics. We
# use a separate `:done` channel because Janet's ev/chan-close *discards* a
# channel's buffered values. close! just closes :done (idempotent, no fiber), so
# nothing leaks.
#
# Single OS thread: go blocks run cooperatively on the event loop, so <! (park)
# and <!! (block) are the same here.
(use ./types)
(use ./pv)
(use ./config)
(defn jolt-chan? [x] (and (table? x) (= :jolt/chan (get x :jolt/type))))
(defn- wrap [vc dc] @{:jolt/type :jolt/chan :ch vc :done dc :closed @[false]})
(defn- vchan [x]
(if (jolt-chan? x) (x :ch) (error (string "expected a channel, got " (type x)))))
(defn- reduced? [x] (and (table? x) (= :jolt/reduced (get x :jolt/type))))
# Buffer specs: (buffer n) fixed, (dropping-buffer n) drops new values when full,
# (sliding-buffer n) drops the oldest when full.
(defn async-buffer [n] @{:jolt/type :jolt/buffer :kind :fixed :n n})
(defn async-dropping-buffer [n] @{:jolt/type :jolt/buffer :kind :dropping :n n})
(defn async-sliding-buffer [n] @{:jolt/type :jolt/buffer :kind :sliding :n n})
(defn- buffer-spec? [x] (and (table? x) (= :jolt/buffer (get x :jolt/type))))
# An always-ready channel, used as the non-blocking fallback in ev/select so a
# give can detect "buffer full" without parking.
(def- full-signal (let [c (ev/chan)] (ev/chan-close c) c))
# Put one value into the channel's value chan honoring its buffer kind. Returns
# true (the put "succeeds" even when dropped, like Clojure's dropping/sliding).
(defn- buf-give [ch v]
(case (ch :bufkind)
:dropping (do (ev/select [(ch :ch) v] full-signal) true) # give if room, else drop
:sliding (let [r (ev/select [(ch :ch) v] full-signal)]
(when (= :close (in r 0)) # full: drop oldest, then add
(protect (ev/take (ch :ch))) (protect (ev/give (ch :ch) v)))
true)
(if (in (protect (ev/give (ch :ch) v)) 0) true false))) # fixed/unbuffered: may park
# A channel transducer is applied on the put side. We build a reducing fn whose
# step gives each output value into the channel (honoring its buffer kind); the
# accumulator is the chan, threaded through but unused (output is the
# side-effecting give). A jolt transducer/rf is a jolt closure, directly
# callable as a Janet function.
(defn- make-add-rf [w]
(fn [& args]
(case (length args)
0 (w :ch) # init
1 (in args 0) # completion: nothing extra to do
(do (buf-give w (in args 1)) (in args 0))))) # step: give output
# (chan) unbuffered; (chan n) / (chan (buffer n)) fixed; (chan (dropping-buffer
# n)) / (chan (sliding-buffer n)); a 2nd arg transducer composes over the buffer.
(defn async-chan [&opt buf xform]
(def spec (cond
(buffer-spec? buf) buf
(and (number? buf) (> buf 0)) {:kind :fixed :n buf}
nil))
(def vc (if spec (ev/chan (spec :n)) (ev/chan)))
(def w (wrap vc (ev/chan)))
(when spec (put w :bufkind (spec :kind)))
(when (and xform (not (nil? xform)))
(put w :xrf (xform (make-add-rf w))))
w)
(defn async-close! [ch]
(when (not (in (ch :closed) 0))
(put (ch :closed) 0 true)
# flush any buffered state of a stateful transducer (completion arity)
(when (ch :xrf) (protect ((ch :xrf) (ch :ch))))
(protect (ev/chan-close (ch :done))))
nil)
# <! / <!! — take, parking the fiber. Drains buffered values, then returns nil
# once the channel is closed and empty.
(defn async-take [ch]
(def r (ev/select (ch :ch) (ch :done)))
(if (= :take (in r 0)) (in r 2) nil))
# >! / >!! — put, parking the fiber. Returns true if delivered, false if the
# channel is closed. nil may not be put on a channel (it is the closed value).
# With a transducer, the value is run through it (so one put may yield zero or
# more values on the channel); a `reduced` result (e.g. from `take`) closes it.
(defn async-give [ch v]
(when (nil? v) (error "Can't put nil on a channel"))
(cond
(in (ch :closed) 0) false
(ch :xrf)
(let [r ((ch :xrf) (ch :ch) v)]
(when (reduced? r) (async-close! ch))
true)
(buf-give ch v)))
# Run thunk (a jolt 0-arg closure, directly callable) in a fiber; return a
# buffered(1) channel that conveys its value once, then closes. A nil result
# just closes. Buffered(1) so a fire-and-forget go leaves no parked fiber.
#
# The dynamic-var bindings in effect at spawn time are conveyed into the fiber
# (Clojure binding conveyance): we snapshot them here (on the spawning fiber)
# and install a private copy inside the new fiber before running the body.
(defn async-go-spawn [thunk]
(def snap (snapshot-bindings))
(def w (async-chan 1))
(ev/go (fn []
(install-bindings snap)
(def res (protect (thunk)))
(when (and (in res 0) (not (nil? (in res 1))))
(async-give w (in res 1)))
(async-close! w)))
w)
# (alts! [ch ...]) — take from whichever channel is ready first; returns
# [value channel] (value is nil if that channel closed). Take-only for v1.
(defn async-alts [chans]
(def cs (cond (pvec? chans) (pv->array chans)
(tuple? chans) chans
(array? chans) chans
(error "alts! expects a vector of channels")))
(def raws @[])
(def lookup @{}) # raw ev/chan -> [jolt-chan done?]
(each c cs
(array/push raws (c :ch)) (put lookup (c :ch) [c false])
(array/push raws (c :done)) (put lookup (c :done) [c true]))
(def r (ev/select ;raws))
(def info (get lookup (in r 1)))
(def jc (in info 0))
(def val (if (or (in info 1) (not= :take (in r 0))) nil (in r 2)))
(pv-from-indexed @[val jc]))
# (timeout ms) — a channel that closes after ms milliseconds.
(defn async-timeout [ms]
(def w (async-chan))
(ev/go (fn [] (ev/sleep (/ ms 1000)) (async-close! w)))
w)
# (put! ch v [cb]) — async put; (take! ch cb) — async take. Fire a fiber and
# call the optional callback with the result.
(defn async-put! [ch v &opt cb]
(ev/go (fn []
(def ok (async-give ch v))
(when (and cb (not (nil? cb))) (cb ok))))
nil)
(defn async-take! [ch cb]
(ev/go (fn []
(def val (async-take ch))
(when (and cb (not (nil? cb))) (cb val))))
nil)
# --- macros (Janet macro-fns that return forms) ---
(defn- sym [name &opt ns] {:jolt/type :symbol :ns ns :name name})
# (go body...) -> (go-spawn (fn* [] body...))
(defn async-go [& body]
@[(sym "go-spawn" "clojure.core.async") (array (sym "fn*") [] ;body)])
# (go-loop bindings body...) -> (go (loop bindings body...))
(defn async-go-loop [bindings & body]
@[(sym "go" "clojure.core.async") (array (sym "loop") bindings ;body)])
# (thread body...) — runs cooperatively in a fiber here (no OS thread); same
# shape as go (returns a result channel).
(defn async-thread [& body]
@[(sym "go-spawn" "clojure.core.async") (array (sym "fn*") [] ;body)])
(def- async-bindings
@{"chan" async-chan
"chan?" jolt-chan?
"close!" async-close!
"<!" async-take "<!!" async-take
">!" async-give ">!!" async-give
"alts!" async-alts "alts!!" async-alts
"timeout" async-timeout
"buffer" async-buffer
"dropping-buffer" async-dropping-buffer
"sliding-buffer" async-sliding-buffer
"put!" async-put!
"take!" async-take!
"go-spawn" async-go-spawn
"go" async-go
"go-loop" async-go-loop
"thread" async-thread})
(def- async-macros @{"go" true "go-loop" true "thread" true})
(defn install-async!
"Create/populate the clojure.core.async namespace in ctx."
[ctx]
(let [ns (ctx-find-ns ctx "clojure.core.async")]
(loop [[name f] :pairs async-bindings]
(def v (ns-intern ns name f))
(when (get async-macros name) (put v :macro true)))
ns))

File diff suppressed because it is too large Load diff

View file

@ -1,353 +0,0 @@
# Native code generation: jolt IR -> C, for hot numeric-leaf fns (jolt-ihdp, the
# lever-1 native-codegen tier of epic jolt-5vsp). The spike
# (docs/foundational-runtime-lever1-native-codegen.md) showed native-C compute
# beats the Janet-VM floor (~18-22x faster than bytecode, edges out JVM Clojure),
# and that compiling a hot LEAF fn to C — called from a bytecode loop — captures
# the win because the forward (bytecode -> C) crossing is nearly free.
#
# This module is the IR -> C translator + a cc/load driver. It is a standalone
# library: nothing wires it into the default compile path yet (that — detecting
# hot fns and installing the C version onto the var cell — is the next step). A
# fn is a candidate only if its whole body is numeric (params/locals/return are
# numbers, all calls are native-op arithmetic): then everything stays in C
# `double`s, unboxed at entry and reboxed at return, with no Janet in the hot loop.
(import ./core_types :as ct)
(import ./phm :as phm)
(defn- norm-node [n] (if (phm/phm? n) (phm/phm-to-struct n) n))
# jolt core fn name -> {:c "<C operator>" :kind :infix|:incdec}. The subset of
# backend/native-ops that maps to a C double operator with matching semantics.
# (mod/rem/bit-ops/min/max deliberately omitted for now — they need helper calls
# or differ from C operators; add them as the candidate grammar grows.)
(def- c-ops
{"+" {:c "+" :kind :infix} "-" {:c "-" :kind :infix}
"*" {:c "*" :kind :infix} "/" {:c "/" :kind :infix}
"<" {:c "<" :kind :infix} ">" {:c ">" :kind :infix}
"<=" {:c "<=" :kind :infix} ">=" {:c ">=" :kind :infix}
"=" {:c "==" :kind :infix}
"inc" {:c "+" :kind :incdec} "dec" {:c "-" :kind :incdec}})
(defn- args-of [node] (ct/vview (node :args)))
# A native-op invoke -> its c-ops entry, else nil (the head must be a clojure.core
# ref to a supported primitive at the right arity).
(defn- op-for [node]
(def n (norm-node node))
(def f (and (= :invoke (n :op)) (norm-node (n :fn))))
(and f (= :var (f :op)) (= "clojure.core" (f :ns))
(let [op (get c-ops (f :name))
nargs (length (args-of n))]
(and op
(if (= (op :kind) :incdec) (= 1 nargs) (= 2 nargs))
op))))
# --- candidate check: every node in the body is C-translatable ---
(var- numeric-ok? nil)
(defn- bindings-ok? [node]
(all (fn [pair] (numeric-ok? (in (ct/vview pair) 1))) (ct/vview (node :bindings))))
(set numeric-ok?
(fn numeric-ok? [raw]
(def node (norm-node raw))
(case (node :op)
:const (number? (node :val))
:local true
:if (and (numeric-ok? (node :test)) (numeric-ok? (node :then)) (numeric-ok? (node :else)))
:do (and (all numeric-ok? (ct/vview (node :statements))) (numeric-ok? (node :ret)))
:let (and (bindings-ok? node) (numeric-ok? (node :body)))
:loop (and (bindings-ok? node) (numeric-ok? (node :body)))
:recur (all numeric-ok? (args-of node))
:invoke (and (op-for node) (all numeric-ok? (args-of node)))
false)))
(defn fn-ir-of
"Unwrap a :def-of-fn or a bare :fn IR node to the :fn node, or nil."
[ir]
(def n (norm-node ir))
(def init (norm-node (get n :init n)))
(and (= :fn (init :op)) init))
(defn numeric-leaf?
"True when ir is a single-fixed-arity fn whose entire body is C-translatable
(numeric in, numeric out, only native-op arithmetic)."
[ir]
(def fnode (fn-ir-of ir))
(and fnode
(let [ars (ct/vview (fnode :arities))]
(and (= 1 (length ars))
(not ((first ars) :rest))
(numeric-ok? ((first ars) :body))))))
# --- C generation ---
# scope: jolt-local-name (string) -> C var name. Fresh C names sidestep charset
# and keyword collisions. rctx (recur target): {:vars [c-names] :flag flag-name}.
(defn- new-namer []
(def counter @[0])
(fn [] (def n (in counter 0)) (put counter 0 (+ n 1)) (string "v" n)))
(defn- fmtnum [x]
(def s (string/format "%.17g" (* 1.0 x)))
(if (or (string/find "." s) (string/find "e" s) (string/find "n" s)) s (string s ".0")))
(var- c-expr nil)
(set c-expr
(fn c-expr [raw scope]
(def node (norm-node raw))
(case (node :op)
:const (fmtnum (node :val))
:local (or (get scope (node :name)) (error (string "cgen: unbound local " (node :name))))
:invoke (let [op (op-for node) as (args-of node)]
(if (= (op :kind) :incdec)
(string "(" (c-expr (in as 0) scope) " " (op :c) " 1)")
(string "(" (c-expr (in as 0) scope) " " (op :c) " " (c-expr (in as 1) scope) ")")))
(error (string "cgen: non-pure op in expr position: " (node :op))))))
(defn- pure? [node]
(case (node :op) :const true :local true :invoke (truthy? (op-for node)) false))
# emit-to: append C statements that compute node's (double) value into dest.
# Control-flow forms (if/do/let/loop/recur) lower to statements; pure nodes to a
# single assignment. A comparison yields C int 0/1, which is a fine double here.
(var- emit-to nil)
(set emit-to
(fn emit-to [raw dest scope rctx out namer]
(def node (norm-node raw))
(case (node :op)
:if (let [t (namer)]
(buffer/push out "double " t ";\n")
(emit-to (node :test) t scope rctx out namer)
(buffer/push out "if (" t " != 0.0) {\n")
(emit-to (node :then) dest scope rctx out namer)
(buffer/push out "} else {\n")
(emit-to (node :else) dest scope rctx out namer)
(buffer/push out "}\n"))
:do (do (each s (ct/vview (node :statements))
(def junk (namer)) (buffer/push out "double " junk ";\n")
(emit-to s junk scope rctx out namer))
(emit-to (node :ret) dest scope rctx out namer))
:let (let [scope2 (table/clone scope)]
(each pair (ct/vview (node :bindings))
(def p (ct/vview pair))
(def cv (namer))
(buffer/push out "double " cv ";\n")
(emit-to (in p 1) cv scope2 rctx out namer)
(put scope2 (string (in p 0)) cv))
(emit-to (node :body) dest scope2 rctx out namer))
:loop (let [scope2 (table/clone scope) cvars @[] flag (namer)]
(each pair (ct/vview (node :bindings))
(def p (ct/vview pair))
(def cv (namer))
(buffer/push out "double " cv ";\n")
(emit-to (in p 1) cv scope2 rctx out namer)
(put scope2 (string (in p 0)) cv)
(array/push cvars cv))
(buffer/push out "int " flag " = 1;\n")
(buffer/push out "while (" flag ") {\n" flag " = 0;\n")
(emit-to (node :body) dest scope2 {:vars cvars :flag flag} out namer)
(buffer/push out "}\n"))
# recur: compute new values into temps first (avoid clobbering loop vars
# mid-update), then assign and set the continue flag. Produces no value —
# the enclosing while re-runs.
:recur (let [as (args-of node) tmps @[]]
(each a as
(def tv (namer))
(buffer/push out "double " tv ";\n")
(emit-to a tv scope rctx out namer)
(array/push tmps tv))
(for i 0 (length tmps)
(buffer/push out (in (rctx :vars) i) " = " (in tmps i) ";\n"))
(buffer/push out (rctx :flag) " = 1;\n"))
(if (pure? node)
(buffer/push out dest " = " (c-expr node scope) ";\n")
(error (string "cgen: unsupported op " (node :op)))))))
(defn- sanitize [name]
(def b @"")
(each c name
(buffer/push b (if (or (and (>= c 97) (<= c 122)) (and (>= c 65) (<= c 90))
(and (>= c 48) (<= c 57)) (= c 95)) c 95)))
(string b))
# Emit just the `static Janet cfun_<c-name>(...) { ... }` definition for one fn
# into buf. Shared by the single-fn and multi-fn (AOT module) generators.
(defn- emit-cfun [buf ir c-name]
(def fnode (fn-ir-of ir))
(def ar (first (ct/vview (fnode :arities))))
(def params (map string (ct/vview (ar :params))))
(def namer (new-namer))
(def scope @{})
(buffer/push buf "static Janet cfun_" c-name "(int32_t argc, Janet *argv) {\n")
(buffer/push buf "janet_fixarity(argc, " (string (length params)) ");\n")
(eachp [i pn] params
(def cv (namer))
(buffer/push buf "double " cv " = janet_getnumber(argv, " (string i) ");\n")
(put scope pn cv))
(def ret (namer))
(buffer/push buf "double " ret ";\n")
(emit-to (ar :body) ret scope nil buf namer)
(buffer/push buf "return janet_wrap_number(" ret ");\n}\n"))
(defn gen-c-module
"entries: [{:sym <C identifier> :ir <numeric-leaf fn IR>} ...] -> C source for
ONE Janet native module exporting every entry's :sym as a cfunction. This is
the AOT/build-time shape (jolt-a7ds): all of an app's hot fns in a single .so,
compiled once at build time. Assumes each :ir is a numeric leaf."
[entries]
(def buf @"")
(buffer/push buf "#include <janet.h>\n")
(each e entries (emit-cfun buf (e :ir) (e :sym)))
(buffer/push buf "static const JanetReg cfuns[] = {\n")
(each e entries
(buffer/push buf " {\"" (e :sym) "\", cfun_" (e :sym) ", NULL},\n"))
(buffer/push buf " {NULL, NULL, NULL}};\n")
(buffer/push buf "JANET_MODULE_ENTRY(JanetTable *env) { janet_cfuns(env, \"cg\", cfuns); }\n")
(string buf))
(defn gen-c-fn
"ir (a numeric-leaf :def-of-fn or :fn node), c-name -> C source for a Janet
native module exporting `c-name` as a cfunction. Assumes (numeric-leaf? ir)."
[ir c-name]
(gen-c-module [{:sym c-name :ir ir}]))
# --- toolchain ---
(defn header-dir
"Directory containing janet.h, probed from syspath + common prefixes, or nil."
[]
(def sp (dyn :syspath))
(def cands @[(string sp "/../../include") "/opt/homebrew/include"
"/usr/local/include" "/usr/include"])
(var found nil)
(each c cands (when (and (not found) (os/stat (string c "/janet.h"))) (set found c)))
found)
(defn toolchain-available?
"True when a C compiler and janet.h are present (so compile-fn can work). Never
throws: a missing cc (not on PATH) makes os/execute raise, so guard it — a
toolchain-less deploy target must get false, not a crash."
[]
(and (header-dir)
(let [r (protect (os/execute ["cc" "--version"] :p
{:out (file/open "/dev/null" :w)
:err (file/open "/dev/null" :w)}))]
(and (r 0) (= 0 (r 1))))))
(defn- mkdirs [path]
(def parts (filter |(not (empty? $)) (string/split "/" path)))
(var acc (if (string/has-prefix? "/" path) "" "."))
(each p parts (set acc (string acc "/" p)) (os/mkdir acc)))
(defn- cache-dir []
# Persistent across runs (like the ctx image cache, which also defaults to
# TMPDIR). Override with JOLT_CGEN_CACHE_DIR.
(string (or (os/getenv "JOLT_CGEN_CACHE_DIR") (os/getenv "TMPDIR") "/tmp") "/jolt-cgen"))
(defn- cc-compile [cpath sopath]
# macOS needs -undefined dynamic_lookup so the module's janet_* refs resolve
# against the host at load time; ELF (Linux/BSD) resolves them then anyway.
(def cmd @["cc" "-shared" "-fPIC" "-O2" (string "-I" (header-dir))])
(when (= :macos (os/which)) (array/push cmd "-undefined") (array/push cmd "dynamic_lookup"))
(array/push cmd cpath "-o" sopath)
(def r (os/execute cmd :p))
(unless (= 0 r) (error (string "cgen: cc failed (" r ") for " cpath))))
(defn- abspath [p] (if (string/has-prefix? "/" p) p (string (os/cwd) "/" p)))
# Build a .so from C source, content-addressed and cached. The C + Janet ABI +
# platform fully determine the .so, so a matching cache entry is always valid.
# Returns the absolute .so path. JOLT_CGEN_NO_CACHE=1 forces a rebuild.
(defn- build-so [src dir]
(def stamp (string src "|" janet/version "-" janet/build "|" (os/which)))
(def key (string (band (hash stamp) 0x7FFFFFFF) "-" (length src)))
(mkdirs dir)
(def base (string dir "/cg-" key))
(def sopath (string base ".so"))
(def abs (abspath sopath))
(unless (and (not (os/getenv "JOLT_CGEN_NO_CACHE")) (os/stat abs))
(def cpath (string base ".c"))
(spit cpath src)
(cc-compile cpath sopath))
abs)
(defn load-module
"Load a prebuilt cgen .so (NO cc — works on a target with no toolchain) and
return a table of symbol-string -> cfunction for every cfunction it exports.
This is the runtime side of the build-time/AOT path (jolt-a7ds)."
[sopath]
(def env @{})
(native (abspath sopath) env)
(def out @{})
(eachp [k v] env
(when (and (symbol? k) (table? v) (or (cfunction? (v :value)) (function? (v :value))))
(put out (string k) (v :value))))
out)
(defn compile-module
"AOT path: compile MANY numeric-leaf fns into ONE native module, once, at build
time. entries: [{:sym <C id> :ir <numeric-leaf IR>} ...]. Returns
{:sopath <abs .so> :fns {sym -> cfunction}}, or nil if the toolchain is absent
or any entry isn't a numeric leaf. opts: :dir (cache dir)."
[entries &opt opts]
(default opts {})
(when (and (header-dir) (all |(numeric-leaf? ($ :ir)) entries))
(def src (gen-c-module entries))
(def sopath (build-so src (get opts :dir (cache-dir))))
{:sopath sopath :fns (load-module sopath)}))
(defn compile-fn
"Compile a single numeric-leaf fn IR to a native Janet cfunction. Returns the
cfunction, or nil if ir isn't a candidate or the toolchain is unavailable.
opts: :dir (cache dir, default a persistent jolt-cgen under TMPDIR), :name (C
identifier base). Content-addressed/cached like compile-module."
[ir &opt opts]
(default opts {})
(when (and (numeric-leaf? ir) (header-dir))
(def c-name (sanitize (get opts :name "jolt_cfn")))
(def src (gen-c-fn ir c-name))
(def sopath (build-so src (get opts :dir (cache-dir))))
(def env @{})
(native sopath env)
((get env (symbol c-name)) :value)))
# --- AOT build/deploy (jolt-a7ds) ---------------------------------------------
# Build phase (needs cc): collect an app's numeric-leaf fns (the backend's
# :cgen-collect? mode fills (ctx :env) :cgen-collected with [{:ns :name :ir}]),
# compile them all into ONE native module, and write a manifest. Deploy phase
# (NO cc, target needs no toolchain): load the prebuilt module + manifest into a
# qname->cfunction map that the backend installs as var roots (:cgen-prebuilt).
(defn aot-build
"collected: [{:ns :name :ir} ...] (numeric-leaf fns). Compile them into one
native module and return {:sopath <abs .so> :entries [{:ns :name :sym}]}, or
nil (toolchain absent / a non-leaf / empty). opts: :dir (cache dir).
Syms are positional (f0, f1, ...) so they're collision-free."
[collected &opt opts]
(default opts {})
(when (and (header-dir) (not (empty? collected)))
(def entries @[])
(def manifest @[])
(eachp [i e] collected
(def sym (string "f" i))
(array/push entries {:sym sym :ir (e :ir)})
(array/push manifest {:ns (e :ns) :name (e :name) :sym sym}))
(def m (compile-module entries opts))
(and m {:sopath (m :sopath) :entries manifest})))
(defn write-manifest
"Persist an aot-build result to path as jdn (the deploy side reads it)."
[path build]
(spit path (string/format "%j" build)))
(defn load-aot
"Deploy side (NO cc): read a manifest written by write-manifest, load its
prebuilt .so, and return a qname (\"ns/name\") -> cfunction map suitable for
(ctx :env) :cgen-prebuilt. nil if the manifest/.so is missing."
[manifest-path]
(when (os/stat manifest-path)
(def build (parse (slurp manifest-path)))
(def fns (load-module (build :sopath)))
(def out @{})
(each e (build :entries)
(when-let [f (get fns (e :sym))]
(put out (string (e :ns) "/" (e :name)) f)))
out))

View file

@ -1,211 +0,0 @@
# Single-binary native build (jolt-a7ds, epic jolt-5vsp). The AOT path
# (cgen.janet) compiles an app's numeric-leaf fns into one prebuilt .so + a
# manifest, loaded at deploy with no cc. This module fuses that native code into
# the jpm-built executable so an app ships as ONE static file: no sidecar .so, no
# toolchain on the target.
#
# Shape (validated by the spike, docs/foundational-runtime-lever1-native-codegen.md):
# a staging dir with `src`/`jolt-core` symlinks into the jolt source tree, the
# generated cg.c (native module of the app's hot fns), an uberscript-style bundle
# of the app source, and an entry that bakes the jolt runtime, installs the native
# fns as var roots (:cgen-prebuilt), and runs -main. `jpm build` static-links cg.a
# (declare-native builds it) into the final exe. Build needs cc; the result does
# not. See build-app for the driver and `jolt cgen-build` (main.janet) for the CLI.
(import ./api :as api)
(import ./types :as t)
(import ./cgen :as cgen)
# --- jolt source location ----------------------------------------------------
# Baking the runtime into the app binary needs the jolt source tree (src/jolt +
# jolt-core). The shipped binary doesn't carry its own source, so locate a
# checkout: JOLT_HOME wins, else walk up from cwd for the marker files.
(defn- jolt-home? [d]
(and (os/stat (string d "/src/jolt/api.janet"))
(os/stat (string d "/jolt-core"))))
(defn find-jolt-home
"Directory of a jolt source checkout (has src/jolt + jolt-core), or nil.
JOLT_HOME overrides; otherwise walk up from `start` (default cwd)."
[&opt start]
(def env-home (os/getenv "JOLT_HOME"))
(cond
(and env-home (jolt-home? env-home)) env-home
(do
(var d (or start (os/cwd)))
(var found nil)
(while (and (not found) (> (length d) 1))
(if (jolt-home? d)
(set found d)
(set d (string/slice d 0 (max 0 (last (string/find-all "/" d)))))))
found)))
# --- generation --------------------------------------------------------------
(defn gen-c-and-manifest
"collected: [{:ns :name :ir} ...] (numeric-leaf fns from a :cgen-collect? load).
Emit the C source for ONE native module exporting them all, plus a positional
manifest [{:ns :name :sym} ...] mapping each app fn to its C symbol. Mirrors
cgen/aot-build but yields C source (compiled at build) rather than a .so."
[collected]
(def entries @[])
(def manifest @[])
(eachp [i e] collected
(def sym (string "f" i))
(array/push entries {:sym sym :ir (e :ir)})
(array/push manifest {:ns (e :ns) :name (e :name) :sym sym}))
{:c-src (cgen/gen-c-module entries) :manifest manifest})
(defn bundle-source
"Concatenate the app's loaded source files (load order: deps before dependents)
into one closed-world bundle the baked entry can eval. Verbatim — DCE is the
uberscript path's concern, not needed here for correctness."
[files]
(def buf @"")
(each f files
(buffer/push-string buf "; --- " f " ---\n" (slurp f) "\n"))
(string buf))
(defn collect-app
"Load main-ns through the full pipeline with :cgen-collect? on (records the
numeric-leaf fns' IR without swapping roots), returning the collected fns and
the source files loaded (for the bundle). source-roots seed :source-paths so
the app + its deps resolve. Throws on a load error."
[main-ns source-roots]
(def ctx (api/init-cached {:compile? true})) # discarded; cached for speed
(each r source-roots
(when (> (length r) 0) (array/push (get (ctx :env) :source-paths) r)))
(put (ctx :env) :direct-linking? true)
(put (ctx :env) :cgen-collect? true)
(put (ctx :env) :loaded-files @[])
(api/load-string ctx (string "(require '[" main-ns "])") "<cgen-build>")
(def seen @{})
(def files @[])
(each f (get (ctx :env) :loaded-files)
(unless (get seen f) (put seen f true) (array/push files f)))
{:collected (or (get (ctx :env) :cgen-collected) @[]) :files files})
(defn run-main!
"Runtime helper baked into a generated app binary: bind *command-line-args* to
args (a tuple/array of strings) and invoke main-ns/-main."
[ctx main-ns args]
(def core (t/ctx-find-ns ctx "clojure.core"))
(t/ns-intern core "*command-line-args*" (tuple/slice (tuple ;args)))
(api/eval-string ctx (string "(apply " main-ns "/-main *command-line-args*)")))
(defn gen-entry-source
"The generated main.janet: imports the jolt runtime + the static cg module,
builds qname->cfunction from the manifest, bakes the ctx with the app compiled
and the native fns installed as roots, and runs -main at startup. Strings are
emitted with %j so embedded source/manifest can't break the quoting."
[main-ns app-source manifest]
(string
"# Generated by `jolt cgen-build` — do not edit.\n"
"(import \"./src/jolt/api\" :as api)\n"
"(import \"./src/jolt/cgen_build\" :as cb)\n"
"(import \"./cg\" :as cg)\n\n"
"(def manifest " (string/format "%j" manifest) ")\n"
"(def app-source " (string/format "%j" app-source) ")\n"
"(def main-ns " (string/format "%j" main-ns) ")\n\n"
"(def cg-env (require \"./cg\"))\n"
"(def prebuilt\n"
" (let [tb @{}]\n"
" (each e manifest\n"
" (when-let [cell (get cg-env (symbol (e :sym)))]\n"
" (put tb (string (e :ns) \"/\" (e :name)) (get cell :value))))\n"
" tb))\n\n"
"(def ctx (api/init {:compile? true}))\n"
"(put (ctx :env) :direct-linking? true)\n"
"(put (ctx :env) :cgen-prebuilt prebuilt)\n"
"(api/load-string ctx app-source \"app.clj\")\n\n"
"(defn main [&]\n"
" (def args (or (dyn :args) @[]))\n"
" (cb/run-main! ctx main-ns (slice args 1)))\n"))
(defn gen-project-source
"The generated project.janet. The executable's entry imports cg at build
(dofile) and static-links cg.a, so both must build first — jpm infers neither,
and cg.a isn't pulled into a non-release `build` task, so wire deps explicitly."
[exe-name]
(string
"(declare-project :name \"jolt-cgen-app\")\n\n"
"(declare-native :name \"cg\" :source @[\"cg.c\"])\n\n"
"(declare-executable :name " (string/format "%j" exe-name) " :entry \"main.janet\")\n\n"
"(add-dep \"./" exe-name "\" \"./cg.so\")\n"
"(add-dep \"./" exe-name "\" \"./cg.a\")\n"))
# --- driver ------------------------------------------------------------------
(defn- mkdir-p [path]
(def parts (filter |(not (empty? $)) (string/split "/" path)))
(var acc (if (string/has-prefix? "/" path) "" "."))
(each p parts (set acc (string acc "/" p)) (os/mkdir acc)))
(defn- rm-rf [path]
# lstat, never stat: the staging dir holds symlinks INTO the jolt tree
# (src, jolt-core); following them would delete real source. A symlink — even
# one pointing at a directory — is removed with os/rm, not recursed into.
(when-let [st (os/lstat path)]
(if (= :directory (st :mode))
(do (each e (os/dir path) (rm-rf (string path "/" e))) (os/rmdir path))
(os/rm path))))
(defn build-app
"Build main-ns into a single static executable at :out. Needs cc + jpm at build
time; the result needs neither. opts:
:main app namespace (string, required)
:out output binary path (required)
:jolt-home jolt source checkout (default: find-jolt-home)
:source-roots extra source roots for the app + deps (default from JOLT_PATH)
:stage staging dir (default: a fresh temp dir; removed unless :keep)
:keep keep the staging dir for inspection
Returns {:out <path> :native-count <n> :stage <dir>}."
[opts]
(def main-ns (assert (opts :main) "cgen-build: :main namespace required"))
(def out (assert (opts :out) "cgen-build: :out path required"))
(def home (or (opts :jolt-home) (find-jolt-home)))
(assert home "cgen-build: could not locate jolt source (set JOLT_HOME)")
(def source-roots
(or (opts :source-roots)
(let [jp (os/getenv "JOLT_PATH")]
(if jp (filter |(> (length $) 0) (string/split ":" jp)) @[]))))
(def exe-name "app")
# 1. Collect the app's numeric-leaf fns + its full source bundle.
(def {:collected collected :files files} (collect-app main-ns source-roots))
(def {:c-src c-src :manifest manifest} (gen-c-and-manifest collected))
(def app-source (bundle-source files))
# 2. Stage: symlinks into the jolt tree + the generated build inputs.
(def stage
(or (opts :stage)
(string (or (os/getenv "TMPDIR") "/tmp") "/jolt-cgen-build-"
(string/format "%x" (band (hash (string main-ns out)) 0x7fffffff)))))
(rm-rf stage)
(mkdir-p stage)
(os/symlink (string home "/src") (string stage "/src"))
(os/symlink (string home "/jolt-core") (string stage "/jolt-core"))
(spit (string stage "/cg.c") c-src)
(spit (string stage "/main.janet") (gen-entry-source main-ns app-source manifest))
(spit (string stage "/project.janet") (gen-project-source exe-name))
# 3. jpm build in the staging dir (cwd + JANET_BUILDPATH=. so artifacts land
# beside the entry, where ./cg and cg.a resolve).
(def prev-cwd (os/cwd))
(def prev-bp (os/getenv "JANET_BUILDPATH"))
(defer (do (os/cd prev-cwd) (os/setenv "JANET_BUILDPATH" prev-bp))
(os/cd stage)
(os/setenv "JANET_BUILDPATH" ".")
# --workers=1: the build shells out to cc; one worker keeps it from saturating
# every core (it runs inside the parallel test gate, where that starves
# siblings into timeouts). Only a few small C files, so the cost is marginal.
(def code (os/execute ["jpm" "--workers=1" "build"] :p))
(os/cd prev-cwd)
(unless (= 0 code) (error (string "cgen-build: jpm build failed (" code ")"))))
# 4. Install the binary, then clean up staging.
(def built (string stage "/" exe-name))
(assert (os/stat built) (string "cgen-build: expected binary at " built))
(let [od (string/slice out 0 (max 0 (last (or (string/find-all "/" out) [0]))))]
(when (> (length od) 0) (mkdir-p od)))
(spit out (slurp built))
(os/chmod out 8r755)
(unless (opts :keep) (rm-rf stage))
{:out out :native-count (length manifest) :stage stage})

View file

@ -1,76 +0,0 @@
# Build-time collection mode.
#
# Jolt can be built with either immutable (persistent) collections — proper
# Clojure value semantics — or fast Janet-native mutable collections.
#
# jpm build # immutable (default)
# JOLT_MUTABLE=1 jpm build # mutable
#
# This reads the environment at module-load time, so for a jpm-compiled
# executable the value is fixed when the binary is built (a true compile flag).
# `mutable?` is a constant, so the type-mode branches throughout core fold away.
(def mutable? (= "1" (os/getenv "JOLT_MUTABLE")))
# Convenience: immutable? is the default.
(def immutable? (not mutable?))
# ---------------------------------------------------------------------------
# Run-mode + cache-key policy (jolt-q5ql). Lifted out of main so it is unit-
# testable without the CLI and so the disk-cache keys share ONE canonical list
# of ctx-shaping env vars (a positional %q key silently misaligned if a knob was
# added in only one place — the footgun this fixes).
# ---------------------------------------------------------------------------
# Every env var that shapes the built/optimized ctx. Both image caches key on
# this exact list, so adding a knob here updates every cache key at once.
(def ctx-shaping-env-vars
["JOLT_PATH" "JOLT_APP_PATHS" "JOLT_MUTABLE" "JOLT_AOT_CORE" "JOLT_FEATURES"
"JOLT_INTERPRET" "JOLT_INTERPRET_MACROS" "JOLT_DIRECT_LINK"
"JOLT_NO_DIRECT_LINK" "JOLT_OPTIMIZE" "JOLT_WHOLE_PROGRAM"
"JOLT_NO_WHOLE_PROGRAM" "JOLT_SHAPE" "JOLT_NO_SHAPE"
"JOLT_NO_IR_PASSES" "JOLT_CHECK_HINTS" "JOLT_CGEN"])
(defn ctx-cache-key
"Build a disk-cache key from labeled prefix pairs plus the value of every
ctx-shaping env var. Each component is tagged by name (`label=value`), so a
new knob can't positionally alias two different builds. `prefix` is a flat
array of label,value,label,value,... (e.g. version + entry ns + opts)."
[prefix]
(def parts @[])
(var i 0)
(while (< i (length prefix))
(array/push parts (string/format "%s=%q" (in prefix i) (in prefix (+ i 1))))
(+= i 2))
(each ev ctx-shaping-env-vars
(array/push parts (string/format "%s=%q" ev (os/getenv ev))))
(string/join parts "|"))
(defn resolve-run-mode
"Resolve the compile/optimization knobs from argv-derived flags + env.
`open-mode?` = an interactive/non-program invocation (repl/-e/help/uberscript);
`main-entry?` = a -m/-M program entry. Returns the ctx env knob map that main
installs. Explicit env always wins (JOLT_NO_DIRECT_LINK / JOLT_DIRECT_LINK)."
[open-mode? main-entry?]
(def dl-forced
(cond (os/getenv "JOLT_NO_DIRECT_LINK") :off
(= "1" (os/getenv "JOLT_DIRECT_LINK")) :on
:none))
(def dl (case dl-forced :off false :on true (not open-mode?)))
# Inference/specialization is the expensive part — default OFF, opt in with
# JOLT_OPTIMIZE (or an explicit JOLT_DIRECT_LINK, which signals a full build).
(def optimize?
(and dl (or (not (nil? (os/getenv "JOLT_OPTIMIZE")))
(= "1" (os/getenv "JOLT_DIRECT_LINK")))))
{:direct-linking? dl
:inline? optimize?
# auto-enabled (vs explicitly requested) — suppresses the checker default-on.
:direct-link-auto? (and dl (= dl-forced :none))
:shapes? (and dl (not (os/getenv "JOLT_NO_SHAPE")))
:map-shapes? (and (os/getenv "JOLT_SHAPE") (not (os/getenv "JOLT_NO_SHAPE")))
:whole-program? (and optimize?
(not (os/getenv "JOLT_NO_WHOLE_PROGRAM"))
(or main-entry? (os/getenv "JOLT_WHOLE_PROGRAM")))
# Native codegen (jolt-ihdp): compile numeric-leaf fns to C at def time and
# install the cfunction as the var root, so callers direct-link to native
# code. Opt-in (cc latency per fn); needs direct-linking so callers embed it.
:cgen? (and dl (= "1" (os/getenv "JOLT_CGEN")))})

View file

@ -1,336 +0,0 @@
# Jolt Core Library
# Clojure-compatible core functions for the Jolt interpreter.
#
# This file is the AGGREGATOR (jolt-nma8, phase 2b): the implementation now lives
# in cohesive cluster modules, loaded here in dependency order and re-exported
# (:export true) so every consumer keeps a single `(use ./core)`. The bottom of
# this file owns the registration table (core-bindings) and init-core!, which
# reference fns from every cluster.
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./regex)
(use ./config)
(use ./pv)
(use ./plist)
# Cluster modules, in load order (each uses the ones before it). :export true
# re-exports each module's OWN defs through core, so `(use ./core)` sees them all.
(import ./core_types :prefix "" :export true)
(import ./core_coll :prefix "" :export true)
(import ./core_print :prefix "" :export true)
(import ./core_io :prefix "" :export true)
(import ./core_refs :prefix "" :export true)
(import ./core_extra :prefix "" :export true)
(def- core-bindings
"Map of symbol name → function for all core functions."
@{"nil?" core-nil?
"string?" core-string?
"number?" core-number?
"fn?" core-fn?
"keyword?" core-keyword?
"symbol?" core-symbol?
"vector?" core-vector?
"map?" core-map?
"seq?" core-seq?
"coll?" core-coll?
"identical?" core-identical?
"integer?" core-integer?
"list?" core-list?
"+" core-+
"-" core-sub
"*" core-*
"/" core-/
"inc" core-inc
"dec" core-dec
"even?" core-even?
"odd?" core-odd?
"mod" core-mod
"rem" core-rem
"quot" core-quot
"rand" core-rand
"=" core-=
"<" core-<
">" core->
"<=" core-<=
">=" core->=
"conj" core-conj
"assoc" core-assoc
"dissoc" core-dissoc
"get" core-get
"contains?" core-contains?
"count" core-count
"format" core-format
"first" core-first
"rest" core-rest
"next" core-next
"cons" core-cons
"seq" core-seq
"vec" core-vec
"__sq1" core-sq1
"__sqcat" core-sqcat
"__sqvec" core-sqvec
"__sqmap" core-sqmap
"__sqset" core-sqset
"into" core-into
"with-meta" core-with-meta
"map" core-map
"filter" core-filter
"remove" core-remove
"reduce" core-reduce
"apply" core-apply
"map-entry?" core-map-entry?
"future-call" core-future-call
"future?" core-future?
"future-cancel" core-future-cancel
"tagged-literal" core-tagged-literal
"re-groups" core-re-groups
"transient" core-transient
"transient?" core-transient?
"persistent!" core-persistent!
"conj!" core-conj!
"assoc!" core-assoc!
"dissoc!" core-dissoc!
"pop!" core-pop!
"hash-combine" core-hash-combine
"hash-ordered-coll" core-hash-ordered-coll
"hash-unordered-coll" core-hash-unordered-coll
"gensym" gensym
"__write" core-write
"__eprint" core-eprint
"__eprintf" core-eprintf
"__jdbc-wrap-conn" core-jdbc-wrap-conn
"__jdbc-conn-raw" core-jdbc-conn-raw
"__jdbc-make-stmt" core-jdbc-make-stmt
"__make-file" core-make-file
"__file?" core-file?
"__pr-str1" core-pr-str1
"__make-uuid" make-uuid
"compare" core-compare
"type" core-type
"slurp" core-slurp
"spit" core-spit
"flush" core-flush
"get-thread-bindings" core-get-thread-bindings
"__thread-bound?" core-thread-bound?*
"__dir?" core-dir?
"__list-dir" core-list-dir
"parse-long" core-parse-long
"parse-double" core-parse-double
"current-time-ms" core-current-time-ms
"mapcat" core-mapcat
"sequence" core-sequence
"keyword" core-keyword
"symbol" core-symbol
"namespace" core-namespace
"reduced" core-reduced
"reduced?" core-reduced?
"rseq" core-rseq
"ex-info" core-ex-info
"__with-out-str" core-with-out-str
"delay?" core-delay?
"make-delay" core-make-delay
"take" core-take
"drop" core-drop
"take-while" core-take-while
"drop-while" core-drop-while
"concat" core-concat
"nth" core-nth
"sort" core-sort
"partition" core-partition
"range" core-range
"vector" core-vector
"hash-map" core-hash-map
"array-map" core-array-map
"hash-set" core-hash-set
"set" core-set
"list" core-list
"set?" core-set?
"disj" core-disj
"coll->cells" coll->cells
"make-lazy-seq" make-lazy-seq
"lazy-cons" lazy-cons
"lazy-from" lazy-from
"str" core-str
"name" core-name
"subs" core-subs
"str-trim" string/trim
"str-upper" string/ascii-upper
"str-lower" string/ascii-lower
"str-find" string/find
"str-replace" core-str-replace-first
"str-replace-all" core-str-replace-all
"str-reverse-b" string/reverse
"str-join" core-str-join
"str-split" core-str-split
"re-pattern" re-pattern
"re-find" re-find
"re-matches" re-matches
"re-seq" re-seq
"regex?" regex?
"str-triml" string/triml
"str-trimr" string/trimr
# Java-style arrays (buffers for bytes, arrays otherwise)
"aclone" core-aclone
"object-array" core-object-array
"int-array" core-int-array
"long-array" core-long-array
"short-array" core-short-array
"double-array" core-double-array
"float-array" core-float-array
"char-array" core-char-array
"boolean-array" core-boolean-array
"byte-array" core-byte-array
"aset-byte" core-aset-byte
"aset-int" core-aset-int
"aset-long" core-aset-long
"aset-short" core-aset-short
"aset-double" core-aset-double
"aset-float" core-aset-float
"aset-char" core-aset-char
"aset-boolean" core-aset-boolean
"make-array" core-make-array
"into-array" core-into-array
"to-array" core-to-array
"bytes" core-bytes
"booleans" core-booleans
"ints" core-ints
"longs" core-longs
"shorts" core-shorts
"doubles" core-doubles
"floats" core-floats
"chars" core-chars
"byte" core-byte
"short" core-short
"bigint" core-bigint
"biginteger" core-biginteger
"chunk-buffer" core-chunk-buffer
"chunk-append" core-chunk-append
"chunk" core-chunk
"chunk-first" core-chunk-first
"chunk-rest" core-chunk-rest
"chunk-next" core-chunk-next
"chunk-cons" core-chunk-cons
"boolean" core-boolean
"cat" core-cat
"disj!" core-disj!
"reader-conditional" core-reader-conditional
"class" core-class
"re-matcher" core-re-matcher
# Bit operations
"__bit-and" core-bit-and
"__bit-or" core-bit-or
"__bit-xor" core-bit-xor
"bit-not" core-bit-not
"bit-shift-left" core-bit-shift-left
"bit-shift-right" core-bit-shift-right
"bit-clear" core-bit-clear
"bit-set" core-bit-set
"bit-flip" core-bit-flip
"bit-test" core-bit-test
"__bit-and-not" core-bit-and-not
"unsigned-bit-shift-right" core-unsigned-bit-shift-right
# Integer coercion / unchecked math
"int" core-int
"long" core-long
"double" core-double
"float" core-float
"char" core-char
# Hash
"hash" core-hash
"atom" core-atom
"deref" core-deref
"reset!" core-reset!
"swap!" core-swap!
"not" core-not
"Object" core-Object
"make-protocol" core-make-protocol
# satisfies?/resolve are interned by install-stateful-fns! (ctx-capturing);
# type->str was an inert SCI stub with no callers.
"implements?" core-implements?
"volatile!" core-volatile!
"Thread" core-Thread
"ThreadLocal" core-ThreadLocal
"IllegalStateException" core-IllegalStateException
"copy-core-var" core-copy-core-var
"copy-var" core-copy-var
"macrofy" core-macrofy
"new-var" core-new-var
"__local-var" core-local-var
"__close" core-close-resource
"avoid-method-too-large" core-avoid-method-too-large
"bytes?" core-bytes?
"meta" core-meta
"var-get" core-var-get
"var-set" core-var-set
"var?" core-var?
"var-dynamic?" core-var-dynamic?
"alter-var-root" core-alter-var-root
"alter-meta!" core-alter-meta!
"reset-meta!" core-reset-meta!
"push-thread-bindings" core-push-thread-bindings
"pop-thread-bindings" core-pop-thread-bindings
# Dynamic vars — stubs for SCI bootstrap
"*unchecked-math*" false
"*clojure-version*" @{:major 1 :minor 11 :incremental 0 :qualifier nil}
"*1" :jolt/nil-sentinel
"*2" :jolt/nil-sentinel
"*3" :jolt/nil-sentinel
"*e" :jolt/nil-sentinel
"*assert" true})
(defn core-macro-names
"Set of core binding names that are macros. Empty now that every core macro
lives in the Clojure overlay (clojure.core.*-syntax / *-macros tiers)."
[]
@{})
# Wire the print-method callback once the overlay (and its print-method
# multimethod) exists: the renderer's record fallthrough consults the methods
# table on the var; only a USER-registered method fires — the multimethod's
# :default would bounce straight back into the renderer.
(defn install-print-method-cb! [ctx]
(def core-ns (ctx-find-ns ctx "clojure.core"))
(def pm-var (ns-find core-ns "print-method"))
(when pm-var
(set-print-method-cb!
(fn [v emit]
(def methods (get pm-var :jolt/methods))
(when methods
(def mt (core-meta v))
(def t (and mt (core-get mt :type)))
(def dval (if (keyword? t) t (core-type v)))
(def m (get methods dval))
(when m
(m v @{:jolt/type :jolt/writer :sink emit})
true)))))
# A record/deftype's own Object/toString (jolt-rt6n): str routes records here
# so a deftype with (toString [_] ...) renders via it instead of the data repr.
(set-record-tostring-cb!
(fn [v]
(def tag (record-tag v))
(when tag
(def m (find-method-any-protocol ctx tag "toString"))
(when m (m v))))))
(def init-core!
(fn [& args]
(case (length args)
1 (let [ctx (args 0)
ns (ctx-find-ns ctx "clojure.core")]
(loop [[name fn] :pairs core-bindings]
(def v (ns-intern ns name (if (= fn :jolt/nil-sentinel) nil fn)))
(when (get (core-macro-names) name)
(put v :macro true)))
ns)
2 (let [ctx (args 0) ns-name (args 1)
ns (ctx-find-ns ctx ns-name)]
(loop [[name fn] :pairs core-bindings]
(def v (ns-intern ns name (if (= fn :jolt/nil-sentinel) nil fn)))
(when (get (core-macro-names) name)
(put v :macro true)))
ns)
(error "Wrong number of args passed to: init-core!"))))

File diff suppressed because it is too large Load diff

View file

@ -1,469 +0,0 @@
# Jolt Core — additional clojure.core fns, transients, hashing
# Extracted from core.janet (jolt-nma8, phase 2b split).
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./regex)
(use ./config)
(use ./pv)
(use ./plist)
(use ./core_types)
(use ./core_coll)
(use ./core_print)
(use ./core_io)
(use ./core_refs)
# Threading macros (as regular functions? No, as macros in Clojure)
# These need to be defined as macros in the Jolt namespace system.
# For now, skip — they need proper macro definition via the evaluator.
# ============================================================
# ============================================================
# Initialization — intern everything into a context's namespace
# ============================================================
(def gensym_counter @{:val 0})
(defn gensym
"Returns a new symbol with a unique name."
[&opt prefix-string]
(default prefix-string "G__")
(def n (get gensym_counter :val))
(put gensym_counter :val (+ n 1))
{:jolt/type :symbol :ns nil :name (string prefix-string n)})
# if-let/when-let/if-some/when-some now live in the Clojure overlay
# (core/30-macros.clj) as defmacros.
(defn core-push-thread-bindings [b] (push-thread-bindings b))
(defn core-pop-thread-bindings [] (pop-thread-bindings))
(defn core-var-get [v] (var-get v))
(defn core-var-set [v val] (var-set v val))
(defn core-var? [x] (var? x))
(defn core-alter-var-root [v f & args] (apply alter-var-root v f args))
(defn core-alter-meta! [v f & args] (apply alter-meta! v f args))
(defn core-reset-meta! [v meta] (reset-meta! v meta))
# intern is a ctx-capturing clojure.core fn now (install-stateful-fns!).
# get-method/methods/remove-method/remove-all-methods/prefer-method are
# overlay macros (core/30-macros.clj) over the evaluator's *-setup fns.
(defn core-with-meta [obj meta]
# Functions and scalars can't carry metadata in Jolt's model — return as-is
# rather than crashing (Clojure attaches meta only to IObj values).
(cond
(or (function? obj) (cfunction? obj) (number? obj) (boolean? obj)
(nil? obj) (string? obj) (keyword? obj) (buffer? obj))
obj
# Symbols carry metadata IN-PLACE in their struct's :meta field (this is how
# the reader attaches ^hint and keeps symbol? true — see reader/read-meta).
# The table-proto path below would make (symbol? (with-meta sym ..)) false and
# break destructuring/hint reading, so keep a symbol a symbol.
(and (struct? obj) (= :symbol (obj :jolt/type)))
(struct ;(kvs obj) :meta meta)
true
(do
(var new-obj @{})
(each k (keys obj)
(put new-obj k (get obj k)))
# table/setproto requires a table, convert struct meta to table. meta may
# be nil (Clojure allows (with-meta obj nil) to clear metadata).
(var meta-tab @{})
(when meta (each k (keys meta) (put meta-tab k (get meta k))))
(table/setproto new-obj meta-tab)
(put new-obj :jolt/meta meta)
new-obj)))
(defn core-var-dynamic? [v]
(var-dynamic? v))
# Java interop stubs
(def core-Object (fn [] (struct ;[:jolt/type :jolt/java-object])))
# Volatiles — typed box so deref/volatile? can recognize them.
(defn core-volatile! [v] @{:jolt/type :jolt/volatile :val v})
# volatile? / vreset! / vswap! now live in the Clojure collection tier — vreset!
# over jolt.host/ref-put!, vswap! over vreset! + get. The constructor stays native.
# Delays — created lazily by the `delay` macro; forced once via force/deref.
(defn core-make-delay [thunk] @{:jolt/type :jolt/delay :fn thunk :realized false :val nil})
(defn core-delay? [x] (and (table? x) (= :jolt/delay (x :jolt/type))))
# Proxy stub — returns nil form (macro, args not evaluated)
# Thread stubs
(def core-Thread (fn [& args] (struct ;[:jolt/type :jolt/thread])))
(def core-ThreadLocal (fn [& args] (struct ;[:jolt/type :jolt/thread-local])))
(def core-IllegalStateException (fn [& args] (struct ;[:jolt/type :jolt/exception])))
# letfn — mutually-recursive local fns. Expands to let* of fn* bindings; jolt
# closures capture the (shared, mutable) bindings table, so forward references
# between the fns resolve at call time.
# doseq — like `for` but eager and returns nil. Reuse `for`, force realization
# with `count`, discard the result.
# assert — (assert x) / (assert x message). Throws when x is falsy.
# ns-name now lives in the Clojure collection tier (pure over get + symbol).
# update lives in the Clojure kernel tier — core/00-kernel.clj. update-in stays
# (it's recursive and has internal callers).
(defn- ks-rest [ks]
(if (tuple? ks) (tuple/slice ks 1) (array/slice ks 1)))
# assoc-in / update-in now live in the Clojure collection tier (canonical
# recursive ports).
# fnil now lives in the Clojure collection tier (core/20-coll.clj), with
# Clojure's canonical 2/3/4-arity (patch the first 1-3 args only).
# copy-var stubs for sci.impl.copy-vars (used by sci.impl.namespaces)
(defn core-copy-core-var [sym] nil)
(defn core-copy-var [sym & args] nil)
(defn core-macrofy [sym fn & more] fn)
(defn core-new-var [sym & args] nil)
# A free-standing var cell (not interned anywhere): with-local-vars binds
# these as locals; var-get/var-set work on any cell.
(defn core-local-var [&opt val]
@{:jolt/type :jolt/var :name "local" :ns nil :root val :gen 0})
# with-open's close seam: a map-like value closes via its :close fn, a host
# file via file/close. No .close interop on the Janet host.
(defn core-close-resource [x]
(cond
(and (or (table? x) (struct? x)) (function? (get x :close))) ((get x :close))
(= :core/file (type x)) (file/close x)
(error (string "with-open: don't know how to close " (type x)))))
# sci stub: pass the registry map through (it was @{} — a raw host table that
# strict map-conj rightly rejects; identity also keeps sci's registry intact).
(defn core-avoid-method-too-large [& args] (if (> (length args) 0) (in args 0) {}))
# declare macro — accepts symbols, does nothing (forward declaration)
# Build a protocol value (a self-evaluating tagged table). Exposed so the overlay
# `defprotocol` can construct one via a fn call rather than embedding a tagged
# struct literal (which the interpreter would try to re-evaluate). `methods` is a
# {kw {:name str}} map; only :name is consulted (by satisfies?).
(defn core-make-protocol [name-str methods]
@{:jolt/type :jolt/protocol
:name {:jolt/type :symbol :ns nil :name name-str}
:methods methods})
# extends? is a real overlay fn now (30-macros, over extenders).
(def core-implements? (fn [& args] false))
# ============================================================
# Additional clojure.core functions (conformance batch)
# ============================================================
(defn core-keyword
"(keyword name) or (keyword ns name). Namespaced keywords are `:ns/name`.
(keyword nil) is nil; the 2-arg form requires string args (nil ns allowed)."
[& args]
(case (length args)
1 (let [a (in args 0)]
(cond
(nil? a) nil
(keyword? a) a
(or (string? a) (core-symbol? a)) (keyword (core-name a))
(error (string "keyword requires a string, symbol or keyword, got " (type a)))))
2 (let [ns (in args 0) nm (in args 1)]
(when (not (and (or (nil? ns) (string? ns)) (string? nm)))
(error "keyword ns and name must be strings"))
(keyword (if ns (string ns "/" nm) nm)))
(keyword ;args)))
(defn core-symbol
"(symbol name) or (symbol ns name) -> a jolt symbol struct. name/ns must be
strings (a single symbol arg is returned as-is)."
[& args]
(case (length args)
1 (let [a (in args 0)]
(cond
(core-symbol? a) a
(or (string? a) (keyword? a)) {:jolt/type :symbol :ns nil :name (core-name a)}
(error (string "symbol requires a string or symbol, got " (type a)))))
2 (let [ns (in args 0) nm (in args 1)]
(when (not (and (or (nil? ns) (string? ns)) (string? nm)))
(error "symbol ns and name must be strings"))
{:jolt/type :symbol :ns ns :name nm})
(error "symbol expects 1 or 2 args")))
# (take-nth's transducer arity lives in the overlay now.)
# filterv now lives in the Clojure collection tier (core/20-coll.clj).
# mapv lives in the Clojure kernel tier — core/00-kernel.clj.
# (interpose's transducer arity lives in the overlay now.)
# interpose / take-nth now live in the Clojure lazy tier (core/40-lazy.clj),
# with the canonical transducer arities.
# keep now lives in the Clojure lazy tier (core/40-lazy.clj).
# empty now lives in the Clojure collection tier (core/20-coll.clj); a lazy
# seq empties to () there (this fn returned a host table for it).
# not-empty now lives in the Clojure collection tier (core/20-coll.clj).
# rseq is defined only on vectors and sorted collections (Reversible).
(defn core-rseq [coll]
(cond
(pvec? coll) (tuple/slice (tuple ;(reverse (pv->array coll))))
(core-sorted? coll) ((sorted-op coll :rseq) coll)
(error (string "rseq requires a vector or sorted collection, got " (type coll)))))
# some-fn now lives in the Clojure collection tier (core/20-coll.clj).
# Associative = maps and (real) vectors only. pvec is a literal/built vector;
# tuples and lists are seq results, not associative.
# ifn? now lives in the Clojure collection tier — canonical IFn set (fns,
# keywords, symbols, maps, sets, vectors, vars); lists are NOT IFn.
# With a single item, Clojure returns it WITHOUT calling f. On ties, the last
# extremal item wins (>=/<= update), matching Clojure.
# Clojure's min-key/max-key: the 2-arg base compares with strict < / > (so the
# second wins on ties/NaN), and each further item switches on <= / >=. This
# asymmetry reproduces the JVM's NaN-ordering behavior. Janet's < / > are used
# directly (NaN comparisons are false, never throwing).
# keys must be numbers (NaN allowed) — like Clojure, which compares them with </>.
# min-key / max-key now live in the Clojure collection tier (core/20-coll.clj).
# vary-meta / namespace-munge now live in the Clojure collection tier
# (core/20-coll.clj) — pure compositions of meta/with-meta and str/map.
# Exceptions (ex-info / ex-data / ex-message)
(defn core-ex-info [msg data & more]
@{:jolt/type :jolt/ex-info :message msg :data data
:cause (if (> (length more) 0) (in more 0) nil)})
# ex-data / ex-message / ex-cause now live in the Clojure collection tier
# (core/20-coll.clj) — pure over get on the tagged value the constructor builds.
# String split/replace that accept either a literal string or a regex value.
(defn core-str-split [pat s &opt limit]
(if (regex? pat)
(re-split pat s limit)
(let [parts (string/split pat s)]
# literal separator: a positive limit caps the part count; rejoin the tail
# with the (constant) separator to recover the unsplit remainder.
(if (and limit (> limit 0) (> (length parts) limit))
(array/push (array/slice parts 0 (- limit 1))
(string/join (array/slice parts (- limit 1)) pat))
parts))))
(defn core-str-replace-all [pat repl s]
(if (regex? pat)
(re-replace-all pat s repl)
(string/replace-all pat repl s)))
(defn core-str-replace-first [pat repl s]
(if (regex? pat)
(re-replace-first pat s repl)
(string/replace pat repl s)))
# Iterator/enumeration seqs — Jolt has no Java iterators, so adapt to plain seq.
# enumeration-seq / iterator-seq live in the Clojure collection tier.
# xml-seq now lives in the Clojure collection tier (core/20-coll.clj).
# line-seq now lives in the Clojure IO tier (core/50-io.clj), over the reader
# protocol of the *in* family.
(defn core-re-matcher [re s] @{:jolt/type :jolt/matcher :re re :s s :pos 0})
# bean / print-method / print-dup / the proxy surface live in the Clojure
# collection tier (JVM-shape stubs; print hooks inert until jolt-g1r).
# == lives in the Clojure collection tier (core/20-coll.clj); memfn is an
# overlay macro (core/30-macros.clj) over the .method call sugar.
# eduction / ->Eduction live in the Clojure collection tier (core/20-coll.clj).
(def- char-escapes
{10 "\\n" 9 "\\t" 13 "\\r" 12 "\\f" 8 "\\b" 34 "\\\"" 92 "\\\\"})
(def- char-names
{10 "newline" 9 "tab" 13 "return" 12 "formfeed" 8 "backspace" 32 "space"})
# char-escape-string / char-name-string now live in the Clojure collection
# tier as char-keyed maps. The CODE-keyed tables below stay: pr-render uses them.
# subseq / rsubseq over sorted collections
# subseq / rsubseq now live in the Clojure sorted tier (core/25-sorted.clj),
# along with the constructors and all sorted-coll semantics.
# ============================================================
# Additional clojure.core functions
# ============================================================
# Integer-valued: a finite number equal to its floor. Infinity floors to itself
# but is NOT integer-valued (so float?/double? are true for ##Inf, and int?/
# pos-int?/… are false), and NaN is excluded by the equality check.
(defn- intval? [x] (and (number? x) (< (math/abs x) math/inf) (= x (math/floor x))))
# Forcing lazy seqs
# Map entries (represented as 2-element vectors)
# key/val require a map entry (a 2-element vector/tuple in Jolt); Clojure throws
# otherwise. (Jolt can't distinguish a 2-vector from a real MapEntry.)
# A map entry is a 2-element tuple — Jolt produces tuples only from map
# iteration (first/seq/map over a map), while vector literals are pvecs and
# lists are arrays. So key/val/map-entry? accept a 2-tuple and reject a plain
# vector, matching Clojure (where a MapEntry is distinct from a vector).
(defn- entry-like? [x] (and (tuple? x) (= 2 (length x))))
# key / val now live in the Clojure collection tier (core/20-coll.clj),
# along with find (previously missing from jolt entirely).
(defn core-map-entry? [x] (entry-like? x))
# Reversible (supports rseq) = vectors and sorted collections.
# Numeric predicates (Jolt has no ratios/bigdec). nat-int?/pos-int?/neg-int?/
# ratio?/decimal?/rational? live in the Clojure collection tier (core/20-coll.clj).
# Jolt has no ratio type, so numerator/denominator have no valid input (Clojure
# requires a Ratio and throws otherwise).
# numerator / denominator now live in the Clojure collection tier (Jolt has
# no ratios; they throw, as on a non-ratio in Clojure).
# special-symbol? lives in the Clojure collection tier (a quoted symbol set).
# record? now lives in the Clojure collection tier (tagged-value predicate).
# Promise: single-threaded box backed by an atom (deref returns nil until set).
# promise / deliver live in the Clojure collection tier (an atom; deref of an
# undelivered promise is nil — single-threaded host, no blocking).
(defn core-tagged-literal [tag form] @{:jolt/type :jolt/tagged-literal :tag tag :form form})
# ensure-reduced / halt-when live in the Clojure collection tier
# (core/20-coll.clj) — halt-when is the canonical ::halt-map version there.
(defn core-re-groups [m] (error "re-groups: stateful matchers are not supported in Jolt"))
# Transients — real mutable scratch collections backed by Janet's native arrays
# and tables (host interop): O(1) conj!/assoc!/dissoc!/disj!/pop!, frozen back to
# a persistent value by persistent!. A transient is a tagged table holding either
# a Janet array (vectors) or a Janet table keyed by canonical key (maps/sets, so
# collection keys still compare by value). The mutating ops return the transient.
(defn core-transient [coll]
(cond
(pvec? coll)
@{:jolt/type :jolt/transient :kind :vector :arr (pv->array coll)}
(set? coll)
(let [t @{}] (each e (phs-seq coll) (put t (tbl-key e) (tbl-box e)))
@{:jolt/type :jolt/transient :kind :set :tbl t})
(or (phm? coll) (and (struct? coll) (nil? (get coll :jolt/type))))
(let [t @{}]
(each pair (realize-for-iteration coll)
(put t (tbl-key (in pair 0)) @[(in pair 0) (in pair 1)]))
@{:jolt/type :jolt/transient :kind :map :tbl t})
# mutable-build arrays (vectors/lists) — copy into a transient vector
(array? coll) @{:jolt/type :jolt/transient :kind :vector :arr (array/slice coll)}
# tuples (reader vectors / map entries) are vectors too
(tuple? coll) @{:jolt/type :jolt/transient :kind :vector :arr (array ;coll)}
(error (string "Don't know how to create a transient from " (type coll)))))
# A transient is invalidated by persistent!; using it afterwards is a bug.
(defn- tr-check-active! [t]
(when (get t :jolt/persistent)
(error "Transient used after persistent! call")))
(defn- tr-conj! [t x]
(tr-check-active! t)
(case (t :kind)
:vector (array/push (t :arr) x)
:set (put (t :tbl) (tbl-key x) (tbl-box x))
:map (cond
# a [k v] pair (map-entry / 2-vector)
(and (or (pvec? x) (tuple? x) (array? x))
(= 2 (if (pvec? x) (pv-count x) (length x))))
(put (t :tbl) (tbl-key (vnth x 0)) @[(vnth x 0) (vnth x 1)])
# a map: merge all its entries
(or (phm? x) (and (struct? x) (nil? (get x :jolt/type))))
(each e (map-entries-of x)
(put (t :tbl) (tbl-key (in e 0)) @[(in e 0) (in e 1)]))
(error "conj! on a transient map requires a [key value] pair or a map")))
t)
(defn- tr-assoc! [t k v]
(tr-check-active! t)
(case (t :kind)
:vector (let [a (t :arr)]
(when (not (and (number? k) (= k (math/floor k)) (>= k 0) (<= k (length a))))
(error (string "Index " k " out of bounds for assoc! on a transient vector of length " (length a))))
(if (= k (length a)) (array/push a v) (put a k v)))
:map (put (t :tbl) (tbl-key k) @[k v])
(error "assoc! expects a transient vector or map"))
t)
# The bang ops require a transient (Clojure throws otherwise); no lenient
# fallback to the persistent op.
(defn core-conj! [& args]
(cond
(= 0 (length args)) (core-transient (make-vec @[])) # (conj!) -> (transient [])
(= 1 (length args)) (first args) # (conj! coll) -> coll, as-is
(let [t (first args) xs (tuple/slice args 1)]
(if (core-transient? t)
(do (each x xs (tr-conj! t x)) t)
(error "conj! requires a transient")))))
(defn core-assoc! [t & kvs]
# assoc! requires an even key/val count, like assoc and Clojure's assoc! — a
# dangling key with no value throws (jolt-ea9k: the former lenient nil-fill
# was non-Clojure and inconsistent with jolt's own plain assoc).
(when (odd? (length kvs))
(error "assoc! expects an even number of key/value arguments"))
(if (core-transient? t)
(do (var i 0) (while (< i (length kvs)) (tr-assoc! t (in kvs i) (in kvs (+ i 1))) (+= i 2)) t)
(error "assoc! requires a transient")))
(defn core-dissoc! [t & ks]
(if (and (core-transient? t) (= :map (t :kind)))
(do (tr-check-active! t) (each k ks (put (t :tbl) (tbl-key k) nil)) t)
(error "dissoc! requires a transient map")))
(defn core-disj! [t & xs]
(if (and (core-transient? t) (= :set (t :kind)))
(do (tr-check-active! t) (each x xs (put (t :tbl) (tbl-key x) nil)) t)
(error "disj! requires a transient set")))
(defn core-pop! [t]
(if (and (core-transient? t) (= :vector (t :kind)))
(do (tr-check-active! t)
(when (= 0 (length (t :arr))) (error "Can't pop empty vector"))
(array/pop (t :arr)) t)
(error "pop! requires a transient vector")))
(defn core-persistent! [t]
(if (core-transient? t)
(do
(tr-check-active! t)
(def result
(case (t :kind)
:vector (make-vec (t :arr))
# The transient already deduped into a native table; bulk-build the
# persistent value ONCE (bottom-up HAMT) instead of folding a phm-assoc
# per entry. This is the lever behind every transient-based builder
# (frequencies/group-by/set/into) — jolt-5vsp collections.
:set (phs-from-seq (map tbl-unbox (values (t :tbl))))
:map (phm-from-pairs (values (t :tbl)))))
# Invalidate: any further bang op (or a second persistent!) now throws.
(put t :jolt/persistent true)
result)
(error "persistent! requires a transient")))
# Unchecked arithmetic — Jolt numbers don't overflow, so these are plain ops.
# unchecked-* arithmetic lives in the Clojure collection tier
# (core/20-coll.clj); only the masking byte/short/char coercions remain above.
# Hashing helpers
# Hashes are masked to 24 bits at each step so intermediate products stay within
# Janet's integer range (a float here would make band error).
(defn- h24 [x] (band (hash x) 0xffffff))
(defn core-hash-combine [a b] (band (bxor (h24 a) (+ (h24 b) 0x9e3779)) 0xffffff))
(defn core-hash-ordered-coll [coll]
(var h 1) (each x (realize-for-iteration coll) (set h (band (+ (* 31 h) (h24 x)) 0xffffff))) h)
(defn core-hash-unordered-coll [coll]
(var h 0) (each x (realize-for-iteration coll) (set h (band (+ h (h24 x)) 0xffffff))) h)
# prefers is a macro over prefers-setup now (the store lives on the VAR).
# parse-uuid lives in the Clojure collection tier (core/20-coll.clj) over
# re-matches + the __make-uuid host constructor (types.janet).

View file

@ -1,235 +0,0 @@
# Jolt Core — I/O, files, JDBC, compare, type
# Extracted from core.janet (jolt-nma8, phase 2b split).
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./regex)
(use ./config)
(use ./pv)
(use ./plist)
(use ./core_types)
(use ./core_coll)
(use ./core_print)
# I/O — minimal wrappers
# ============================================================
# print/println use str semantics (bare strings); pr/prn use readable (quoted).
# All space-separate their args, like Clojure.
# print/println live in the Clojure collection tier (core/20-coll.clj) over
# the __write / __pr-str1 host seams; str-render-one stays for core-str.
(defn core-write [s] (prin s) nil)
(defn core-eprint [s] (eprint s) nil)
(defn core-eprintf [fmt & args] (eprintf fmt ;args) nil)
# next.jdbc host shims (the db library's next.jdbc compat layer builds on these).
# A connection is a tagged table wrapping a jdbc.core conn (:raw) plus clj
# callbacks: :exec runs one SQL string in the transaction (so the janet-side
# Statement.executeBatch can run SQL without a janet->clj call), :close closes
# the underlying conn, :product is the JDBC database product name. instance?
# Connection (evaluator) and the :jolt/jdbc-conn tagged methods (host_interop) key
# off this shape.
(defn core-jdbc-wrap-conn [raw exec closef product]
@{:jolt/type :jolt/jdbc-conn :raw raw :exec exec :close closef
:product product :closed @[false]})
# Robust unwrap: a wrapped conn -> its :raw; anything else passes through (so the
# next.jdbc fns accept either a wrapped conn or a bare jdbc.core conn/spec).
(defn core-jdbc-conn-raw [x]
(if (and (table? x) (= :jolt/jdbc-conn (get x :jolt/type))) (get x :raw) x))
(defn core-jdbc-make-stmt [w]
# :close lets with-open close the statement (core-close-resource calls :close);
# nothing to release — executeBatch already ran the commands.
@{:jolt/type :jolt/jdbc-stmt :exec (get w :exec) :cmds @[] :close (fn [] nil)})
# java.io.File model (jolt-hjw). io/file and (File. …) build a tagged :jolt/file
# value so (instance? File x) works and migratus's File-vs-jar branching takes
# the filesystem path. The File method surface + nio glob live in host_interop; here
# are the constructor/predicate builtins and the path coercion str/slurp use.
(defn core-file-path
"The path string of a :jolt/file, or (string x) for anything else."
[x]
(if (and (table? x) (= :jolt/file (get x :jolt/type))) (get x :path) (string x)))
(defn core-make-file [path &opt child]
(def base (core-file-path path))
@{:jolt/type :jolt/file :path (if child (string base "/" (core-file-path child)) base)})
(defn core-file? [x] (and (table? x) (= :jolt/file (get x :jolt/type))))
# newline lives in the Clojure collection tier (core/20-coll.clj).
# Clojure 1.11 string->scalar parsers: nil on malformed input, throw on a
# non-string. Validation is strict (scan-number alone accepts 0x10 etc.).
(defn- parse-arg-str [s who]
(if (or (string? s) (buffer? s)) (string s)
(error (string who " requires a string, got " (type s)))))
(defn core-parse-long [s]
(def str* (parse-arg-str s "parse-long"))
(def n (length str*))
(def start (if (and (> n 0) (or (= 43 (in str* 0)) (= 45 (in str* 0)))) 1 0))
(if (and (> n start)
(do (var ok true)
(for i start n (when (or (< (in str* i) 48) (> (in str* i) 57)) (set ok false)))
ok))
(scan-number str*)
nil))
(defn core-parse-double [s]
(def str* (parse-arg-str s "parse-double"))
# strict float shape: [+-] digits [. digits] [eE [+-] digits] — at least one
# digit overall; "Infinity"/"-Infinity"/"NaN" accepted like the reference.
(cond
(= str* "Infinity") math/inf
(= str* "-Infinity") (- math/inf)
(= str* "NaN") math/nan
(do
(def pat (peg/compile ~(sequence (opt (set "+-")) (choice (sequence (some :d) (opt (sequence "." (any :d)))) (sequence "." (some :d))) (opt (sequence (set "eE") (opt (set "+-")) (some :d))) -1)))
(if (peg/match pat str*) (scan-number str*) nil))))
# parse-boolean lives in the Clojure collection tier (core/20-coll.clj).
# Host time source for the `time` macro (monotonic, milliseconds).
(defn core-current-time-ms [] (* 1000 (os/clock :monotonic)))
# Host IO (host-classified in the spec): path-based slurp/spit, *out* flush.
# Opts (:encoding ...) are accepted and ignored — everything is UTF-8 here.
# Reader shims (java.io.StringReader / PushbackReader / anything carrying
# :s + :pos) DRAIN instead of opening a file: Ring middleware slurps request
# bodies, and the jolt Ring adapter hands those over as StringReaders.
(defn core-slurp [src & opts]
(cond
(core-file? src) (string (slurp (core-file-path src)))
(and (table? src) (string? (get src :s)) (number? (get src :pos)))
(let [s (src :s) p (src :pos)]
(put src :pos (length s))
(string/slice s p))
# a byte-array / buffer body: bytes already in hand, just stringify
(buffer? src) (string src)
(string (slurp src))))
(defn core-spit [path content & opts]
(def append? (do (var a false) (var i 0)
(while (< i (length opts))
(when (and (= :append (in opts i)) (in opts (+ i 1))) (set a true))
(+= i 2))
a))
(def f (file/open (core-file-path path) (if append? :a :w)))
(file/write f (str-render-one content))
(file/close f)
nil)
(defn core-flush []
(def out (dyn :out))
(when out (file/flush out))
nil)
# Thread-binding introspection over the frame stack (types/cur-binding-stack).
(defn core-get-thread-bindings []
# Innermost frame wins: merge frames oldest-first. The result is a Janet
# STRUCT keyed by the var tables themselves — the exact frame representation
# var-get reads (identity-keyed get) — so the map can be re-pushed by
# with-bindings*/bound-fn* and remains lookup-able with (get m the-var).
(def acc @{})
(each frame (snapshot-bindings)
(each entry (realize-for-iteration frame)
(put acc (in entry 0) (in entry 1))))
(table/to-struct acc))
(defn core-thread-bound?* [v]
(var found false)
(each frame (snapshot-bindings)
(each entry (realize-for-iteration frame)
(when (= (in entry 0) v) (set found true))))
found)
# Directory primitives for file-seq (paths, not File objects — host-classified).
(defn core-dir? [path]
(def st (os/stat path))
(and st (= :directory (st :mode))))
(defn core-list-dir [path]
(def entries (os/dir path))
(map (fn [e] (string path "/" e)) (sort entries)))
# Clojure compare: a total order over comparable values. nil sorts first;
# numbers numerically; strings/keywords lexically; symbols by ns then name;
# booleans false<true; chars by codepoint; vectors by length then elementwise;
# uuids by canonical string; insts by epoch ms. Cross-type comparison throws
# (like Clojure's ClassCastException).
(var core-compare nil)
(set core-compare (fn ccompare [a b]
(defn cmp3 [x y] (cond (< x y) -1 (> x y) 1 0))
(cond
(and (nil? a) (nil? b)) 0
(nil? a) -1
(nil? b) 1
(and (number? a) (number? b)) (cmp3 a b)
(and (or (string? a) (buffer? a)) (or (string? b) (buffer? b)))
(cmp3 (string a) (string b))
(and (keyword? a) (keyword? b)) (cmp3 (string a) (string b))
(and (core-symbol? a) (core-symbol? b))
(let [r (cmp3 (string (or (a :ns) "")) (string (or (b :ns) "")))]
(if (= 0 r) (cmp3 (a :name) (b :name)) r))
(and (boolean? a) (boolean? b))
(cond (= a b) 0 (= a false) -1 1)
(and (core-char? a) (core-char? b)) (cmp3 (a :ch) (b :ch))
(and (struct? a) (= :jolt/uuid (get a :jolt/type))
(struct? b) (= :jolt/uuid (get b :jolt/type)))
(cmp3 (a :str) (b :str))
(and (struct? a) (= :jolt/inst (get a :jolt/type))
(struct? b) (= :jolt/inst (get b :jolt/type)))
(cmp3 (a :ms) (b :ms))
(and (jvec? a) (jvec? b))
(let [la (vcount a) lb (vcount b)]
(if (not= la lb)
(cmp3 la lb)
(do
(var r 0) (var i 0)
(while (and (= r 0) (< i la))
(set r (ccompare (vnth a i) (vnth b i)))
(++ i))
r)))
(error (string "Cannot compare " (type a) " with " (type b))))))
# Clojure type: the :type metadata when present, else the value's type. With no
# class objects on this host, the "class" is a symbol: a deftype/record value
# yields its type tag symbol; everything else a taxonomy keyword
# (host-classified — see spec coverage).
(defn core-type [x]
(def m (core-meta x))
(def override (and m (core-get m :type)))
(if (not (nil? override))
override
(cond
(and (table? x) (get x :jolt/deftype))
{:jolt/type :symbol :ns nil :name (get x :jolt/deftype)}
(nil? x) nil
(boolean? x) :boolean
(number? x) :number
(or (string? x) (buffer? x)) :string
(keyword? x) :keyword
(core-symbol? x) :symbol
(core-char? x) :char
(and (struct? x) (get x :jolt/type)) (get x :jolt/type)
(jvec? x) :vector
(core-map? x) :map
(set? x) :set
(core-seq? x) :seq
(or (function? x) (cfunction? x)) :fn
(table? x) (or (get x :jolt/type) :table)
:else (keyword (type x)))))
# Capture *out*: run thunk with Janet's :out dynamic bound to a buffer, so all
# print/println/pr/prn output (which go through `prin` -> (dyn :out)) is collected
# and returned as a string. The with-out-str macro (overlay) wraps a body thunk.
(defn core-with-out-str [thunk]
(def buf @"")
(with-dyns [:out buf] (thunk))
(string buf))
# pr/prn/pr-str live in the Clojure collection tier (core/20-coll.clj); the
# renderer itself stays host (representation-coupled, shared with hot str).
(defn core-pr-str1 [x] (let [b @""] (pr-render b x) (string b)))
# ============================================================

View file

@ -1,250 +0,0 @@
# Jolt Core — string + print rendering (pr-str/str)
# Extracted from core.janet (jolt-nma8, phase 2b split).
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./regex)
(use ./config)
(use ./pv)
(use ./plist)
(use ./core_types)
(use ./core_coll)
# String functions
# ============================================================
# Readable rendering of a value (Clojure pr semantics): strings quoted,
# keywords with leading ':', symbols by name, collections with their reader
# syntax. Used by both pr-str (readable) and str (collection elements).
# A namespace's :name may be a string or a symbol struct depending on the
# creation path — normalize for display.
(defn- ns-display-name [ns]
(def n (ns :name))
(if (and (struct? n) (= :symbol (get n :jolt/type))) (n :name) (string n)))
# print-method callback (jolt-g1r): set by api/init AFTER the overlay loads,
# to a (fn [v emit] handled?) that looks for a USER-registered print-method
# multimethod entry for v's dispatch value and renders through it (emit takes
# string pieces). The renderer consults it only on the record/tagged
# fallthrough, so built-in rendering pays nothing.
(var print-method-cb nil)
(defn set-print-method-cb! [f] (set print-method-cb f))
# Late-bound hook to a record's custom Object/toString (jolt-rt6n). Returns the
# string a deftype's (toString [_] ...) produces, or nil when the type defines
# none. core can't reach the ctx type-registry directly, so install-print-method-cb!
# wires this per-ctx. str routes records through it; the data repr is the fallback.
(var record-tostring-cb nil)
(defn set-record-tostring-cb! [f] (set record-tostring-cb f))
(def- pr-char-escapes
{34 "\\\"" 92 "\\\\" 10 "\\n" 9 "\\t" 13 "\\r" 12 "\\f" 8 "\\b"})
(var pr-render nil)
# Format a number the way Clojure prints it: infinity and NaN have named forms
# (Janet renders them "inf"/"-inf"/"nan").
(defn- fmt-number [v]
(cond
(not (number? v)) (string v)
(= v math/inf) "Infinity"
(= v (- math/inf)) "-Infinity"
(not= v v) "NaN"
(string v)))
(defn- pr-render-seq [buf items open close]
(buffer/push-string buf open)
(var first true)
(each x items
(if first (set first false) (buffer/push-string buf " "))
(pr-render buf x))
(buffer/push-string buf close))
(defn- pr-render-pairs [buf pairs]
(buffer/push-string buf "{")
(var first true)
(each pair pairs
(if first (set first false) (buffer/push-string buf ", "))
(pr-render buf (in pair 0))
(buffer/push-string buf " ")
(pr-render buf (in pair 1)))
(buffer/push-string buf "}"))
(defn- name-of
"Extract a plain name string from a string, symbol struct, or a namespace/var
table (reading its :name) — never recurses into the cyclic ns structure."
[x]
(cond
(nil? x) nil
(string? x) x
(and (struct? x) (= :symbol (get x :jolt/type))) (x :name)
(or (struct? x) (table? x)) (name-of (get x :name))
(string x)))
(defn- var-display
"Render a Jolt var as #'ns/name. A var's :meta/:ns refs are cyclic, so this
reads only its :name and :ns name — printing the var's pairs would loop."
[v]
(let [nm (name-of (v :name))
ns (name-of (v :ns))]
(if ns (string "#'" ns "/" nm) (string "#'" nm))))
(defn- pr-push-escaped
"Readable string body: escape per char-escapes (quote, backslash, \\n & co),
so pr-str round-trips through the reader (this was unescaped, jolt pre-r6)."
[buf s]
(each c (string/bytes s)
(if-let [esc (get pr-char-escapes c)]
(buffer/push-string buf esc)
(buffer/push-byte buf c))))
(set pr-render
(fn [buf v]
(cond
(nil? v) (buffer/push-string buf "nil")
(= true v) (buffer/push-string buf "true")
(= false v) (buffer/push-string buf "false")
(string? v) (do (buffer/push-string buf "\"") (pr-push-escaped buf v) (buffer/push-string buf "\""))
(buffer? v) (do (buffer/push-string buf "\"") (pr-push-escaped buf (string v)) (buffer/push-string buf "\""))
(keyword? v) (do (buffer/push-string buf ":") (buffer/push-string buf (string v)))
(core-char? v) (do (buffer/push-string buf "\\")
(buffer/push-string buf
(case (v :ch)
10 "newline" 32 "space" 9 "tab" 13 "return"
12 "formfeed" 8 "backspace" 0 "nul"
(char->string v))))
(number? v) (buffer/push-string buf (fmt-number v))
(and (struct? v) (= :symbol (v :jolt/type)))
(buffer/push-string buf (if (v :ns) (string (v :ns) "/" (v :name)) (v :name)))
(and (struct? v) (= :jolt/inst (v :jolt/type)))
(do (buffer/push-string buf "#inst \"") (buffer/push-string buf (inst->rfc3339 v))
(buffer/push-string buf "\""))
(= :jolt/namespace (get v :jolt/type))
(do (buffer/push-string buf "#namespace[")
(buffer/push-string buf (ns-display-name v))
(buffer/push-string buf "]"))
(and (table? v) (= :jolt/var (get v :jolt/type))) (buffer/push-string buf (var-display v))
(shape-rec? v)
(let [rtag (record-tag v)
pairs (map (fn [k] [k (shape-get v k nil)]) (shape-keys v))]
(cond
# a record shape-rec prints Clojure-style: #ns.Type{:k v, ...}
(and rtag print-method-cb (print-method-cb v (fn [piece] (buffer/push-string buf piece)))) nil
rtag (do (buffer/push-string buf (string "#" rtag))
(pr-render-pairs buf pairs))
(pr-render-pairs buf pairs)))
(core-sorted-map? v) (pr-render-pairs buf
(map (fn [e] [(vnth e 0) (vnth e 1)]) (sorted-entries-arr v)))
(core-sorted-set? v) (pr-render-seq buf (sorted-entries-arr v) "#{" "}")
(lazy-seq? v) (pr-render-seq buf (realize-for-iteration v) "(" ")")
(set? v) (pr-render-seq buf (phs-seq v) "#{" "}")
(phm? v) (pr-render-pairs buf (phm-entries v))
(pvec? v) (pr-render-seq buf (pv->array v) "[" "]")
(plist? v) (pr-render-seq buf (pl->array v) "(" ")")
(and (table? v) (get v :jolt/deftype))
(if (and print-method-cb (print-method-cb v (fn [piece] (buffer/push-string buf piece))))
nil
# Clojure's record syntax: #ns.Type{:k v, ...} (fields only, the
# deftype tag elided). This used to print the raw janet table.
(do
(buffer/push-string buf (string "#" (get v :jolt/deftype)))
(pr-render-pairs buf
(filter (fn [pair] (not= :jolt/deftype (in pair 0))) (pairs v)))))
(tuple? v) (pr-render-seq buf v "[" "]")
# mutable mode: arrays are vectors -> print with [] (else lists -> ())
(array? v) (if mutable? (pr-render-seq buf v "[" "]") (pr-render-seq buf v "(" ")"))
# Any remaining TAGGED value dispatches through print-method when the
# hook is wired: the io tier owns the cold renderings (uuid, regex,
# transient, channel — branches that used to live here), and user
# defmethods on any :jolt/* tag fire from inside nested values. Before
# the overlay loads (init-time error messages) these fall through to
# the raw pairs view below.
(and print-method-cb (get v :jolt/type)
(print-method-cb v (fn [piece] (buffer/push-string buf piece))))
nil
(struct? v) (pr-render-pairs buf (pairs v))
(table? v) (pr-render-pairs buf (pairs v))
true (buffer/push-string buf (string v)))))
(defn str-render-one
"Render one value with Clojure's `str`/.toString semantics (bare strings,
nil -> empty, keywords/symbols by name, collections via pr-render)."
[v]
(cond
(nil? v) ""
(string? v) v
(buffer? v) (string v)
(core-char? v) (char->string v)
(keyword? v) (string ":" (string v))
(and (struct? v) (= :symbol (v :jolt/type)))
(if (v :ns) (string (v :ns) "/" (v :name)) (v :name))
(and (struct? v) (= :jolt/uuid (v :jolt/type))) (v :str)
(and (struct? v) (= :jolt/inst (v :jolt/type))) (inst->rfc3339 v)
# a java.io.File renders as its path (Clojure's File.toString)
(and (table? v) (= :jolt/file (get v :jolt/type))) (get v :path)
# (str pattern) -> raw regex source (no #"" delimiters), so libraries that
# compose patterns via (re-pattern (str p1 ...)) work (Pattern.toString).
(and (table? v) (= :jolt/regex (get v :jolt/type))) (get v :source)
(= :jolt/namespace (get v :jolt/type)) (ns-display-name v)
(and (table? v) (= :jolt/var (get v :jolt/type))) (var-display v)
(number? v) (fmt-number v)
(= true v) "true"
(= false v) "false"
# a record/deftype with a custom Object/toString renders via it (Clojure's
# str/.toString semantics); plain records fall through to the data repr.
(if-let [s (and record-tostring-cb (record-tag v) (record-tostring-cb v))]
s
(let [buf @""] (pr-render buf v) (string buf)))))
(defn core-str [& xs]
(if (= 0 (length xs)) ""
(do
(var result @[])
(each x xs (array/push result (str-render-one x)))
(string/join result ""))))
(defn core-str-join
"clojure.string/join: stringify each element (Clojure semantics), then join."
[coll &opt sep]
(default sep "")
(let [items (realize-for-iteration coll)
parts @[]]
(each x items (array/push parts (str-render-one x)))
(string/join parts (str-render-one sep))))
(defn core-name
"Returns the name string of a keyword, symbol, or string (without namespace)."
[x]
(cond
(keyword? x) (let [s (string x) i (string/find "/" s)] (if i (string/slice s (+ i 1)) s))
(and (struct? x) (= :symbol (x :jolt/type))) (x :name)
(string? x) x
""))
(defn core-namespace
"Returns the namespace string of a keyword/symbol, or nil if none."
[x]
(cond
(keyword? x) (let [s (string x) i (string/find "/" s)] (if i (string/slice s 0 i) nil))
(and (struct? x) (= :symbol (x :jolt/type)))
(if (x :ns) (if (struct? (x :ns)) ((x :ns) :name) (string (x :ns))) nil)
nil))
(def core-subs
(fn [& args]
(when (not (or (= 2 (length args)) (= 3 (length args))))
(error "Wrong number of args passed to: subs"))
(let [s (args 0)
start (get args 1)]
(when (not (string? s)) (error (string "subs requires a string, got " (type s))))
(let [len (length s)
end (if (= 3 (length args)) (args 2) len)]
# Clojure validates bounds (no negative/from-end/clamping like Janet):
# 0 <= start <= end <= (count s).
(when (not (and (number? start) (number? end)
(= start (math/floor start)) (= end (math/floor end))
(>= start 0) (<= start end) (<= end len)))
(error "String index out of range"))
(string/slice s start end)))))
# ============================================================

View file

@ -1,313 +0,0 @@
# Jolt Core — arrays, bit ops, coercions, hash, atoms/refs
# Extracted from core.janet (jolt-nma8, phase 2b split).
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./regex)
(use ./config)
(use ./pv)
(use ./plist)
(use ./core_types)
(use ./core_coll)
(use ./core_print)
(use ./core_io)
# Java-style arrays — backed by Janet's C primitives. Byte arrays use Janet
# buffers (contiguous, O(1) indexed get/put — genuinely fast); object and
# numeric arrays use Janet arrays. aget/aset/alength/aclone work over both.
# ============================================================
# alength / aget / aset now live in the Clojure collection tier — count/nth reads
# and an aset write through jolt.host/ref-put!. The typed/object array constructors
# below stay native (they build the mutable backing).
(defn core-aclone [arr]
(cond
(buffer? arr) (buffer/slice arr)
(pvec? arr) (array ;(pv->array arr))
(array/slice arr)))
# Numeric / object arrays: (T-array size) | (T-array size init) | (T-array seq)
(defn- make-num-array [a rest init]
(if (number? a)
(array/new-filled a (if (> (length rest) 0) (in rest 0) init))
(array ;(realize-for-iteration a))))
(defn core-object-array [a & rest] (make-num-array a rest nil))
(defn core-int-array [a & rest] (make-num-array a rest 0))
(defn core-long-array [a & rest] (make-num-array a rest 0))
(defn core-short-array [a & rest] (make-num-array a rest 0))
(defn core-double-array [a & rest] (make-num-array a rest 0))
(defn core-float-array [a & rest] (make-num-array a rest 0))
(defn core-char-array [a & rest]
# JVM char-array also accepts a STRING/char-seq (char[] of its characters) —
# selmer's parse-str does (char-array template).
(cond
(string? a) (map make-char (string/bytes a))
(buffer? a) (map make-char (string/bytes (string a)))
(make-num-array a rest (make-char 0))))
(defn core-boolean-array [a & rest] (make-num-array a rest false))
# Byte arrays — Janet buffers (each element a 0..255 byte).
(defn core-byte-array [a & rest]
(if (number? a)
(buffer/new-filled a (band (if (> (length rest) 0) (in rest 0) 0) 0xff))
(let [b (buffer/new 0)]
(each x (realize-for-iteration a) (buffer/push-byte b (band x 0xff)))
b)))
(defn core-aset-byte [arr i v] (put arr i (band v 0xff)) v)
(defn core-aset-int [arr i v] (put arr i v) v)
(defn core-aset-long [arr i v] (put arr i v) v)
(defn core-aset-short [arr i v] (put arr i v) v)
(defn core-aset-double [arr i v] (put arr i v) v)
(defn core-aset-float [arr i v] (put arr i v) v)
(defn core-aset-char [arr i v] (put arr i v) v)
(defn core-aset-boolean [arr i v] (put arr i v) v)
(defn core-make-array [a & rest]
# (make-array len) or (make-array type len ...); ignore the type tag
(let [len (if (number? a) a (in rest 0))] (array/new-filled len nil)))
(defn core-into-array [a & rest]
(let [s (if (> (length rest) 0) (in rest 0) a)]
(array ;(realize-for-iteration s))))
(defn core-to-array [coll]
(def arr @[]) (each x (realize-for-iteration coll) (array/push arr x)) arr)
# to-array-2d lives in the Clojure collection tier (core/20-coll.clj).
# Array-element casts — identity on arrays; `bytes` coerces to a byte buffer.
(defn core-bytes [x] (if (buffer? x) x (core-byte-array x)))
(defn core-booleans [x] x)
(defn core-ints [x] x)
(defn core-longs [x] x)
(defn core-shorts [x] x)
(defn core-doubles [x] x)
(defn core-floats [x] x)
(defn core-chars [x] x)
# Scalar numeric coercions
(defn core-byte [x] (let [b (band (math/trunc x) 0xff)] (if (>= b 128) (- b 256) b)))
(defn core-short [x] (let [s (band (math/trunc x) 0xffff)] (if (>= s 0x8000) (- s 0x10000) s)))
# The masking unchecked-byte/short/char and float/double coercions live in
# the Clojure collection tier (core/20-coll.clj).
# 64-bit integers (Janet int/s64 — C-backed)
(defn core-bigint [x] (int/s64 x))
(defn core-biginteger [x] (int/s64 x))
# bigdec now lives in the Clojure collection tier (no BigDecimal: a double).
# Chunked seqs — Jolt does not chunk, so these are simple eager equivalents.
(defn core-chunk-buffer [capacity] @[])
(defn core-chunk-append [b x] (array/push b x) b)
(defn core-chunk [b] b)
# chunked-seq? now lives in the Clojure collection tier (always false on Jolt).
(defn core-chunk-first [s] (core-first s))
(defn core-chunk-rest [s] (core-rest s))
(defn core-chunk-next [s] (core-next s))
(defn core-chunk-cons [chunk rest] (core-concat (realize-for-iteration chunk) rest))
# More clojure.core: real implementations backed by existing Jolt machinery.
(defn core-boolean [x] (if x true false))
(defn core-cat [rf]
(fn [& a]
(case (length a)
0 (rf) 1 (rf (a 0))
(do (var acc (a 0)) (each x (realize-for-iteration (a 1)) (set acc (rf acc x))) acc))))
(defn core-reader-conditional [form splicing?]
@{:jolt/type :jolt/reader-conditional :form form :splicing? splicing?})
# reader-conditional? now lives in the Clojure collection tier (tagged-value predicate).
# sorted-map-by / sorted-set-by (and all other sorted-coll constructors and
# semantics) now live in the Clojure sorted tier (core/25-sorted.clj).
# array-seq / seque live in the Clojure collection tier (core/20-coll.clj).
# supers now lives in the Clojure collection tier (no class hierarchy: #{}).
(defn core-class [x]
(cond
# typed throwables carry their JVM class name (host_net's HTTP/SSL shims):
# (class e) -> "java.net.SocketTimeoutException" so (= ClassSym (class e)) holds
(and (table? x) (get x :class)) (get x :class)
(nil? x) nil (number? x) "java.lang.Number" (string? x) "java.lang.String"
(boolean? x) "java.lang.Boolean" (keyword? x) "clojure.lang.Keyword"
(function? x) "clojure.lang.IFn" (buffer? x) "[B"
(string (type x))))
# clojure-version / munge / test now live in the Clojure collection tier
# (core/20-coll.clj).
# ============================================================
# Bit operations (needed for persistent data structures)
# ============================================================
(def core-bit-and (fn [a b] (band a b)))
(def core-bit-or (fn [a b] (bor a b)))
(def core-bit-xor (fn [a b] (bxor a b)))
(def core-bit-not (fn [a] (bnot a)))
(def core-bit-shift-left (fn [x n] (blshift x n)))
(def core-bit-shift-right (fn [x n] (brshift x n)))
(def core-bit-clear (fn [x n] (band x (bnot (blshift 1 n)))))
(def core-bit-set (fn [x n] (bor x (blshift 1 n))))
(def core-bit-flip (fn [x n] (bxor x (blshift 1 n))))
(def core-bit-test (fn [x n] (not= 0 (band x (blshift 1 n)))))
(def core-bit-and-not (fn [a b] (band a (bnot b))))
(def core-unsigned-bit-shift-right (fn [x n] (brushift x n)))
# ============================================================
# Integer coercion
# ============================================================
(def core-int (fn [x] (if (core-char? x) (x :ch) (math/trunc x))))
(def core-long (fn [x] (if (core-char? x) (x :ch) (math/trunc x))))
(def core-double (fn [x] (* 1.0 (if (core-char? x) (x :ch) x))))
(def core-float core-double)
# num and the unchecked-*/promoting-' arithmetic live in the Clojure
# collection tier (core/20-coll.clj) — jolt numbers don't overflow.
(defn core-char [x]
"(char code-or-char) -> a character value."
(cond
(core-char? x) x
(number? x) (make-char (math/trunc x))
(string? x) (make-char (in x 0))
(error "char expects a number or character")))
# ============================================================
# Hash
# ============================================================
(def core-hash (fn [x] (hash x)))
# ============================================================
# Atom
# ============================================================
(defn core-atom
"Create an atom. Accepts optional :validator fn and :meta map."
[val & opts]
(var atm @{:jolt/type :jolt/atom :value val :watches @{} :validator nil})
(var i 0)
(while (< i (length opts))
(case (opts i)
:validator (put atm :validator (opts (+ i 1)))
:meta (let [m (opts (+ i 1))]
(var meta-tab @{})
(each k (keys m) (put meta-tab k (get m k)))
(table/setproto atm meta-tab)
(put atm :jolt/meta m)))
(+= i 2))
atm)
# atom? now lives in the Clojure collection tier (tagged-value predicate).
# Futures — run the body on a real OS thread (ev/thread) for true parallelism.
# Janet threads have separate heaps, so the thunk and the state it closes over are
# MARSHALLED (copied) to the worker thread and the result is marshalled back. A
# future therefore sees a *snapshot* of captured state and communicates only via
# its return value — mutating a captured atom does not propagate to the parent.
# Coordination uses two channels: a thread-chan carries the single [:ok v] /
# [:error e] result back, and a parent-local chan acts as a broadcast latch that
# is closed when the result lands so any number of deref-ers can unpark.
(defn core-future? [x] (and (table? x) (= :jolt/future (x :jolt/type))))
(defn core-future-call [thunk]
(def tc (ev/thread-chan 1)) # worker thread -> collector (shared, thread-safe)
(def latch (ev/chan)) # parent-local: closed when the result is in
(def fut @{:jolt/type :jolt/future :latch latch :cached false :res nil :cancelled false})
# Worker: compute on a fresh OS thread, send back a marshalled result. The give
# is guarded so a non-marshallable value can't strand deref-ers forever.
(ev/spawn-thread
(def res (try [:ok (thunk)] ([e] [:error e])))
(try (ev/give tc res)
([_] (ev/give tc [:error "future result is not marshallable across threads"]))))
# Collector: a parent-side fiber bridges the single result into the box and
# closes the latch to wake every waiter. If the future was already cancelled,
# the box is finalized — drop the late result and don't re-close the latch.
(ev/spawn
(def res (ev/take tc))
(when (not (fut :cancelled))
(put fut :res res)
(put fut :cached true)
(try (ev/chan-close latch) ([_] nil))))
fut)
(defn- future-result [fut]
(def res (fut :res))
(if (= :error (in res 0)) (error (in res 1)) (in res 1)))
# future-done? / future-cancelled? now live in the Clojure collection tier (pure
# reads of :cached/:cancelled). core-future? stays — deref/future-cancel call it.
# Janet OS threads can't be interrupted, so the worker still runs to completion
# in the background; we can only mark the *future* cancelled (done) so deref
# raises and realized?/future-done?/future-cancelled? reflect it. Returns false
# if the future has already completed (matching Clojure).
(defn core-future-cancel [x]
(if (and (core-future? x) (not (x :cached)) (not (x :cancelled)))
(do
(put x :cancelled true)
(put x :res [:error "future cancelled"])
(put x :cached true)
(try (ev/chan-close (x :latch)) ([_] nil))
true)
false))
# future macro: (future body...) -> (future-call (fn* [] body...))
(defn core-deref [ref & opts]
(cond
(and (table? ref) (= :jolt/reduced (ref :jolt/type)))
(ref :val)
(and (table? ref) (= :jolt/atom (ref :jolt/type)))
(ref :value)
(and (table? ref) (= :jolt/volatile (ref :jolt/type)))
(ref :val)
(and (table? ref) (= :jolt/delay (ref :jolt/type)))
(if (ref :realized) (ref :val)
(let [v ((ref :fn))] (put ref :val v) (put ref :realized true) v))
(and (table? ref) (= :jolt/future (ref :jolt/type)))
(if (empty? opts)
(do (when (not (ref :cached)) (ev/take (ref :latch))) (future-result ref))
# (deref future timeout-ms timeout-val): wait at most timeout-ms. The
# deadline cancels the parked take; if the result still hasn't landed we
# return the supplied timeout value (the future keeps running).
(let [timeout-val (in opts 1)]
(when (not (ref :cached))
(try (ev/with-deadline (/ (in opts 0) 1000) (ev/take (ref :latch))) ([_] nil)))
(if (ref :cached) (future-result ref) timeout-val)))
(and (table? ref) (= :jolt/var (ref :jolt/type)))
(ref :root)
ref))
(defn- atom-validate
"Call validator on atm. Returns the value if valid, errors otherwise."
[atm val]
(let [v (atm :validator)]
(if v
(if (v val) val
(error "Validator rejected value"))
val)))
(defn- atom-notify-watches
[atm old-val new-val]
(loop [[k w] :pairs (atm :watches)]
(w k atm old-val new-val)))
(defn core-reset! [atm val]
(let [old-val (atm :value)]
(atom-validate atm val)
(put atm :value val)
(atom-notify-watches atm old-val val)
val))
(defn core-swap! [atm f & args]
(var old-val (atm :value))
(var new-val (apply f old-val args))
(atom-validate atm new-val)
(put atm :value new-val)
(atom-notify-watches atm old-val new-val)
new-val)
# Atom peripheral ops (swap-vals!/reset-vals!/compare-and-set!/get-validator/
# add-watch/remove-watch/set-validator!) now live in the Clojure collection tier —
# composed over the native atom ops + jolt.host/ref-put!. atom/swap!/reset!/deref
# and the atom-validate/atom-notify-watches helpers stay native (compiler-critical).
# ============================================================

View file

@ -1,470 +0,0 @@
# Jolt Core — vector helpers, predicates, math, comparison, equality
# Extracted from core.janet (jolt-nma8, phase 2b split).
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./regex)
(use ./config)
(use ./pv)
(use ./plist)
# ------------------------------------------------------------
# Vector representation helpers
#
# In immutable mode a vector value is a structural-sharing persistent vector
# (pvec); in mutable mode it is a plain Janet array. Janet tuples may also still
# appear (e.g. literals that have not been routed through make-vec), so the read
# helpers below accept tuple, pvec and (mutable mode) array uniformly.
# ------------------------------------------------------------
(defn jvec?
"True when x is a vector VALUE. In immutable mode that is a persistent vector
or tuple; in mutable mode vectors are plain arrays (so vectors and lists share
one fast representation — `vector?` is true for both)."
[x]
(if mutable?
(or (array? x) (tuple? x))
(or (tuple? x) (pvec? x))))
(defn vcount [x] (if (pvec? x) (pv-count x) (length x)))
(defn vnth [x i] (if (pvec? x) (pv-nth x i) (in x i)))
(defn vview
"An indexed (tuple/array) view of a vector value, for iteration/slicing."
[x]
(if (pvec? x) (pv->array x) x))
(defn make-vec
"Build a vector value from a Janet array/tuple of elements, honoring the
build-time collection mode."
[xs]
(if mutable? (array ;xs) (pv-from-indexed xs)))
(defn core-transient?
"True when x is a transient (a mutable scratch collection). See `transient`."
[x]
(and (table? x) (= :jolt/transient (get x :jolt/type))))
# Sorted-coll tag checks + entries view, defined this early because canon-key,
# empty?, and jolt-equal? (all below) need them. The sorted-coll SEMANTICS are
# pure Clojure (core/25-sorted.clj); see the dispatch section further down.
# SEED-TWIN: sorted-map?/sorted-set?/sorted? also live in the overlay
# (jolt-core/clojure/core/25-sorted.clj); the overlay copies are the public ones
# (NOT in core-bindings). These seed copies exist only for earlier-tier seed
# dispatch. Change both copies together — docs/seed-overlay-registry.md.
(defn core-sorted-map? [x] (and (table? x) (= :jolt/sorted-map (x :jolt/type))))
(defn core-sorted-set? [x] (and (table? x) (= :jolt/sorted-set (x :jolt/type))))
(defn core-sorted? [x] (or (core-sorted-map? x) (core-sorted-set? x)))
# The comparator-ordered entries as a Janet array (entries are jolt vectors:
# pvecs in immutable mode, arrays in mutable mode) — for the seed's printers/
# equality. Materialized from the red-black tree via the coll's own :entries op
# (jolt-0hbr); the old sorted-VECTOR rep is read directly as a fallback.
(defn sorted-entries-arr [coll]
(def ef (let [ops (coll :ops)] (and ops (ops :entries))))
(def e (if ef (ef coll) (coll :entries)))
(if (pvec? e) (pv->array e) e))
# Lazy cell chain over an indexed (tuple/array) collection, walking by INDEX —
# O(1) per step. Slicing the remainder per step (the old shape) made every
# full walk over a concrete collection O(n^2).
(defn indexed-cells [t i]
(if (>= i (length t)) nil
@[(in t i) (fn [] (indexed-cells t (+ i 1)))]))
# A nil element/key/value nested in a collection used AS a map key would be
# dropped when canon-key re-keys a native Janet table (a Janet struct can't hold a
# nil key or value), so #{nil 1} would canonicalize like #{1} and collide as a key
# (jolt-zcm9). Box a nested nil to a marker. It must be VALUE-hashable: the
# canonical struct becomes a long-lived phm key whose hash has to survive the
# marshal/snapshot/fork that init-cached uses, so an identity-hashed mutable table
# (like the transient sentinel below) won't do — its hash isn't preserved across
# unmarshal. An interned keyword hashes by content. Collision risk is only a real
# element equal to this exact keyword — the same negligible class as canon-key's
# existing set/map struct aliasing.
(def canon-nil (keyword "jolt.lang/canonical-nil"))
(defn- canon-box [c] (if (nil? c) canon-nil c))
# Canonicalize a collection key/element to a value-hashable Janet struct/tuple so
# the PHM/PHS treat value-equal maps/vectors as the same key (Janet hashes tables
# by identity otherwise). Installed into phm via set-canonicalize-key!.
(var canon-key nil)
(set canon-key
(fn [k]
(cond
(pvec? k) (tuple ;(map canon-key (pv->array k)))
(plist? k) (tuple ;(map canon-key (pl->array k)))
(set? k) (do (def t @{}) (each e (phs-seq k) (put t (canon-box (canon-key e)) true)) (table/to-struct t))
(phm? k) (do (def t @{}) (each pair (phm-entries k) (put t (canon-box (canon-key (in pair 0))) (canon-box (canon-key (in pair 1))))) (table/to-struct t))
# sorted colls canonicalize like their unsorted counterparts, so
# (get {(sorted-map :a 1) :hit} {:a 1}) finds the key
(core-sorted-map? k) (do (def t @{}) (each e (sorted-entries-arr k) (put t (canon-box (canon-key (vnth e 0))) (canon-box (canon-key (vnth e 1))))) (table/to-struct t))
(core-sorted-set? k) (do (def t @{}) (each x (sorted-entries-arr k) (put t (canon-box (canon-key x)) true)) (table/to-struct t))
(and (table? k) (get k :jolt/deftype))
(do (def t @{}) (each kk (keys k) (when (not= kk :jolt/deftype) (put t kk (canon-box (canon-key (get k kk)))))) (table/to-struct t))
(struct? k) (do (def t @{}) (each kk (keys k) (put t (canon-box (canon-key kk)) (canon-box (canon-key (get k kk))))) (table/to-struct t))
(array? k) (tuple ;(map canon-key k))
(tuple? k) (tuple ;(map canon-key k))
k)))
(set-canonicalize-key! canon-key)
# Janet tables silently drop a nil key (put/get with nil is a no-op), but Clojure
# maps allow a nil key. The transient map/set keys its native table by canon-key,
# which returns nil only for nil input — so route nil to a unique sentinel. This
# one is a fresh mutable table (canon-key never produces one, so no collision); it
# is fine here because a transient's native table is built and consumed within one
# operation and never crosses a marshal boundary, so identity hashing is stable.
(def tbl-nil-key @{})
(defn tbl-key [k] (if (nil? k) tbl-nil-key (canon-key k)))
# A transient SET stores `(tbl-key x) -> x`, i.e. the member IS the table value. A
# nil member can't be a Janet table value either (put with a nil value drops the
# entry), so box nil as the same sentinel and unbox on read-back.
(defn tbl-box [x] (if (nil? x) tbl-nil-key x))
(defn tbl-unbox [v] (if (= v tbl-nil-key) nil v))
# All [k v] entries of a map (struct or phm), nil-valued keys included. Use this
# instead of (keys (phm-to-struct m)) — phm-to-struct drops keys whose value is
# nil, which is exactly what Clojure maps must keep.
(defn map-entries-of [m]
(if (phm? m) (phm-entries m) (map (fn [k] [k (in m k)]) (keys m))))
# assoc one entry onto a map value (struct or phm), preserving a nil key/value and
# value-comparing collection keys (promotes a struct to a phm when needed). A
# single-entry core-assoc usable by fns defined before core-assoc itself.
(defn map-assoc1 [m k v]
(cond
(phm? m) (phm-assoc m k v)
(or (nil? k) (nil? v) (table? k) (array? k))
(do (var p (make-phm)) (each ek (keys m) (set p (phm-assoc p ek (in m ek)))) (phm-assoc p k v))
(do (def t (merge @{} m)) (put t k v) (table/to-struct t))))
# Build a map from a flat [k v k v ...] array: a phm when any key/value is nil or
# a key is a collection (value hashing); a struct otherwise. One O(n) pass.
(defn- kvs->map [kvs]
(var need-phm false) (var i 0)
(while (< i (length kvs))
(let [k (in kvs i) v (in kvs (+ i 1))]
(when (or (nil? k) (nil? v) (table? k) (array? k)) (set need-phm true)))
(+= i 2))
(if need-phm
(do (var m (make-phm)) (var j 0)
(while (< j (length kvs)) (set m (phm-assoc m (in kvs j) (in kvs (+ j 1)))) (+= j 2)) m)
(struct ;kvs)))
(defn realize-for-iteration [c]
"Normalize a seqable to a Janet array/tuple for iteration: pvec -> array,
set -> seq, lazy-seq -> realized array; others pass through. Warning: will
loop on infinite lazy-seqs. Terminates on the empty cell, not on nil."
(cond
# nil is an empty seq in Clojure — iterating it yields nothing.
(nil? c) @[]
(shape-rec? c) (map (fn [k] (tuple k (shape-get c k nil))) (shape-keys c))
(pvec? c) (pv->array c)
(plist? c) (pl->array c)
(set? c) (phs-seq c)
(phm? c) (phm-entries c)
# sorted colls iterate their comparator-ordered entries/elements
(core-sorted? c) (sorted-entries-arr c)
# a string is a seqable of CHARS in Clojure (not bytes/1-char strings) — mirror
# core-seq so vec/set/into over a string agree with seq (jolt-dl4s)
(string? c) (tuple ;(map make-char (string/bytes c)))
# byte array (Janet buffer) -> array of byte values
(buffer? c) (let [a @[]] (each x c (array/push a x)) a)
# struct map literal (no :jolt/type marker — not a symbol/char) -> entries
(and (struct? c) (nil? (get c :jolt/type))) (map (fn [k] (tuple k (get c k))) (keys c))
# raw host table (System/getenv, os/environ) — also a map: entries
(and (table? c) (nil? (get c :jolt/type)) (nil? (get c :jolt/deftype)))
(map (fn [k] (tuple k (get c k))) (keys c))
(lazy-seq? c)
(do
(var items @[])
(var cur c)
(var go true)
(while go
(let [cell (realize-ls cur)]
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell)))
(set go false)
(do
(array/push items (in cell 0))
(let [rt (in cell 1)]
(if (nil? rt) (set go false) (set cur (ls-rest-cached cur rt))))))))
items)
c))
# Syntax-quote form builders. The syntax-quote lowering (evaluator) emits calls to
# these so a `(...)/`[...] body is plain compilable code instead of an interpreted
# special form. A list FORM is a Janet array, a vector FORM a tuple (the reader's
# representation), so these build those types. Each concat part is either a 1-elem
# wrap (__sq1, a non-spliced item) or a spliced seq (~@), flattened in order.
(defn core-sq1 [x] @[x])
(defn core-sqcat [& parts]
(def r @[])
(each p parts (each x (realize-for-iteration p) (array/push r x)))
r)
(defn core-sqvec [& parts]
(def r @[])
(each p parts (each x (realize-for-iteration p) (array/push r x)))
(tuple/slice r))
# Map builder: parts are alternating k v (no splicing in map syntax-quote).
(defn core-sqmap [& parts]
# A syntax-quoted map template is Clojure's array-map case: construction
# order is source order and must survive into the built map, which usually
# becomes a FORM whose entries the evaluator walks (jolt-p3c). Same
# carriers as the reader: struct prototype / phm field.
(def kvs (array ;parts))
(def m (kvs->map kvs))
(cond
(struct? m) (struct/with-proto (struct :jolt/kv-order (tuple/slice kvs)) ;kvs)
(table? m) (do (put m :jolt/kv-order (tuple/slice kvs)) m)
m))
# Set builder: like core-sqvec but yields a set, so `#{~@a} splices into a set.
(defn core-sqset [& parts]
(def r @[])
(each p parts (each x (realize-for-iteration p) (array/push r x)))
(apply make-phs r))
# ============================================================
# Predicates
# ============================================================
# SEED-TWIN: char? is also defined in the overlay (jolt-core/clojure/core/
# 20-coll.clj) and the overlay copy is the public one (NOT in core-bindings).
# This seed copy is internal type dispatch only. docs/seed-overlay-registry.md.
(defn core-char? [x] (and (struct? x) (= :jolt/char (x :jolt/type))))
(defn char-code [c] (c :ch))
(defn char->string [c] (string/from-bytes (c :ch)))
(defn core-nil? [x] (nil? x))
(defn core-not [x] (if x false true))
# some? / true? / false? now live in the Clojure collection tier.
(defn core-string? [x] (string? x))
(defn core-number? [x] (number? x))
(defn core-fn? [x] (or (function? x) (cfunction? x)))
(defn core-keyword? [x] (keyword? x))
(defn core-symbol? [x] (and (struct? x) (= :symbol (x :jolt/type))))
# A record shape-rec is a Janet tuple (jvec? true), but a record is NOT a vector
# in Clojure — `(vector? record)` is false, and so is `(sequential? record)`.
# Excluding it here keeps map-destructuring of a record off the `& {:keys}` kwargs
# coerce path (which does `(apply hash-map x)` for a sequential x). jvec? itself
# stays as-is for internal representation dispatch.
(defn core-vector? [x] (and (jvec? x) (not (shape-rec? x))))
# map? is STRICT: a plain struct map literal, a phm, a sorted map, or a record.
# Tagged structs (symbols/chars/uuids — anything with :jolt/type) are VALUES,
# not maps. (sorted-map? is defined later, so the table check is inlined.)
(defn core-map? [x]
(or (shape-rec? x)
(phm? x)
(and (struct? x) (nil? (get x :jolt/type)))
(and (table? x)
(or (not (nil? (get x :jolt/deftype)))
(= :jolt/sorted-map (get x :jolt/type))))))
# seq? is true only for actual sequences (lists, lazy-seqs) — NOT vectors, which
# are not ISeq in Clojure. (A Janet array represents a Clojure list/seq result.)
(defn core-seq? [x] (or (array? x) (plist? x) (lazy-seq? x)))
# coll? mirrors map?'s strictness for structs/tables, and includes the sorted
# collections and records (IPersistentCollection in Clojure).
(defn core-coll? [x]
(or (array? x) (tuple? x) (pvec? x) (plist? x) (phm? x) (set? x) (lazy-seq? x)
(and (struct? x) (nil? (get x :jolt/type)))
(and (table? x)
(or (not (nil? (get x :jolt/deftype)))
(= :jolt/sorted-map (get x :jolt/type))
(= :jolt/sorted-set (get x :jolt/type))))))
(defn core-identical? [a b] (= a b))
# Strictness helpers: like Clojure, numeric ops reject non-numbers, and the
# integer ops (odd?/even?) reject non-integers (incl. infinities, NaN, fractions).
(defn- finite-num? [x] (and (number? x) (= x x) (< (if (< x 0) (- x) x) math/inf)))
(defn- need-num [x op]
(if (number? x) x (error (string op " requires a number, got " (type x)))))
(defn- need-int [x op]
(if (and (number? x) (= x x) (< (if (< x 0) (- x) x) math/inf) (= x (math/floor x))) x
(error (string op " requires an integer"))))
# zero? / pos? live in the syntax tier (core/00-syntax.clj) — empty? and the
# analyzer use them; neg? lives in the collection tier (20-coll.clj).
# even?/odd? are PERF-WALL residents: (filter even? ...) is idiomatic and the
# overlay versions cost an extra call layer per element (seq-pipe bench 4x).
(defn core-even? [n] (= 0 (% (need-int n "even?") 2)))
(defn core-odd? [n] (not= 0 (% (need-int n "odd?") 2)))
# Finite integral number: NaN and the infinities are NOT integers (floor of
# inf is inf, so the naive floor check wrongly accepted them).
(defn core-integer? [x]
(and (number? x) (= x x)
(< x math/inf) (> x (- math/inf))
(= x (math/floor x))))
(defn core-list? [x] (or (plist? x) (and (array? x) (not (get x :jolt/type)))))
# empty? now lives in the syntax tier (core/00-syntax.clj): the expanders
# call it, so it must exist before the kernel tier compiles.
# every? lives in the syntax tier (core/00-syntax.clj) — the analyzer uses it;
# the canonical seq/first/next walk short-circuits lazy seqs the same way.
# ============================================================
# Math — Clojure semantics (variadic, / with one arg = reciprocal)
# ============================================================
(def core-+ (fn [& args] (if (= 0 (length args)) 0 (+ ;args))))
(def core-sub
(fn [& args]
(if (= 0 (length args))
(error "Wrong number of args (0) passed to: -")
(apply - args))))
(def core-* (fn [& args] (if (= 0 (length args)) 1 (* ;args))))
(def core-/
(fn [& args]
(case (length args)
0 (error "Wrong number of args (0) passed to: /")
1 (/ 1 (args 0))
(apply / args))))
(def core-inc inc)
(def core-dec dec)
# Clojure integer division: quot truncates toward zero; rem matches the sign of
# the dividend; mod matches the sign of the divisor (floored).
(def core-quot (fn [n d]
(when (or (not (finite-num? n)) (not (finite-num? d))) (error "quot requires finite numbers"))
(when (= d 0) (error "Divide by zero"))
(let [q (/ n d)] (if (< q 0) (math/ceil q) (math/floor q)))))
(def core-rem (fn [n d] (- n (* (core-quot n d) d))))
(def core-mod (fn [n d]
(let [m (core-rem n d)]
(if (or (= m 0) (= (> n 0) (> d 0))) m (+ m d)))))
# max / min now live in the Clojure collection tier (canonical pairwise
# >/<, so non-numbers throw and NaN behaves as on the JVM).
(defn core-rand [& n] (let [r (math/random)] (if (empty? n) r (* r (in n 0)))))
# rand-int / shuffle / random-uuid now live in the Clojure collection tier
# over the rand host seam (canonical: rand-int truncates toward zero).
# ============================================================
# Comparison
# ============================================================
(defn- eq-seqable
"If x is a Clojure sequential (vector/list/lazy-seq), return its elements as
an array; otherwise nil. Lets = compare across tuple/array/lazy-seq."
[x]
(cond
# a shape-rec is a MAP, not a sequence, even though it is a tuple
(shape-rec? x) nil
(lazy-seq? x) (realize-for-iteration x)
(pvec? x) (pv->array x)
(plist? x) (pl->array x)
(tuple? x) x
(array? x) x
nil))
(defn- eq-map-pairs
"Return [k v] pairs for a map-like value (phm/sorted-map/struct/table), else nil."
[x]
(cond
# a record shape-rec returns nil so equality falls to deep=, which is
# type-aware (the descriptor is interned per type): a record equals only a
# same-type record, never a plain map — mirroring the :jolt/deftype table
# form below. A plain-map shape-rec compares by pairs.
(shape-rec? x) (if (record-tag x) nil (map (fn [k] @[k (shape-get x k nil)]) (shape-keys x)))
(phm? x) (phm-entries x)
# sorted-map equals any map with the same pairs (representation-agnostic, as
# in Clojure); sorted-set is handled by the set branch of jolt-equal?
(core-sorted-map? x) (map (fn [e] @[(vnth e 0) (vnth e 1)]) (sorted-entries-arr x))
(core-sorted-set? x) nil
(and (table? x) (get x :jolt/deftype)) nil
(struct? x) (pairs x)
(table? x) (pairs x)
nil))
# Elements of a set-like value (phs or sorted-set) as an array, else nil.
(defn- eq-set-elems [x]
(cond
(set? x) (phs-seq x)
(core-sorted-set? x) (sorted-entries-arr x)
nil))
(var jolt-equal? nil)
(set jolt-equal?
(fn [a b]
(let [sa (eq-seqable a) sb (eq-seqable b)]
(cond
# both sequential: compare element-wise (vectors/lists/lazy-seqs equal)
(and sa sb)
(if (= (length sa) (length sb))
(do (var ok true) (var i 0)
(while (and ok (< i (length sa)))
(unless (jolt-equal? (in sa i) (in sb i)) (set ok false))
(++ i))
ok)
false)
(or sa sb) false
# sets (phs or sorted-set, in any combination)
(or (set? a) (set? b) (core-sorted-set? a) (core-sorted-set? b))
# value-based: same size and every element of a is value-equal to some
# element of b (so #{ {:a 1} } equals #{ (hash-map :a 1) } regardless of
# the elements' underlying representations)
(let [ea (eq-set-elems a) eb (eq-set-elems b)]
(if (and ea eb (= (length ea) (length eb)))
(do
(var ok true)
(each x ea
(unless (some (fn [y] (jolt-equal? x y)) eb) (set ok false)))
ok)
false))
# maps: compare key/value pairs recursively, order-independent
true
(let [pa (eq-map-pairs a) pb (eq-map-pairs b)]
(if (or pa pb)
(if (and pa pb (= (length pa) (length pb)))
(do (var ok true)
(each pair pa
(let [k (in pair 0) v (in pair 1)
found (do (var fv :jolt/none)
(each p2 pb (when (jolt-equal? k (in p2 0)) (set fv (in p2 1))))
fv)]
(unless (and (not= found :jolt/none) (jolt-equal? v found)) (set ok false))))
ok)
false)
(deep= a b)))))))
(defn core-= [& args]
(if (< (length args) 2) true
(do
(var ok true)
(var i 0)
(while (and ok (< i (dec (length args))))
(unless (jolt-equal? (args i) (args (+ i 1))) (set ok false))
(++ i))
ok)))
# not= lives in the syntax tier (core/00-syntax.clj) — the kernel uses it.
# Comparisons are variadic: (< a b c) means a < b < c.
(defn- chain-cmp [op opname xs]
# 1-arity (e.g. (< x)) is true regardless of x and does no type check.
(when (>= (length xs) 2) (each x xs (need-num x opname)))
(var ok true) (var i 0)
(while (and ok (< i (dec (length xs))))
(unless (op (in xs i) (in xs (+ i 1))) (set ok false))
(++ i))
ok)
(defn core-< [& xs] (chain-cmp < "<" xs))
(defn core-> [& xs] (chain-cmp > ">" xs))
(defn core-<= [& xs] (chain-cmp <= "<=" xs))
(defn core->= [& xs] (chain-cmp >= ">=" xs))
# ============================================================

View file

@ -1,300 +0,0 @@
# deps.edn resolution for Jolt.
#
# Resolve git and :local/root dependencies from a deps.edn into a list of source
# directories, which the loader then searches (see evaluator/find-ns-file). We
# reuse jpm's git fetch + cache (jpm/pm) rather than shipping a package manager.
# Maven (:mvn/version) deps are ignored — git only, pure clj/cljc only.
#
# jpm is loaded lazily (require, not import) so it's needed only at resolve time
# (dev/build), never embedded in the shipped binary.
(import ./reader :as reader)
# Read deps.edn with Jolt's reader (not Janet's parse) so EDN `;` line comments
# are handled. It returns plain Janet data — structs with keyword keys, tuples —
# which we walk directly. (#{} sets and tagged literals aren't expected in the
# :deps/:paths we read.)
(defn read-edn [path]
(when (os/stat path)
(try (reader/parse-string (slurp path)) ([_] nil))))
(defn- jpm-fn [mod sym]
(get-in (require mod) [sym :value]))
(defn- ensure-jpm-config [tree]
((jpm-fn "jpm/config" 'load-default))
(setdyn :modpath tree)
(setdyn :gitpath (dyn :gitpath "git")))
(defn- clone-git [spec]
# spec is a deps.edn dep value: {:git/url ... :git/sha/:git/tag ...}
(def resolve-bundle (jpm-fn "jpm/pm" 'resolve-bundle))
(def download-bundle (jpm-fn "jpm/pm" 'download-bundle))
# Run git silenced (jpm's shell honors :silent): its checkout chatter
# ("HEAD is now at …") otherwise lands on STDOUT and corrupts the
# documented `JOLT_PATH=$(jolt-deps path)` capture. Progress goes to stderr.
(eprintf "jolt-deps: fetching %s @ %s"
(get spec :git/url) (or (get spec :git/sha) (get spec :git/tag) "HEAD"))
(with-dyns [:silent true]
(def b (resolve-bundle {:url (get spec :git/url)
:sha (get spec :git/sha)
:tag (get spec :git/tag)
:shallow false}))
(download-bundle (b :url) (b :type) (b :tag) (b :shallow))))
(defn- src-roots
"Source dirs of a project/dep at `dir`: its deps.edn :paths joined to dir
(default [\"src\"])."
[dir edn]
(map |(string dir "/" $) (or (and edn (get edn :paths)) ["src"])))
# --- user config + aliases (tools.deps-shaped, scoped to git/:local) -----------
(defn config-dir
"User-level config dir: $JOLT_CONFIG, else $XDG_CONFIG_HOME/jolt, else
~/.jolt — the same fallback chain the Clojure CLI uses for ~/.clojure."
[]
(or (os/getenv "JOLT_CONFIG")
(when-let [x (os/getenv "XDG_CONFIG_HOME")]
(when (> (length x) 0) (string x "/jolt")))
(string (os/getenv "HOME") "/.jolt")))
(defn- merge-per-key [a b] # dictionary union, b's entries win
(def out @{})
(each m [a b] (when (dictionary? m) (eachp [k v] m (put out k v))))
out)
# Reader symbols carry position metadata, so any map keyed by SYMBOLS (deps
# libs, task names) must be re-keyed by name before merging or deduping.
(defn- sym-name [x]
(if (and (dictionary? x) (get x :name))
(if-let [ns (get x :ns)]
(string ns "/" (get x :name))
(get x :name))
(string x)))
(defn- ensure-jpm-dep
"A :jpm/module dep declares a janet module installed through jpm (e.g.
spork/http). jolt-deps doesn't manage janet packages — jpm does — so this
just verifies the module is importable, optionally running `jpm install
<:jpm/install>` once when it isn't, and fails with the install hint
otherwise. Contributes no source roots; the janet.* bridge autoloads the
module at first use."
[lib spec]
(def mod (get spec :jpm/module))
(defn importable? [] ((protect (require mod)) 0))
(unless (importable?)
(when-let [pkg (get spec :jpm/install)]
(eprintf "jolt-deps: %s: jpm module %s missing — running `jpm install %s`"
(sym-name lib) mod pkg)
(os/execute ["jpm" "install" pkg] :p))
(unless (importable?)
(errorf "%s: janet module %s is not importable. Install it with `jpm install %s` (jolt-deps leaves janet packages to jpm)."
(sym-name lib) mod (or (get spec :jpm/install) mod)))))
(defn- merge-by-name [a b] # union of symbol-keyed dictionaries, b wins
(def out @{})
(each m [a b] (when (dictionary? m) (eachp [k v] m (put out (sym-name k) v))))
out)
(defn- mkdirs [p]
(def abs (string/has-prefix? "/" p))
(var acc nil)
(each seg (filter |(not= "" $) (string/split "/" p))
(set acc (cond (nil? acc) (if abs (string "/" seg) seg) (string acc "/" seg)))
(unless (os/stat acc) (os/mkdir acc))))
(defn- default-tree
"Where git clones land when no tree is given: $JOLT_GITLIBS, else
(config-dir)/gitlibs — a global, sha-immutable cache shared across projects
(the tools.gitlibs ~/.gitlibs model), not a per-project ./jpm_tree."
[]
(def g (os/getenv "JOLT_GITLIBS"))
(if (and g (> (length g) 0)) g (string (config-dir) "/gitlibs")))
(defn load-config
"The project deps.edn merged over the user-level one (config-dir)/deps.edn.
tools.deps merge semantics: scalar keys and :paths replace (project wins),
:deps and :aliases merge per key with the project winning. Relative
:local/root in the USER file is resolved against the cwd — prefer absolute
paths there."
[deps-edn-path]
(def proj (read-edn deps-edn-path))
(def user (read-edn (string (config-dir) "/deps.edn")))
(if (and (nil? user) (nil? proj))
nil
# normalize even when only one file exists: :deps/:tasks come back keyed
# by NAME (reader symbols carry position metadata and never compare equal)
(let [u (when (dictionary? user) user)
p (when (dictionary? proj) proj)
out (merge-per-key u p)]
(put out :deps (merge-by-name (and u (get u :deps)) (and p (get p :deps))))
(put out :aliases (merge-per-key (and u (get u :aliases)) (and p (get p :aliases))))
(put out :tasks (merge-by-name (and u (get u :tasks)) (and p (get p :tasks))))
out)))
(defn combine-aliases
"Combine the selected alias keywords against `edn`'s :aliases:
:extra-paths and :extra-deps accumulate in order, :main-opts is last-wins
(the tools.deps CLI rules). Unknown alias -> error."
[edn aliases]
(def als (or (and (dictionary? edn) (get edn :aliases)) {}))
(def extra-paths @[])
(def extra-deps @{})
(var main-opts nil)
(each a (or aliases [])
(def spec (get als a))
(when (nil? spec) (error (string "unknown alias: " a)))
(each p (or (get spec :extra-paths) []) (array/push extra-paths p))
(when (dictionary? (get spec :extra-deps))
(eachp [lib coord] (get spec :extra-deps) (put extra-deps lib coord)))
(when-let [mo (get spec :main-opts)]
(set main-opts (tuple ;(map string mo)))))
{:extra-paths extra-paths :extra-deps extra-deps :main-opts main-opts})
(defn alias-main-opts
"The :main-opts the selected aliases produce (last alias with the key wins),
or nil. Reads the merged user+project config."
[deps-edn-path aliases]
(get (combine-aliases (load-config deps-edn-path) aliases) :main-opts))
(defn resolve-deps
"Resolve the git/:local deps of `deps-edn-path` into an ordered, de-duplicated
array of source dirs (the project's own :paths first, then each dependency's,
transitively). `tree` is where jpm's clone cache lives (default ./jpm_tree).
`aliases` (keywords) pull :extra-paths/:extra-deps from the merged config's
:aliases. The user-level deps.edn (see load-config) merges under the project."
[deps-edn-path &opt tree aliases]
(default tree (default-tree))
(mkdirs tree)
(ensure-jpm-config tree)
(def edn (load-config deps-edn-path))
(def extra (combine-aliases edn aliases))
(def roots @[])
(def seen @{}) # lib name -> chosen coordinate (for conflict reporting)
(defn add-root [r] (unless (index-of r roots) (array/push roots r)))
(defn coord-str [spec]
(cond
(and (dictionary? spec) (get spec :local/root))
(string ":local/root " (get spec :local/root))
(and (dictionary? spec) (get spec :git/url))
(string (get spec :git/url) " @ " (or (get spec :git/sha) (get spec :git/tag)))
(string/format "%j" spec)))
(defn coord= [a b]
(and (deep= (get a :local/root) (get b :local/root))
(deep= (get a :git/url) (get b :git/url))
(deep= (get a :git/sha) (get b :git/sha))
(deep= (get a :git/tag) (get b :git/tag))
(deep= (get a :jpm/module) (get b :jpm/module))))
(def queue @[])
(defn discover [lib spec base-dir]
(def k (sym-name lib))
(if-let [prev (get seen k)]
(unless (coord= prev spec)
(eprintf "WARNING: %s: conflicting coordinates — using %s, ignoring %s"
k (coord-str prev) (coord-str spec)))
(do
(put seen k spec)
(when (and (dictionary? spec) (get spec :jpm/module))
(ensure-jpm-dep lib spec))
(def dir
(cond
(and (dictionary? spec) (get spec :git/url))
# :deps/root (tools.deps): the project lives in a subdirectory
# of the repo — monorepos like ring-clojure/ring.
(let [cloned (clone-git spec)
root (get spec :deps/root)]
(if root (string cloned "/" root) cloned))
(and (dictionary? spec) (get spec :local/root))
(let [lr (get spec :local/root)]
(if (string/has-prefix? "/" lr) lr (string base-dir "/" lr)))
nil)) # :mvn/* and anything else: skip
(when dir
(def dep-edn (read-edn (string dir "/deps.edn")))
(each r (src-roots dir dep-edn) (add-root r))
(array/push queue [dep-edn dir])))))
# the project's own paths (+ alias extra paths) lead the roots
(each r (src-roots (os/cwd) edn) (add-root r))
(each pp (extra :extra-paths) (add-root (string (os/cwd) "/" pp)))
# breadth-first: every top-level dep registers before any transitive dep —
# so a top-level coordinate always wins, matching tools.deps. Alias
# :extra-deps go first: a selected alias's pin beats the project's.
(eachp [lib spec] (extra :extra-deps)
(discover lib spec (os/cwd)))
(eachp [lib spec] (or (and (dictionary? edn) (get edn :deps)) {})
(discover lib spec (os/cwd)))
(while (> (length queue) 0)
(def [dep-edn dir] (get queue 0))
(array/remove queue 0)
(when (dictionary? dep-edn)
(eachp [lib spec] (or (get dep-edn :deps) {})
(discover lib spec dir))))
roots)
(defn resolve-deps-cached
"Like resolve-deps, but caches the resolved roots in the tree keyed on a hash
of the project deps.edn + the user deps.edn + the selected aliases, so an
unchanged config resolves without re-fetching."
[deps-edn-path &opt tree aliases]
(default tree (default-tree))
(when (os/stat deps-edn-path)
# the roots depend on the PROJECT (config + aliases), so their cache is
# project-local like tools.deps' .cpcache; the clone tree stays global
(os/mkdir ".cpcache")
(def cache-file ".cpcache/jolt-deps.jdn")
(def user-path (string (config-dir) "/deps.edn"))
# The raw key material, not (hash …): janet's hash is seeded per process,
# so a hashed key never matches across invocations and the cache never hit.
(def key [(slurp deps-edn-path)
(or (when (os/stat user-path) (slurp user-path)) "")
(string/format "%j" (map string (or aliases [])))])
(def cached (when (os/stat cache-file) (try (parse (slurp cache-file)) ([_] nil))))
(if (and cached (deep= key (get cached :key)))
(get cached :roots)
(let [roots (resolve-deps deps-edn-path tree aliases)]
(spit cache-file (string/format "%j" {:key key :roots roots}))
roots))))
(defn project-source-roots
"The project's OWN source roots — its deps.edn :paths plus any alias
:extra-paths, joined to cwd — as opposed to dependency roots. These are the
'app' namespaces: the runtime scopes the whole-program inference fixpoint to
them (JOLT_APP_PATHS) so a dep-heavy app's startup doesn't re-infer every
transitive dependency namespace (jolt-87e)."
[deps-edn-path &opt aliases]
(def out @[])
(when (os/stat deps-edn-path)
(def edn (load-config deps-edn-path))
(def extra (combine-aliases edn aliases))
(each r (src-roots (os/cwd) edn) (array/push out r))
(each pp (extra :extra-paths) (array/push out (string (os/cwd) "/" pp))))
out)
# --- :tasks (the honest subset of babashka's) ----------------------------------
# A STRING task is a shell command. A MAP task carries :main-opts (jolt args —
# `-e "(...)"` covers expression tasks) and an optional :doc. Babashka-style
# bare-expression tasks aren't supported: the reader hands us parsed data, and
# round-tripping it back to source isn't worth the fragility.
(defn tasks
"Sorted [name doc] pairs from the merged user+project :tasks."
[deps-edn-path]
(def m (get (load-config deps-edn-path) :tasks))
(def names (sort (keys (or m @{}))))
(map (fn [n]
(def v (get m n))
[n (when (dictionary? v) (get v :doc))])
names))
(defn task-spec
"What running task `name` means: {:type :shell :cmd s} or
{:type :jolt :argv [...]}; nil when undefined."
[deps-edn-path name]
(def v (get (or (get (load-config deps-edn-path) :tasks) @{}) name))
(cond
(nil? v) nil
(or (string? v) (buffer? v)) {:type :shell :cmd (string v)}
(and (dictionary? v) (get v :main-opts))
{:type :jolt :argv (tuple ;(map string (get v :main-opts)))}
(error (string "task " name ": use a shell string or {:main-opts [...]}"))))

View file

@ -1,27 +0,0 @@
# jolt-deps — deprecated shim. Dependency resolution is now built into `jolt`
# itself (the runtime stays deps-agnostic; the CLI front-end resolves deps.edn
# into JOLT_PATH in-process). This binary forwards everything to `jolt` so
# existing scripts keep working — prefer calling `jolt` directly:
#
# jolt-deps -M:nrepl -> jolt -M:nrepl
# jolt-deps path -> jolt path
# jolt-deps run FILE -> jolt run FILE
#
# The jolt binary is found via $JOLT_BIN, else the `jolt` sitting next to this
# shim (built as a pair), else `jolt` on PATH.
(defn- jolt-bin []
(or (os/getenv "JOLT_BIN")
(let [self (or (first (dyn :args)) (dyn :executable))
slashes (when self (string/find-all "/" self))
dir (when (and slashes (> (length slashes) 0))
(string/slice self 0 (last slashes)))
sibling (when dir (string dir "/jolt"))]
(when (and sibling (os/stat sibling)) sibling))
"jolt"))
(defn main [&]
(def argv (tuple/slice (or (dyn :args) @[]) 1))
(eprint "jolt-deps is deprecated — dependency resolution is built into `jolt` now "
"(e.g. `jolt -M:nrepl`, `jolt path`, `jolt run FILE`).")
(os/exit (os/execute [(jolt-bin) ;argv] :p)))

View file

@ -1,609 +0,0 @@
# Jolt Evaluator — base: forward vars, syntax-quote, ns-loading, registries
# Extracted from evaluator.janet (jolt-oudv, phase 2a split).
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./pv)
(use ./plist)
(use ./config)
(use ./reader)
(use ./regex)
# Host PNG encoder, exposed to the overlay as `janet.png/encode` / `janet.png/write`
# (resolved through module-load-env below). Pure Janet, no jolt deps.
(import ./png :prefix "png/")
# The env this module was loaded under — proto-chains to the Janet root env;
# the janet/* interop bridge falls back to it inside env-less fibers.
(def module-load-env (fiber/getenv (fiber/current)))
# jpm-module autoload: a janet.<module>/<name> reference whose module isn't
# in the env is satisfied by requiring it from the jpm module path on first
# use — (janet.spork.http/server ...) just works when spork is installed,
# and the same goes for any jpm module. Loaded bindings are cached here
# (and failures negatively cached, so a missing module errors fast).
(def janet-bridge-extras @{})
(def janet-bridge-failed @{})
(defn bridge-autoload
"jname is spork.http/server-shaped: require spork/http, cache its public
bindings under the dotted prefix, return the one asked for (nil when the
module is missing or has no such binding)."
[jname]
(def slash (string/find "/" jname))
(when slash
(def mod-ns (string/slice jname 0 slash))
(unless (get janet-bridge-failed mod-ns)
(def mod-path (string/replace-all "." "/" mod-ns))
(def r (protect (require mod-path)))
(if (r 0)
(eachp [sym entry] (r 1)
(when (and (symbol? sym) (table? entry) (not (get entry :private)))
(put janet-bridge-extras (string mod-ns "/" sym) (get entry :value))))
(put janet-bridge-failed mod-ns true))))
(in janet-bridge-extras jname))
(defn sym-name?
[sym-s name-str]
(and (struct? sym-s) (= :symbol (sym-s :jolt/type)) (= name-str (sym-s :name))))
(defn- special-symbol?
[name]
(or (= name "quote") (= name "syntax-quote") (= name "unquote")
(= name "unquote-splicing") (= name "do") (= name "if")
(= name "def") (= name "defmacro") (= name "fn*") (= name "let*") (= name "loop*")
(= name "recur") (= name "throw") (= name "try")
(= name "set!") (= name "var")
(= name "eval")
(= name "new") (= name ".")
# var-get/var-set/var?/alter-var-root/alter-meta!/reset-meta! are plain
# clojure.core fns (core-bindings); find-var/intern are ctx-capturing fns
# (install-stateful-fns!) — no longer special forms (Stage 2 tier 6).
# locking/instance?/satisfies?/defonce/read-string/macroexpand-1 and the
# multimethod table ops are overlay macros / clojure.core fns now
# (Stage 2 tier 6c) — not special forms.
))
(var eval-form nil)
# Macro expansion cache (interpreter): a macro CALL form expands ONCE and the
# result is reused — macroexpansion is a compile-time step with zero runtime cost,
# the proper Lisp model. Keyed by the call form's identity (a fn body re-evaluates
# the same form arrays each call). Also gives compile-once gensym semantics (a
# foo# auto-gensym is fixed across calls, unlike per-call re-expansion). Cleared
# when a macro is (re)defined so stale expansions don't linger.
(def macro-cache @{})
# Compile hook for macro expanders: set by the api to (fn [ctx args-form body] ->
# compiled-janet-fn | nil). When set and the body is compilable (no &env/&form,
# analyzer available), defmacro uses the compiled expander instead of the
# interpreted closure — macro expansion at native speed, zero runtime cost.
(var macro-compile-hook nil)
(defn form-uses-sym? [form nm]
(cond
(and (struct? form) (= :symbol (form :jolt/type))) (= nm (form :name))
(or (array? form) (tuple? form))
(do (var found false) (each x form (when (form-uses-sym? x nm) (set found true) (break))) found)
(and (struct? form) (nil? (form :jolt/type)))
(do (var found false) (each k (keys form)
(when (or (form-uses-sym? k nm) (form-uses-sym? (get form k) nm)) (set found true) (break))) found)
false))
# A transient is a tagged mutable table @{:jolt/type :jolt/transient :kind ...}.
(defn- jolt-transient? [x]
(and (table? x) (= :jolt/transient (get x :jolt/type))))
# Read-only lookup over a transient (vector index / map key / set membership),
# mirroring core-get. Map/set backing tables are keyed by the same canon used
# by phm, so canonicalize collection keys here too.
(defn- transient-lookup [t k default]
(case (t :kind)
:vector (let [a (t :arr)]
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length a)))
(in a k) default))
:map (let [e (get (t :tbl) (canon k))] (if (nil? e) default (in e 1)))
:set (if (nil? (get (t :tbl) (canon k))) default k)
default))
(defn coll-lookup
"Clojure `get` semantics over a jolt collection, used for collection-as-IFn."
[coll k default]
(cond
(jolt-transient? coll) (transient-lookup coll k default)
(shape-rec? coll) (shape-get coll k default)
# sorted colls are tables — without this arm they fell into the raw
# table-get branch and (:k (sorted-map ...)) was always nil (jolt-4vr spec)
(and (table? coll) (or (= :jolt/sorted-map (coll :jolt/type))
(= :jolt/sorted-set (coll :jolt/type))))
((get (coll :ops) :get) coll k default)
(phm? coll) (phm-get coll k default)
(set? coll) (if (phs-contains? coll k) k default)
(pvec? coll)
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (pv-count coll)))
(pv-nth coll k) default)
(or (tuple? coll) (array? coll))
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length coll)))
(in coll k) default)
(or (struct? coll) (table? coll))
(let [v (get coll k :jolt/not-found)]
(if (= v :jolt/not-found) default v))
(nil? coll) default
default))
(defn jolt-invoke
"Apply f to already-evaluated args. Handles real functions and Clojure's
IFn collections: vectors (index lookup), maps/sets/keywords/symbols (get),
and deftype/record values implementing IFn. `args` is an array."
[ctx f args]
(cond
(or (function? f) (cfunction? f)) (apply f args)
# a var is callable as its current value (Clojure vars implement IFn) —
# e.g. (wrap-request #'core/request) threads the var as the base client
(var? f) (jolt-invoke ctx (var-get f) args)
(jolt-transient? f) (transient-lookup f (get args 0) (get args 1))
# a record shape-rec is callable: IFn impl if it has one, else map-like
# field access. A plain (non-record) shape-rec is just field access.
(shape-rec? f)
(let [tag (record-tag f)
ifn (when tag (find-protocol-method ctx tag "IFn" "-invoke"))]
(if ifn (apply ifn f args) (shape-get f (get args 0) (get args 1))))
(keyword? f) (coll-lookup (get args 0) f (get args 1))
(and (struct? f) (= :symbol (f :jolt/type)))
(coll-lookup (get args 0) f (get args 1))
(and (table? f) (or (= :jolt/sorted-map (f :jolt/type))
(= :jolt/sorted-set (f :jolt/type))))
# the overlay-attached :get op (comparator-based lookup, like Clojure)
((get (f :ops) :get) f (get args 0) (get args 1))
(phm? f) (phm-get f (get args 0) (get args 1))
(set? f) (if (phs-contains? f (get args 0)) (get args 0) (get args 1))
(pvec? f)
(let [k (get args 0)]
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (pv-count f)))
(pv-nth f k)
(error (string "Index " k " out of bounds for vector of length " (pv-count f)))))
(or (tuple? f) (array? f))
(let [k (get args 0)]
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length f)))
(in f k)
(error (string "Index " k " out of bounds for vector of length " (length f)))))
# Map literal only (struct with no :jolt/type). A tagged struct (char/etc.)
# is not callable — symbols are handled above; chars fall through to the error.
(and (struct? f) (nil? (get f :jolt/type)))
(let [v (get f (get args 0) :jolt/not-found)]
(if (= v :jolt/not-found) (get args 1) v))
(and (table? f) (get f :jolt/deftype))
(let [ifn-fn (find-protocol-method ctx (get f :jolt/deftype) "IFn" "-invoke")]
(if ifn-fn (apply ifn-fn f args)
(if (and (get f :jolt/protocol-methods) (get (f :jolt/protocol-methods) :-invoke))
(apply (get (f :jolt/protocol-methods) :-invoke) f args)
# No IFn impl: fall back to map-like field access, e.g. (point :x)
(let [v (get f (get args 0) :jolt/not-found)]
(if (= v :jolt/not-found) (get args 1) v)))))
(and (table? f) (get f :jolt/protocol-methods))
(let [invoke-fn (get (f :jolt/protocol-methods) :-invoke)]
(if invoke-fn (apply invoke-fn f args)
(error (string "Cannot call " (type f) " as a function"))))
(error (string "Cannot call " (type f) " as a function"))))
(defn- sq-symbol
"Resolve a symbol inside syntax-quote. `foo#` becomes a stable auto-gensym
(per-expansion, via gsmap); special forms are left unqualified; a clojure.core
name is fully qualified to clojure.core/ (matching Clojure, for hygiene); other
symbols are qualified to the current namespace so they resolve when the macro is
used elsewhere."
[ctx form gsmap]
(if (nil? (form :ns))
(let [nm (form :name)]
(cond
(string/has-suffix? "#" nm)
(or (get gsmap nm)
(let [g {:jolt/type :symbol :ns nil
:name (string (string/slice nm 0 -2) "__" (string (gensym)) "__auto")}]
(put gsmap nm g) g))
(special-symbol? nm) form
(ns-find (ctx-find-ns ctx "clojure.core") nm)
{:jolt/type :symbol :ns "clojure.core" :name nm}
# Unresolved -> qualify to the namespace being COMPILED when set (the
# analyzer runs interpreted in jolt.analyzer, so ctx-current-ns is wrong
# mid-compile — the same seam resolve-var/h-current-ns use). Matters when
# a macro expander's template is lowered while a symbol it references is
# not yet defined (deftype's extend-type, defined later in the same tier):
# it must qualify to the macro's home ns, not jolt.analyzer.
{:jolt/type :symbol
:ns (or (get (ctx :env) :compile-ns) (ctx-current-ns ctx))
:name nm}))
# Alias-qualified (impl/foo): resolve the alias to its target namespace so the
# emitted symbol resolves at the macro's USE site, which has no such alias
# (jolt-9av). Matches Clojure's syntax-quote. A real ns name (not an alias)
# has no entry and is left as written.
(let [cur (ctx-find-ns ctx (or (get (ctx :env) :compile-ns) (ctx-current-ns ctx)))
target (and cur (or (ns-alias-lookup cur (form :ns))
(ns-import-lookup cur (form :ns))))]
(if target
{:jolt/type :symbol :ns target :name (form :name)}
form))))
(defn d-realize
"Realize a lazy-seq to an array for positional destructuring / splicing; pass
others (pvec/plist coerced to array, everything else unchanged). nil is an
empty seq, as everywhere in Clojure — ~@nil splices nothing (an interpreted
macro's empty & rest binds nil, which used to blow up `each`)."
[val]
(if (nil? val) @[]
(if (pvec? val) (pv->array val)
(if (plist? val) (pl->array val)
(if (lazy-seq? val)
(do
(var items @[]) (var cur val) (var go true)
(while go
(let [cell (realize-ls cur)]
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell)))
(set go false)
(do (array/push items (in cell 0))
(let [rt (in cell 1)]
(if (nil? rt) (set go false) (set cur (ls-rest-cached cur rt))))))))
items)
val)))))
(defn syntax-quote*
[ctx bindings form &opt gsmap]
(default gsmap @{})
(cond
(and (array? form) (> (length form) 0) (sym-name? (first form) "unquote"))
(eval-form ctx bindings (in form 1))
(and (array? form) (> (length form) 0) (sym-name? (first form) "unquote-splicing"))
(error "~@ used outside of a list or vector in syntax-quote")
(or (number? form) (string? form) (keyword? form) (nil? form) (= true form) (= false form))
form
(and (struct? form) (= :symbol (form :jolt/type)))
(sq-symbol ctx form gsmap)
(tuple? form)
(do (var result @[]) (var i 0) (while (< i (length form))
(let [item (in form i)]
(if (and (array? item) (> (length item) 0) (sym-name? (first item) "unquote-splicing"))
(let [sv (eval-form ctx bindings (in item 1))]
(each v (d-realize sv) (array/push result v)))
(array/push result (syntax-quote* ctx bindings item gsmap))))
(++ i)) (tuple ;result))
(array? form)
(do (var result @[]) (var i 0) (while (< i (length form))
(let [item (in form i)]
(if (and (array? item) (> (length item) 0) (sym-name? (first item) "unquote-splicing"))
(let [sv (eval-form ctx bindings (in item 1))]
(each v (d-realize sv) (array/push result v)))
(array/push result (syntax-quote* ctx bindings item gsmap))))
(++ i)) result)
# set literal: lower each element (processing ~/~@) and rebuild a set.
(and (struct? form) (= :jolt/set (form :jolt/type)))
(do (var result @[])
(each item (form :value)
(if (and (array? item) (> (length item) 0) (sym-name? (first item) "unquote-splicing"))
(let [sv (eval-form ctx bindings (in item 1))]
(each v (d-realize sv) (array/push result v)))
(array/push result (syntax-quote* ctx bindings item gsmap))))
(make-phs ;result))
(and (struct? form) (get form :jolt/type)) form
(struct? form)
(do (var kvs @[])
(def order (form-kv-order form))
(if order
(each x order (array/push kvs (syntax-quote* ctx bindings x gsmap)))
(each k (keys form)
(array/push kvs (syntax-quote* ctx bindings k gsmap))
(array/push kvs (syntax-quote* ctx bindings (get form k) gsmap))))
# keep carrying source order through nested syntax-quote (jolt-p3c)
(struct/with-proto (struct :jolt/kv-order (tuple/slice kvs)) ;kvs))
form))
# Syntax-quote LOWERING: instead of evaluating a `(...) form to a value (what
# syntax-quote* does), produce equivalent CONSTRUCTION CODE so a backtick body is
# plain compilable code (read -> macroexpand -> compile, zero runtime cost).
# Mirrors syntax-quote*/sq-symbol exactly; the canonical algorithm is
# tools.reader's syntax-quote*/expand-list. List forms build via __sqcat (-> array),
# vectors via __sqvec (-> tuple), maps via __sqmap; symbols become (quote resolved);
# ~ leaves the expr in place, ~@ passes the seq straight to __sqcat for splicing.
(defn- sqsym* [nm] {:jolt/type :symbol :ns nil :name nm})
(var syntax-quote-lower nil)
(defn- sq-lower-part [ctx item gsmap]
(if (and (array? item) (> (length item) 0) (sym-name? (first item) "unquote-splicing"))
(in item 1)
@[(sqsym* "__sq1") (syntax-quote-lower ctx item gsmap)]))
(set syntax-quote-lower
(fn syntax-quote-lower [ctx form &opt gsmap]
(default gsmap @{})
(cond
(and (array? form) (> (length form) 0) (sym-name? (first form) "unquote"))
(in form 1)
(and (array? form) (> (length form) 0) (sym-name? (first form) "unquote-splicing"))
(error "~@ used outside of a list or vector in syntax-quote")
(or (number? form) (string? form) (keyword? form) (nil? form) (= true form) (= false form))
form
(and (struct? form) (= :symbol (form :jolt/type)))
@[(sqsym* "quote") (sq-symbol ctx form gsmap)]
(array? form)
(array/concat @[(sqsym* "__sqcat")] (map (fn [it] (sq-lower-part ctx it gsmap)) form))
(tuple? form)
(array/concat @[(sqsym* "__sqvec")] (map (fn [it] (sq-lower-part ctx it gsmap)) form))
# set literal: lower each element (so ~/~@ are processed) and rebuild a set.
(and (struct? form) (= :jolt/set (form :jolt/type)))
(array/concat @[(sqsym* "__sqset")] (map (fn [it] (sq-lower-part ctx it gsmap)) (form :value)))
# other tagged structs (chars): returned as-is (no recursion)
(and (struct? form) (get form :jolt/type))
@[(sqsym* "quote") form]
(struct? form)
(do (var parts @[(sqsym* "__sqmap")])
(def order (form-kv-order form))
(if order
(each x order (array/push parts (syntax-quote-lower ctx x gsmap)))
(each k (keys form)
(array/push parts (syntax-quote-lower ctx k gsmap))
(array/push parts (syntax-quote-lower ctx (get form k) gsmap))))
parts)
@[(sqsym* "quote") form])))
(defn resolve-var
[ctx bindings sym-s]
(let [name (sym-s :name) ns (sym-s :ns)]
(if (not (nil? ns))
# Resolve ns aliases (e.g. `p/thrown?` where `p` is a require :as alias) so
# aliased refs/macros resolve. During compilation the analyzer (interpreted,
# in jolt.analyzer) rebinds ctx-current-ns to its own ns, so look up the alias
# against the COMPILE ns (:compile-ns, the user's ns) when set — otherwise an
# aliased ref like g/foo wouldn't resolve mid-compile. Same ns h-current-ns uses.
(let [cur-name (or (get (ctx :env) :compile-ns) (ctx-current-ns ctx))
current-ns (ctx-find-ns ctx cur-name)
aliased-ns (or (ns-alias-lookup current-ns ns) (ns-import-lookup current-ns ns))
target-ns (ctx-find-ns ctx (or aliased-ns ns))]
(ns-find target-ns name))
(if (get bindings name) nil
(let [current-ns (ctx-current-ns ctx)
ns (ctx-find-ns ctx current-ns)
v (ns-find ns name)]
(if v v
(let [core-ns (ctx-find-ns ctx "clojure.core")]
(ns-find core-ns name))))))))
(defn sym-name-str
[sym-s]
(if (sym-s :ns) (string (sym-s :ns) "/" (sym-s :name)) (sym-s :name)))
(defn- ns->relpath
"Namespace name to its file-relative path (dots->dirs, dashes->_), no extension."
[ns-name]
(string/replace-all "." "/" (string/replace-all "-" "_" ns-name)))
(defn- find-ns-file
"Search the context's source roots (stdlib first, then deps.edn dirs) for the
namespace's source, trying .clj then .cljc. Returns the path or nil."
[ctx ns-name]
(let [rel (ns->relpath ns-name)
roots (or (get (ctx :env) :source-paths) @["src/jolt"])]
(var found nil)
(each root roots
(each ext [".clj" ".cljc"]
(when (nil? found)
(let [p (string root "/" rel ext)]
(when (os/stat p) (set found p))))))
found))
(defn- load-ns-source
"Parse and evaluate every form of a namespace's source in the given context.
Routes through the loader's eval-toplevel when the api has installed it
(the :toplevel-eval hook) so REQUIRED namespaces compile like everything
else — without it they ran interpreted-only: slower, and their fns were
anonymous closures in stack traces (jolt-2o7.1)."
[ctx src &opt file]
(default file "<source>")
(def toplevel (get (ctx :env) :toplevel-eval))
# a require runs nested inside an outer file's eval; save/restore the outer
# checker source so its later forms still convert offsets correctly (jolt-fqy)
(def checking (or (checker-enabled?) (get (ctx :env) :inline?)))
(def saved-src (and checking (get (ctx :env) :tc-source)))
(def saved-file (and checking (get (ctx :env) :tc-file)))
(when checking
(track-positions! true)
(put (ctx :env) :tc-source src)
(put (ctx :env) :tc-file file))
(defer (when checking
(put (ctx :env) :tc-source saved-src)
(put (ctx :env) :tc-file saved-file))
(each [f line] (parse-all-positioned src file)
(try
(if toplevel (toplevel ctx f) (eval-form ctx @{} f))
([err fib]
# innermost failing form wins; files unwound through form the
# 'while loading …' chain (mirrors loader/eval-forms-positioned,
# which this can't import — circularity) (jolt-2o7.4)
(def env (ctx :env))
(when (nil? (get env :error-pos))
(put env :error-pos {:file file :line line}))
(when (nil? (get env :error-loading)) (put env :error-loading @[]))
(def chain (get env :error-loading))
(when (not= (last chain) file) (array/push chain file))
(propagate err fib))))))
# jolt-87e: is a namespace loaded from `path` part of the APP (vs a dependency)?
# True when its file sits under one of the declared app source roots
# (:app-source-paths, from JOLT_APP_PATHS / jolt-deps). When NO app roots are
# declared (a bare program run, or jolt invoked without jolt-deps), everything
# counts as app so whole-program covers the whole program exactly as before.
# Only app namespaces defer into the one whole-program fixpoint; dependency
# namespaces infer per-ns at load, so a dep-heavy app's startup doesn't re-infer
# hundreds of transitive dependency namespaces in a single closed-world pass.
(defn- app-source-ns?
[ctx path]
(def roots (get (ctx :env) :app-source-paths))
(if (or (nil? roots) (empty? roots))
true
(and path (truthy? (some |(string/has-prefix? $ path) roots)))))
(defn maybe-require-ns
"If namespace ns-name isn't populated yet, load its source — from a file on the
context's source roots, else from the stdlib baked into the image. Restores the
current namespace afterwards (a library's own `ns` form, or our manual switch
for ns-form-less stdlib files, changes it). No-op for already-loaded namespaces."
[ctx ns-name]
(let [ns (ctx-find-ns ctx ns-name)]
(when (and (= 0 (length (ns :mappings)))
(not (get (get (ctx :env) :loaded-namespaces @{}) ns-name))
(not= ns-name "clojure.core"))
(let [path (find-ns-file ctx ns-name)
embedded (get (get (ctx :env) :embedded-sources @{}) ns-name)
stdlib? (not (nil? embedded))]
# Clojure throws FileNotFoundException here; succeeding silently leaves
# an empty namespace behind and defers the failure to the first
# unresolved symbol, far from the actual cause (a typo, a missing
# JOLT_PATH root). Best-effort loaders (the SCI bootstrap, which loads
# clj-targeted sources whose requires can't all exist on this host)
# opt out via :lenient-require? on the env.
(when (and (nil? path) (nil? embedded)
(not (get (ctx :env) :lenient-require?)))
(error (string "Could not locate " ns-name
" on the context's source paths (JOLT_PATH / :paths)")))
(when (or path embedded)
(let [saved (ctx-current-ns ctx)
# jolt-87e: is this an app namespace, or a dependency/library? Only
# the app is the closed world the whole-program optimizer reasons
# over; dependencies are open-world libraries.
app? (app-source-ns? ctx path)
# Whole-program optimize is active for this load.
wp-active? (and (get (ctx :env) :inline?)
(get (ctx :env) :whole-program?)
(not (get (ctx :env) :infer-program-done?)))
# A dependency under whole-program optimize compiles at DEFAULT
# cost: :inline? off for its load, so the per-form inline +
# inference passes — the bulk of optimize-mode startup — don't run
# over hundreds of library forms. (Direct-linking + shape-recs stay
# on, exactly like a non-optimized direct-link build.) This is what
# makes JOLT_OPTIMIZE viable on dep-heavy apps; the app's own nses
# below keep full optimization. (jolt-87e)
dep-cheap? (and wp-active? (not app?))
saved-inline (get (ctx :env) :inline?)]
# Stdlib files have no `ns` form, so switch into the target ns first
# (their defs intern there); a library's own `ns` form overrides this.
(ctx-set-current-ns ctx ns-name)
(when dep-cheap? (put (ctx :env) :inline? false))
(if path
(load-ns-source ctx (slurp path) path)
(load-ns-source ctx embedded (string ns-name " (stdlib)")))
(when dep-cheap? (put (ctx :env) :inline? saved-inline))
# Inter-procedural collection-type inference (jolt-767): once the whole
# unit is loaded, run the closed-world fixpoint + recompile so param-
# dependent lookups specialize. Only in optimization mode; best-effort
# (a failure here must not break loading). Hook installed by the api to
# avoid an evaluator->backend circular import.
(when (get (ctx :env) :inline?)
(cond
# whole-program (jolt-t34), APP namespace: defer — record the ns and
# run ONE fixpoint over all app units later (the closed-world pass
# sees every caller, so cross-ns param types propagate).
(and wp-active? app?)
(let [lst (or (get (ctx :env) :inferred-nses)
(let [a @[]] (put (ctx :env) :inferred-nses a) a))]
(array/push lst ns-name))
# whole-program, DEPENDENCY namespace (jolt-87e): nothing to do —
# it compiled cheaply above (no inference to run or defer).
wp-active?
nil
# per-ns mode (whole-program off), or a lazy require AFTER the batch
# ran: infer this unit on its own.
(when-let [iu (get (ctx :env) :infer-unit!)]
(protect (iu ctx ns-name)))))
# Record load order for tooling (uberscript): a dependency finishes
# loading before its requirer, so this is topological. Skip the
# baked-in stdlib — it's part of the runtime, not something to bundle.
(when (and path (not stdlib?))
(when-let [lf (get (ctx :env) :loaded-files)] (array/push lf path)))
(ctx-set-current-ns ctx saved)))))))
(defn eval-require
[ctx spec]
(let [ns-sym (in spec 0)
ns-name (sym-name-str ns-sym)]
(var alias nil)
(var refer-syms nil)
(var i 1)
(let [slen (length spec)]
# Scan ALL options — a spec may carry both :as and :refer, e.g.
# [clojure.string :as str :refer [blank?]]; don't stop at the first.
(while (< i slen)
(let [item (in spec i)]
(cond
(or (= item :as) (and (struct? item) (= :symbol (item :jolt/type)) (= "as" (item :name))))
(do (set alias ((in spec (+ i 1)) :name)) (+= i 2))
(or (= item :refer) (and (struct? item) (= :symbol (item :jolt/type)) (= "refer" (item :name))))
(do (set refer-syms (in spec (+ i 1))) (+= i 2))
(++ i)))))
(maybe-require-ns ctx ns-name)
(when alias
(let [current-ns (ctx-find-ns ctx (ctx-current-ns ctx))]
(ns-add-alias current-ns alias ns-name)))
(when refer-syms
(let [source-ns (ctx-find-ns ctx ns-name)
target-ns (ctx-find-ns ctx (ctx-current-ns ctx))]
(if (or (= refer-syms :all)
(and (struct? refer-syms) (= :symbol (refer-syms :jolt/type))
(= "all" (refer-syms :name))))
# :refer :all — share EVERY var (this used to each over the :all
# keyword itself and silently refer nothing; selmer's
# [selmer.util :refer :all] left *tag-open* & co unresolved)
(eachp [nm v] (source-ns :mappings)
(put (target-ns :mappings) nm v))
(each refer-sym refer-syms
(let [name (if (struct? refer-sym) (refer-sym :name) refer-sym)
v (ns-find source-ns name)]
(when v
# Share the SOURCE var (the Clojure model): macro-ness travels with
# it and source-ns redefinitions propagate to the referer.
(put (target-ns :mappings) name v)))))))
nil))
(defn bind-put
"Put a value into bindings. Uses :jolt/nil sentinel for nil values
because Janet's (put table key nil) silently drops the key."
[bindings key value]
(put bindings key (if (nil? value) :jolt/nil value)))
(defn binding-get
"Get a value from bindings, walking the prototype chain."
[bindings name]
(var result :jolt/not-found)
(var t bindings)
(while (not (nil? t))
(when (in t name)
(set result (in t name))
(break))
(set t (table/getproto t)))
result)
# Pluggable host-class shims (java.time etc. register here at module load):
# class-statics: "ClassName" -> {"member" value-or-fn} (Foo/bar resolution)
# tagged-methods: :jolt/tag -> {"method" (fn [self args...])} ((.m obj) dispatch)
(def class-statics @{})
(def tagged-methods @{})
(defn register-class-statics! [class-name tbl] (put class-statics class-name tbl))
(defn register-tagged-methods! [tag tbl] (put tagged-methods tag tbl))
# Constructor shims: (ClassName. args) resolves ClassName as a value, so the
# ctor fns are interned as clojure.core vars at init (install-stateful-fns!).
(def class-ctors @{})
(defn register-class-ctor! [nm f] (put class-ctors nm f))
# java.util.Iterator shim: (.iterator coll) gives a jolt iterator over any
# seqable, with (.hasNext it) / (.next it). Some Clojure libs (e.g. hiccup's
# iterate!) loop with the Java Iterator protocol; this makes that work over jolt
# collections. The realizer (core/realize-for-iteration, which handles every
# collection type) is late-bound because core loads after this file.
(var coll-realizer nil)
(defn set-coll-realizer! [f] (set coll-realizer f))
# Late-bound (wired in api): routes a Java collection-interop method call
# (.nth/.count/.valAt/.seq …) on a jolt persistent collection to the clojure.core
# equivalent. Returns :jolt/ci-none when it doesn't apply. Lets clj-targeted libs
# (malli) that use .nth/.count on vectors/maps in their :clj branches work.
(var coll-interop nil)
(defn set-coll-interop! [f] (set coll-interop f))

View file

@ -1,443 +0,0 @@
# Jolt Evaluator — symbol/var resolution, params, destructuring, class lookup
# Extracted from evaluator.janet (jolt-oudv, phase 2a split).
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./pv)
(use ./plist)
(use ./config)
(use ./reader)
(use ./regex)
(use ./eval_base)
(register-tagged-methods! :jolt/iterator
@{"hasNext" (fn [self] (< (self :pos) (length (self :items))))
"next" (fn [self]
(def x (in (self :items) (self :pos)))
(put self :pos (+ 1 (self :pos)))
x)})
# Class names evaluate to their CANONICAL NAME STRING — the same value
# core-class returns — so (defmethod m String ...) keys match a
# (defmulti m (comp class :body)) dispatch (ring.util.request does this).
# `new` resolves the actual constructor from class-ctors by short name.
(def class-canonical-names
@{"String" "java.lang.String" "Number" "java.lang.Number"
"Boolean" "java.lang.Boolean" "Long" "java.lang.Long"
"Integer" "java.lang.Integer" "Double" "java.lang.Double"
"InputStream" "java.io.InputStream" "OutputStream" "java.io.OutputStream"
"File" "java.io.File" "Reader" "java.io.Reader" "Writer" "java.io.Writer"
"ISeq" "clojure.lang.ISeq" "Keyword" "clojure.lang.Keyword"
"Symbol" "clojure.lang.Symbol" "MapEntry" "clojure.lang.MapEntry"
"StringReader" "java.io.StringReader" "StringWriter" "java.io.StringWriter"
"StringBuilder" "java.lang.StringBuilder"
"StringTokenizer" "java.util.StringTokenizer"
"Charset" "java.nio.charset.Charset" "Base64" "java.util.Base64"
"Exception" "java.lang.Exception"
"IllegalArgumentException" "java.lang.IllegalArgumentException"
"InterruptedException" "java.lang.InterruptedException"
"Throwable" "java.lang.Throwable"})
# A class used as a VALUE should evaluate to what (clojure.core/type instance)
# returns for its instances, so a registry keyed by class (e.g. malli's
# class-schemas) matches a value's (type ...). For jolt's native tagged types the
# class maps to its :jolt/type keyword — Pattern <-> a compiled regex.
(def- class-value-overrides
@{"Pattern" :jolt/regex "java.util.regex.Pattern" :jolt/regex})
(defn class-value-for
"The value a class-name symbol evaluates to: a type override, else its canonical
name string."
[nm]
(or (get class-value-overrides nm)
(get class-canonical-names nm)
# qualified already, or unknown: the name itself is the token
nm))
(defn ctor-for-class-token
"Constructor fn for a class token (a canonical-name string): try the full
name, then the short name after the last dot."
[tok]
(or (in class-ctors tok)
(let [parts (string/split "." tok)]
(in class-ctors (last parts)))))
# java.lang.String method surface for clj-compat interop: (.toLowerCase s),
# (.indexOf s x), ... — the methods portable cljc libraries actually call.
# Case mapping is ASCII (the whole engine is byte-based); indexOf returns -1
# on miss, as on the JVM.
(defn- str-needle [x]
(cond
(and (struct? x) (= :jolt/char (get x :jolt/type))) (string/from-bytes (x :ch))
# (.indexOf s 61): an int needle is a char CODE on the JVM, not its decimal
# text (ring-codec splits k=v pairs this way)
(number? x) (string/from-bytes (math/trunc x))
(string x)))
# java.lang.Number surface (ring-codec: (.byteValue (Integer/valueOf s 16))).
(def number-methods
{"byteValue" (fn [n] (let [b (band (math/trunc n) 0xff)] (if (> b 127) (- b 256) b)))
"shortValue" (fn [n] (let [v (band (math/trunc n) 0xffff)] (if (> v 32767) (- v 65536) v)))
"intValue" (fn [n] (math/trunc n))
"longValue" (fn [n] (math/trunc n))
"floatValue" (fn [n] (* 1.0 n))
"doubleValue" (fn [n] (* 1.0 n))
"toString" (fn [n &opt radix] (if (= radix 16) (string/format "%x" (math/trunc n)) (string n)))})
# Universal java.lang.Object / exception / persistent-collection methods that
# reitit's :clj branches call on non-string targets: (.getMessage e),
# (.assoc m k v), (.get m k). Consulted in the method-dispatch fallthrough.
(def object-methods
{"getMessage" (fn [e] (cond (and (table? e) (= :jolt/ex-info (get e :jolt/type))) (get e :message)
(string? e) e
(string e)))
"getCause" (fn [e] (and (table? e) (get e :cause)))
"toString" (fn [x] (string x))
"equals" (fn [a b] (deep= a b))
"hashCode" (fn [x] (hash x))
# (.iterator coll) -> a jolt iterator (see :jolt/iterator above). Materializes
# the collection to an indexable array via the late-bound core realizer.
"iterator" (fn [coll] @{:jolt/type :jolt/iterator :pos 0
:items (if coll-realizer (coll-realizer coll) @[])})})
(def string-methods
{"getBytes" (fn [s &opt charset] (buffer s))
"toString" (fn [s] s)
"toLowerCase" (fn [s] (string/ascii-lower s))
"toUpperCase" (fn [s] (string/ascii-upper s))
"trim" (fn [s] (string/trim s))
"intern" (fn [s] s)
# file-path surface: io/file returns plain path strings, so the java.io.File
# / java.net.URL methods selmer's template cache calls land here
"toURI" (fn [s] s)
"toURL" (fn [s] s)
"getPath" (fn [s] s)
"getName" (fn [s] (if-let [i (string/find "/" (string/reverse s))]
(string/slice s (- (length s) i)) s))
"exists" (fn [s] (not (nil? (os/stat s))))
"lastModified" (fn [s] (if-let [st (os/stat s)] (math/floor (* 1000 (st :modified))) 0))
# JVM String.split takes a REGEX string; trailing empties dropped like the JVM
"split" (fn [s re &opt limit]
(def parts (re-split (re-pattern re) s))
(while (and (> (length parts) 0) (= "" (last parts)))
(array/pop parts))
parts)
"length" (fn [s] (length s))
"isEmpty" (fn [s] (= 0 (length s)))
"charAt" (fn [s i] {:jolt/type :jolt/char :ch (s i)})
"codePointAt" (fn [s i] (s i))
"indexOf" (fn [s x &opt from] (or (string/find (str-needle x) s (or from 0)) -1))
"lastIndexOf" (fn [s x]
(let [n (str-needle x)]
(var found -1) (var i 0)
(while (< i (length s))
(let [f (string/find n s i)]
(if f (do (set found f) (set i (+ f 1))) (set i (length s)))))
found))
"substring" (fn [s start &opt end] (string/slice s start end))
"startsWith" (fn [s p] (string/has-prefix? p s))
"endsWith" (fn [s p] (string/has-suffix? p s))
"contains" (fn [s sub] (not (nil? (string/find (str-needle sub) s))))
"concat" (fn [s o] (string s o))
"replace" (fn [s a b] (string/replace-all (str-needle a) (str-needle b) s))
"replaceAll" (fn [s regex replacement] (re-replace-all (re-pattern regex) s replacement))
"replaceFirst" (fn [s regex replacement] (re-replace-first (re-pattern regex) s replacement))
"matches" (fn [s regex] (not (nil? (re-matches (re-pattern regex) s))))
"compareTo" (fn [s o] (cond (< s o) -1 (> s o) 1 0))
"equalsIgnoreCase" (fn [s o] (= (string/ascii-lower s) (string/ascii-lower (string o))))})
(defn resolve-sym
[ctx bindings sym-s]
(let [name (sym-s :name) ns (sym-s :ns)]
# Math/Thread/System/Long and every other class resolve through the generic
# class-statics registry (host_interop registers them at load); no special-case.
(if (get class-statics ns)
(let [v (get (get class-statics ns) name)]
(if (nil? v) (error (string "Unsupported member: " ns "/" name)) v))
(if (not (nil? ns))
(let [current-ns (ctx-find-ns ctx (ctx-current-ns ctx))
aliased-ns (or (ns-alias-lookup current-ns ns) (ns-import-lookup current-ns ns))
target-ns (ctx-find-ns ctx (or aliased-ns ns))
v (and target-ns (ns-find target-ns name))]
(if v (var-get v)
# Explicit Janet interop. The `janet` namespace segment marks every
# crossing into host code, where Clojure semantics no longer hold:
# janet/<name> -> Janet root binding (janet/slurp, janet/type)
# janet.<module>/<name> -> Janet module binding (janet.net/server,
# janet.os/clock)
# This makes the whole Janet stdlib reachable from Clojure while keeping
# the interop boundary visible at the call site.
(if (or (= ns "janet") (string/has-prefix? "janet." ns))
(let [jname (if (= ns "janet") name (string (string/slice ns 6) "/" name))
# worker fibers may carry no env (fiber/new without :e inherit)
# — fall back to the env captured at module load
# four-step resolution: the runtime fiber's env (when it
# has one), the evaluator's module env (worker/connection
# fibers carry a foreign or empty env — net/server handler
# fibers resolve janet/struct through here), the autoload
# cache, then a jpm-module require on first miss
entry (or (when-let [fe (fiber/getenv (fiber/current))]
(in fe (symbol jname)))
(in module-load-env (symbol jname))
(in janet-bridge-extras jname)
(bridge-autoload jname))]
(if (not (nil? entry))
(if (table? entry) (entry :value) entry)
(error (string "Unable to resolve Janet symbol: " jname))))
# syntax-quote ns-qualifies bare class names inside macros
# (selmer.util/StringBuilder); class names never belong to an ns —
# fall back to the constructor / statics shims before giving up.
(if (or (in class-ctors name) (get class-canonical-names name) (get class-value-overrides name))
(class-value-for name)
(error (string "Unable to resolve symbol: " ns "/" name))))))
# Use :jolt/not-found sentinel to distinguish nil binding from absent binding
(let [local (get bindings name :jolt/not-found-1)
local (if (= local :jolt/not-found-1) (binding-get bindings name) local)]
(if (not= local :jolt/not-found)
(if (= local :jolt/nil) nil local)
(let [current-ns (ctx-current-ns ctx) ns (ctx-find-ns ctx current-ns) v (ns-find ns name)]
(if v (var-get v)
# Check clojure.core as auto-referred fallback
(let [core-ns (ctx-find-ns ctx "clojure.core")
core-v (ns-find core-ns name)]
(if core-v
(var-get core-v)
# Try class-name resolution: Foo.Bar.Baz -> ns "Foo.Bar", name "Baz"
(let [dot-idx (string/find "." name)]
(if dot-idx
(let [last-dot (do
(var idx dot-idx)
(var next-dot (string/find "." name (+ idx 1)))
(while (not (nil? next-dot))
(set idx next-dot)
(set next-dot (string/find "." name (+ idx 1))))
idx)
class-ns (string/slice name 0 last-dot)
class-name (string/slice name (+ last-dot 1))]
(let [target-ns (ctx-find-ns ctx class-ns) tv (ns-find target-ns class-name)]
(if tv (var-get tv) tv)))
# No implicit Janet fallback (Stage 3): an unresolved
# Clojure symbol is an error. Host access is the explicit
# janet/ prefix above.
(if (or (in class-ctors name) (get class-canonical-names name) (get class-value-overrides name))
(class-value-for name)
(error (string "Unable to resolve symbol: " name " in this context")))))))))))))))
(defn- parse-arg-names
"Parse a parameter vector, handling & rest args.
Returns {:fixed [names...] :rest name-or-nil :all [names...]}"
[args-form]
(var fixed @[])
(var rest-name nil)
(var i 0)
(while (< i (length args-form))
(let [a (in args-form i)]
(if (and (struct? a) (= :symbol (a :jolt/type)) (= "&" (a :name)))
(do
(+= i 1)
(if (< i (length args-form))
(do
(set rest-name ((in args-form i) :name))
(+= i 1))
(error "& without argument in parameter list")))
(do
(if (and (struct? a) (= :symbol (a :jolt/type)))
(array/push fixed (a :name))
# destructuring form: recurse into it
(when (indexed? a)
(var di 0)
(while (< di (length a))
(def inner (in a di))
(if (and (struct? inner) (= :symbol (inner :jolt/type)) (= "&" (inner :name)))
(do
(+= di 1)
(if (< di (length a))
(do
(set rest-name ((in a di) :name))
(+= di 1))
(error "& without argument in parameter list")))
(do
(if (and (struct? inner) (= :symbol (inner :jolt/type)))
(array/push fixed (inner :name))
# nested destructuring - extract names
(when (indexed? inner)
(each sym inner
(when (and (struct? sym) (= :symbol (sym :jolt/type)))
(array/push fixed (sym :name))))))
(+= di 1))))))
(+= i 1)))))
(var all @[])
(each n fixed (array/push all n))
(if rest-name (array/push all rest-name))
{:fixed (tuple/slice (tuple ;fixed)) :rest rest-name :all (tuple/slice (tuple ;all))})
# ============================================================
# Destructuring (Clojure-compatible, recursive)
# ============================================================
(defn parse-params
"Parse a parameter vector into raw patterns: {:fixed [pat...] :rest pat-or-nil}.
Unlike parse-arg-names, patterns are kept intact (not flattened) so they can
be destructured against the corresponding argument."
[args-form]
(var fixed @[])
(var rest-pat nil)
(var i 0)
(while (< i (length args-form))
(let [a (in args-form i)]
(if (and (struct? a) (= :symbol (a :jolt/type)) (= "&" (a :name)))
(do (+= i 1)
(when (< i (length args-form)) (set rest-pat (in args-form i)))
(+= i 1))
(do (array/push fixed a) (+= i 1)))))
{:fixed (tuple/slice (tuple ;fixed)) :rest rest-pat})
(defn rest-args-val
"What a rest param binds to: nil when no args remain (Clojure semantics —
(fn [& r]) called with nothing gives r = nil, never an empty seq)."
[args i]
(when (> (length args) i) (tuple/slice args i)))
(defn plain-sym? [p] (and (struct? p) (= :symbol (p :jolt/type))))
(defn require-symbol-params
"fn* is a primitive: its params must be plain symbols. The fn/defn MACROS desugar
destructuring into plain params + a body let before emitting fn*, so fn* never
legitimately sees a pattern — matching Clojure, where (fn* [[a b]] ...) is the
compile error 'fn params must be Symbols'. Enforcing it here keeps the interpreter
consistent with the self-hosted analyzer (which also requires plain fn* params)
and with Clojure, instead of leniently destructuring a form Clojure rejects."
[param-info]
(each p (param-info :fixed)
(unless (plain-sym? p) (error "fn params must be Symbols")))
(let [r (param-info :rest)]
(when (and r (not (plain-sym? r))) (error "fn params must be Symbols"))))
(defn- d-get
"Look up key k in a map-like value (phm/struct/table/nil)."
[m k]
(cond
(phm? m) (phm-get m k)
(or (struct? m) (table? m)) (get m k)
true nil))
(defn- find-or-default
"Find the :or default expression for binding name nm, or :jolt/none."
[or-map nm]
(var result :jolt/none)
(when or-map
(each k (keys or-map)
(when (and (struct? k) (= :symbol (k :jolt/type)) (= nm (k :name)))
(set result (get or-map k)))))
result)
(var destructure-bind nil)
(set destructure-bind
(fn dbind [ctx bindings pat val]
(cond
# plain symbol
(and (struct? pat) (= :symbol (pat :jolt/type)))
(bind-put bindings (pat :name) val)
# sequential pattern (vector of sub-patterns)
(indexed? pat)
(let [rv (d-realize val)
seqable? (indexed? rv)]
(var di 0) (var vi 0)
(def n (length pat))
(while (< di n)
(let [elem (in pat di)]
(cond
# & rest
(and (struct? elem) (= :symbol (elem :jolt/type)) (= "&" (elem :name)))
(do
# rest binds a seq (jolt list = array), per Clojure semantics.
# For lazy-seqs, preserve laziness: walk vi steps via ls-rest
# instead of slicing the eagerly-realized array.
(destructure-bind ctx bindings (in pat (+ di 1))
(if (lazy-seq? val)
(do
(var c val) (var i 0)
(while (< i vi)
(let [nxt (ls-rest c)]
(if (nil? nxt) (break)
(do (set c nxt) (++ i)))))
c)
(if (and seqable? (< vi (length rv)))
(array/slice (if (tuple? rv) (array/slice rv) rv) vi)
@[])))
(set di (+ di 2)))
# :as whole
(= elem :as)
(do
(destructure-bind ctx bindings (in pat (+ di 1)) val)
(set di (+ di 2)))
# positional element
true
(do
(destructure-bind ctx bindings elem
(if (and seqable? (< vi (length rv))) (in rv vi) nil))
(+= di 1) (+= vi 1))))))
# map pattern (struct/table that isn't a symbol)
(or (struct? pat) (table? pat))
(let [rv (d-realize val)
# Destructuring a sequential value as a map treats it as kwargs:
# alternating k/v pairs, or a single trailing map (Clojure's
# `[& {:keys ...}]`). A real map value is used as-is.
mval (if (and (indexed? rv) (not (or (struct? rv) (table? rv))))
(if (and (= 1 (length rv))
(let [e (in rv 0)] (or (struct? e) (table? e) (phm? e))))
(in rv 0)
(let [m @{}]
(var i 0)
(while (< (+ i 1) (length rv))
(put m (in rv i) (in rv (+ i 1)))
(+= i 2))
m))
val)]
(def or-map (get pat :or))
(def as-sym (get pat :as))
(when as-sym (destructure-bind ctx bindings as-sym mval))
# :keys (keyword), :strs (string), :syms (symbol). A namespaced symbol
# in :keys/:syms (x/y) looks up the namespaced key but binds local y.
(each spec [[:keys :kw] [:strs :str] [:syms :sym]]
(let [kw (in spec 0) kind (in spec 1) names (get pat kw)]
(when (and names (indexed? names))
(each s names
(let [sym? (and (struct? s) (= :symbol (s :jolt/type)))
local (if sym? (s :name) (string s))
nsp (and sym? (s :ns))
key (case kind
:kw (keyword (if nsp (string nsp "/" local) local))
:str local
:sym {:jolt/type :symbol :ns nsp :name local})
v (d-get mval key)
v (if (nil? v)
(let [d (find-or-default or-map local)]
(if (= d :jolt/none) nil (eval-form ctx bindings d)))
v)]
(bind-put bindings local v))))))
# direct {local-pattern key-expr} entries (local may itself be a
# nested vector/map pattern). Special keys are keywords; skip them.
(each k (keys pat)
(when (not (keyword? k))
(let [key-val (eval-form ctx bindings (get pat k))
v (d-get mval key-val)]
(if (and (struct? k) (= :symbol (k :jolt/type)))
# symbol target: apply :or default if missing
(let [nm (k :name)
v (if (nil? v)
(let [d (find-or-default or-map nm)]
(if (= d :jolt/none) nil (eval-form ctx bindings d)))
v)]
(bind-put bindings nm v))
# nested pattern target
(destructure-bind ctx bindings k v))))))
true (error (string "Unsupported destructuring pattern: " (string/format "%q" pat))))))
# ---- host-type protocol extension (extend-protocol String/Number/... ) ----
(def host-type-names
{"Long" true "Integer" true "Short" true "Byte" true "BigInteger" true "BigInt" true
"Double" true "Float" true "Number" true "BigDecimal" true "Ratio" true
"String" true "CharSequence" true "Boolean" true "Character" true
"Keyword" true "Symbol" true "Object" true "IFn" true "Fn" true
"PersistentVector" true "PersistentList" true "PersistentHashMap" true
"PersistentHashSet" true "IPersistentMap" true "IPersistentVector" true
"IPersistentSet" true "IPersistentCollection" true "ISeq" true "Atom" true "nil" true
# java.util interfaces + seq types ring & friends extend on
"Map" true "Set" true "List" true "Collection" true "LazySeq" true
"APersistentMap" true})

View file

@ -1,874 +0,0 @@
# Jolt Evaluator — protocols, multimethods, deftype/reify, stateful fn install
# Extracted from evaluator.janet (jolt-oudv, phase 2a split).
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./pv)
(use ./plist)
(use ./config)
(use ./reader)
(use ./regex)
(use ./eval_base)
(use ./eval_resolve)
(defn- canonical-host-tag
"If type-name names a host type (optionally java.*/clojure.lang.* qualified),
return its bare canonical name; else nil (it's a deftype/record name)."
[type-name]
(let [base (cond
(string/has-prefix? "java.lang." type-name) (string/slice type-name 10)
(string/has-prefix? "java.util." type-name) (string/slice type-name 10)
(string/has-prefix? "clojure.lang." type-name) (string/slice type-name 13)
type-name)]
(if (get host-type-names base) base nil)))
(defn- value-host-tags
"Candidate host type-tags for a runtime value, most-specific first."
[obj]
(cond
(number? obj) ["Long" "Integer" "Number" "Double" "Object"]
(string? obj) ["String" "CharSequence" "Object"]
(or (= true obj) (= false obj)) ["Boolean" "Object"]
(keyword? obj) ["Keyword" "Object"]
(and (struct? obj) (= :jolt/char (get obj :jolt/type))) ["Character" "Object"]
(and (struct? obj) (= :symbol (get obj :jolt/type))) ["Symbol" "Object"]
(plist? obj) ["PersistentList" "IPersistentList" "IPersistentCollection" "ISeq" "List" "Collection" "Object"]
(lazy-seq? obj) ["LazySeq" "ISeq" "IPersistentCollection" "Collection" "Object"]
# maps: phm / plain struct / sorted / records — java.util.Map covers them
# all in ring-style extend-protocol clauses
(or (phm? obj)
(shape-rec? obj) # plain shape maps AND records — both map-like
(and (struct? obj) (nil? (get obj :jolt/type)))
(and (table? obj) (or (get obj :jolt/deftype)
(= :jolt/sorted-map (get obj :jolt/type)))))
["PersistentHashMap" "APersistentMap" "IPersistentMap" "Map" "IPersistentCollection" "Object"]
(or (set? obj) (and (table? obj) (= :jolt/sorted-set (get obj :jolt/type))))
["PersistentHashSet" "IPersistentSet" "Set" "IPersistentCollection" "Object"]
(or (tuple? obj) (array? obj) (pvec? obj)) ["PersistentVector" "IPersistentVector" "IPersistentCollection" "ISeq" "Object"]
(or (function? obj) (cfunction? obj)) ["IFn" "Fn" "Object"]
(nil? obj) ["nil" "Object"]
["Object"]))
# ---------------------------------------------------------------------------
# Stateful primitives as ordinary fns (Stage 2 jolt-eaa). These mutate/read the
# per-ctx protocol registry, so they need ctx. They're interned into clojure.core
# as closures over ctx (install-stateful-fns!), which makes them resolve + COMPILE
# as plain :var invokes — the back end embeds the per-ctx var cell, and the closure
# captures ctx so a compiled protocol dispatcher works even when called later.
# Both the interpreter and compiled code call these same closures; there is no
# longer a special-form handler for them. proto/method/type names arrive as
# STRINGS (the defprotocol/extend-type macros pass (name sym), not the symbol).
(defn protocol-dispatch-impl [ctx proto-name method-name obj rest-args]
# an empty jolt rest arg is NIL (Clojure semantics); janet apply needs a tuple
(default rest-args [])
(def type-tag (or (record-tag obj)
(if (and (table? obj) (get obj :jolt/protocol-methods)) (get obj :jolt/deftype))))
(if (and (table? obj) (get obj :jolt/protocol-methods))
(let [reified-fns (get obj :jolt/protocol-methods)
f (get reified-fns (keyword method-name))]
(if f (apply f obj rest-args)
(error (string "No reified method " method-name " for " type-tag))))
(if type-tag
(let [f (find-protocol-method ctx type-tag proto-name method-name)]
(if f (apply f obj rest-args)
(error (string "No method " method-name " in " proto-name " for " type-tag))))
# host value: try candidate host type-tags (Long/String/Object/...), with a
# generation-guarded inline cache (same walk for every value of a host class).
(let [env (ctx :env)
reg-gen (or (get env :type-registry-gen) 0)
pc (let [c (get env :proto-dispatch-cache)]
(if (and c (= (c :gen) reg-gen)) c
(let [n @{:gen reg-gen :map @{}}]
(put env :proto-dispatch-cache n) n)))
cands (value-host-tags obj)
ckey [(first cands) proto-name method-name]
cached (get (pc :map) ckey)
found (if (nil? cached)
(let [f (do (var r nil)
(each tag cands
(when (nil? r)
(set r (find-protocol-method ctx tag proto-name method-name))))
r)]
(put (pc :map) ckey (if f f :jolt/none))
f)
(if (= cached :jolt/none) nil cached))]
(if found (apply found obj rest-args)
(error (string "No dispatch for " method-name " on " (type obj))))))))
(defn register-method-impl [ctx type-name proto-name method-name f]
# host types register under a bare canonical tag; deftype/record names stay
# namespace-qualified to the ns the (extend-)type form runs in.
(def host (canonical-host-tag type-name))
(def type-tag (if host host (string (ctx-current-ns ctx) "." type-name)))
(register-protocol-method ctx type-tag proto-name method-name f))
(defn make-reified-impl [ctx methods-map & rest-args]
# methods-map is the EVALUATED {keyword fn} map (a phm when compiled, a struct/
# table when interpreted) — the fn* literals are already fns, just store them.
# proto-names are the (short) names of every protocol the reify implements.
(def proto-names (if (and (= 1 (length rest-args)) (indexed? (in rest-args 0)))
(in rest-args 0) # wiring passed the rest tuple as one arg
rest-args))
(def obj @{:jolt/deftype (string "reified-" (if (> (length proto-names) 0) (in proto-names 0) ""))
:jolt/protocols (tuple ;proto-names)
:jolt/protocol-methods @{}})
(def pairs (if (phm? methods-map)
(phm-entries methods-map)
(map (fn [k] [k (get methods-map k)]) (keys methods-map))))
(each p pairs (put (obj :jolt/protocol-methods) (in p 0) (in p 1)))
obj)
(defn require-impl
"(require '[ns :as a :refer [...]] ...) — load + alias/refer each spec. A fn, so
the args (quoted specs) arrive evaluated. Varargs (Clojure-compatible); each spec
is a vector [ns & opts] or a bare ns symbol (treated as [ns])."
[ctx & specs]
(each spec specs
(let [s (if (pvec? spec) (pv->array spec) spec)]
(cond
(and (indexed? s) (> (length s) 0)) (eval-require ctx s)
(and (struct? s) (= :symbol (s :jolt/type))) (eval-require ctx @[s])
(error "require expects a vector spec or a namespace symbol"))))
nil)
(defn in-ns-impl
"(in-ns 'foo) — switch the current namespace (creating it if needed). A fn; the
quoted symbol arrives evaluated."
[ctx sym]
(def ns-name (if (and (struct? sym) (= :symbol (sym :jolt/type))) (sym :name) (string sym)))
(def the-ns-obj (ctx-find-ns ctx ns-name))
# An ns entered in-session counts as loaded (Clojure's ns macro commutes the
# name into *loaded-libs*), so a later require/use of it must not try to load
# a file — see maybe-require-ns. Namespace objects are immutable structs, so
# the set lives on the env.
(def loaded (or (get (ctx :env) :loaded-namespaces)
(let [t @{}] (put (ctx :env) :loaded-namespaces t) t)))
(put loaded ns-name true)
(ctx-set-current-ns ctx ns-name)
the-ns-obj)
(defn use-impl
"(use '[ns ...] ...) — refer ALL public vars of each used ns into the CURRENT ns.
A fn; quoted specs arrive evaluated. Each spec is a ns symbol or a [ns & opts]
vector (a pvec/tuple, not a Janet array — coerce, then take the head as the ns)."
[ctx & specs]
(def target-ns (ctx-find-ns ctx (ctx-current-ns ctx)))
(each s specs
(let [spec (if (pvec? s) (pv->array s) s)
ns-sym (if (indexed? spec) (in spec 0) spec)
src-name (sym-name-str ns-sym)]
(maybe-require-ns ctx src-name)
(let [source-ns (ctx-find-ns ctx src-name)]
# Refer maps the SOURCE VAR itself (the Clojure model): redefinitions in
# the source ns propagate, the :macro flag travels for free, and
# ns-refers can identify refers by the var's home :ns.
(loop [[sym v] :pairs (source-ns :mappings)]
(put (target-ns :mappings) sym v)))))
nil)
(defn import-impl
"(import 'pkg.Class ...) — register the short class name as an alias of the fully
qualified name in the current ns. A fn; quoted class symbols arrive evaluated."
[ctx & class-specs]
(def ns (ctx-find-ns ctx (ctx-current-ns ctx)))
(defn sym-name [x] (if (and (struct? x) (= :symbol (x :jolt/type))) (x :name) (string x)))
(defn import-one [class-name &opt pkg]
(def last-dot (do (var idx -1) (var pos 0)
(while (< pos (length class-name))
(when (= (class-name pos) 46) (set idx pos)) (++ pos))
idx))
(def short-name (if (>= last-dot 0) (string/slice class-name (+ last-dot 1)) class-name))
(def pkg-name (cond pkg pkg (>= last-dot 0) (string/slice class-name 0 last-dot) nil))
(ns-import ns short-name class-name)
# a deftype "class" lives as a ctor var in its defining jolt ns — share it
# (the JVM import makes (TextNode. ...) resolvable; this is our analog)
(when pkg-name
(when-let [src-ns (get ((ctx :env) :namespaces) pkg-name)
v (ns-find src-ns short-name)]
(put (ns :mappings) short-name v))))
(each class-spec class-specs
(if (or (array? class-spec) (tuple? class-spec)
(and (table? class-spec) (= :jolt/pvec (class-spec :jolt/type))))
# vector spec: [pkg Class1 Class2 ...]
(let [items (if (table? class-spec) (pv->array class-spec) class-spec)
pkg (sym-name (in items 0))]
(for i 1 (length items)
(import-one (string pkg "." (sym-name (in items i))) pkg)))
(import-one (sym-name class-spec))))
nil)
(defn refer-clojure-impl
"(refer-clojure :exclude [a b]) — currently only :exclude is honored: unmap the
excluded names from the current ns. A fn; quoted args arrive evaluated."
[ctx & args]
(when (and (>= (length args) 2) (= (in args 0) :exclude))
(let [ns (ctx-find-ns ctx (ctx-current-ns ctx))
excl (in args 1)]
(each sym excl
(ns-unmap ns (if (and (struct? sym) (= :symbol (sym :jolt/type))) (sym :name) (string sym))))))
nil)
# Multimethod value -> its var. methods/get-method take the multimethod VALUE
# (Clojure semantics) and recover the var (hence :jolt/methods) through this,
# which works from a compiled fn in any namespace — resolving the symbol at call
# time in the current ns did not (a bare multifn ref in its defining ns saw an
# empty table once defmethods lived in other namespaces; migratus hit this).
(def multi-registry @{})
(defn defmulti-setup
"(defmulti name dispatch & opts) — intern a multimethod var. A fn; name arrives
quoted, dispatch + opts (:default key, :hierarchy h) arrive evaluated. The
defmulti macro is the thin wrapper. Builds the dispatch closure over the method
table (shared with the var's :jolt/methods so defmethod adds to it)."
[ctx name-sym dispatch-raw & opts]
(def dispatch-fn (if (keyword? dispatch-raw) (fn [x] (get x dispatch-raw)) dispatch-raw))
(def default-key
(do (var dv :default) (var i 0)
(while (< i (length opts))
(if (= :default (in opts i)) (do (set dv (in opts (+ i 1))) (set i (length opts))) (+= i 2)))
dv))
(def hierarchy
(do (var h nil) (var i 0)
(while (< i (length opts))
(if (= :hierarchy (in opts i)) (do (set h (in opts (+ i 1))) (set i (length opts))) (+= i 2)))
h))
(def ns (ctx-find-ns ctx (ctx-current-ns ctx)))
(def methods @{})
(def isa-cache @[nil])
(def dispatch-cache @{})
# the prefers table, shared with the var (prefer-method-setup mutates it)
(def v-box @[nil])
(def mm-fn
(fn [& args]
(let [dv* (apply dispatch-fn args)
dv (if (nil? dv*) :jolt/nil-sentinel dv*)
method (get methods dv)]
(if method
(apply method args)
(let [cached (get dispatch-cache dv)]
(if cached
(apply cached args)
# isa? is the OVERLAY's (the hierarchy system is pure Clojure now,
# stage 3); resolve its var lazily, once. A :hierarchy option is an
# atom (deref per dispatch, like Clojure's var) or a plain map.
(let [isa-fn (do
(when (nil? (isa-cache 0))
(put isa-cache 0
(var-get (ns-find (ctx-find-ns ctx "clojure.core") "isa?"))))
(isa-cache 0))
h (if hierarchy
(if (and (table? hierarchy) (= :jolt/atom (get hierarchy :jolt/type)))
(hierarchy :value)
hierarchy)
nil)
# Collect EVERY isa-matching method key, then pick the
# dominant one: x dominates y when x is prefer-method'd
# over y (direct preference) or (isa? x y). Two matches
# with no dominant is an ambiguity ERROR, as in Clojure —
# this used to silently take whichever key the table
# yielded first, ignoring prefer-method (jolt-heo).
found (do
(def matches @[])
(each k (keys methods)
(when (if h (isa-fn h dv k) (isa-fn dv k))
(array/push matches k)))
(defn pref? [x y]
(def px (get (or (get v-box 0) @{}) x))
(and px (not (nil? (get px y)))))
(defn dom? [x y]
(or (pref? x y) (if h (isa-fn h x y) (isa-fn x y))))
(case (length matches)
0 nil
1 (get methods (in matches 0))
(do
(var best (in matches 0))
(var i 1)
(while (< i (length matches))
(when (dom? (in matches i) best) (set best (in matches i)))
(++ i))
(var amb nil)
(each k matches
(when (and (nil? amb) (not (deep= k best)) (not (dom? best k)))
(set amb k)))
(when amb
(error (string "Multiple methods in multimethod '" (name-sym :name)
"' match dispatch value — neither is preferred")))
(get methods best))))]
(if found
(do (put dispatch-cache dv found) (apply found args))
(let [dm (get methods default-key)]
(if dm (apply dm args)
(error (string "No method in multimethod " (name-sym :name)
" for dispatch value: " dv))))))))))))
(def v (ns-intern ns (name-sym :name) mm-fn))
# pre-create the prefers store so the dispatch closure and
# prefer-method-setup share one table
(def prefs-tbl (or (get v :jolt/prefers)
(do (put v :jolt/prefers @{}) (get v :jolt/prefers))))
(put v-box 0 prefs-tbl)
(put v :jolt/methods methods)
(put v :jolt/dispatch-cache dispatch-cache)
(put v :jolt/default default-key)
(when hierarchy (put v :jolt/hierarchy hierarchy))
(put multi-registry mm-fn v)
(var-get v))
(defn defmethod-setup
"(defmethod mm dispatch-val impl) — add a method to a multimethod. A fn; mm
arrives quoted, dispatch-val evaluated, impl is the COMPILED method fn (the
defmethod macro builds (fn …)). Auto-creates the multimethod if it's missing."
[ctx mm-sym dispatch-val impl]
(def mm-var
(or (resolve-var ctx @{} mm-sym)
(let [ns (ctx-find-ns ctx (ctx-current-ns ctx))
stub (fn [& args] nil)]
(def v (ns-intern ns (mm-sym :name) stub))
(put v :jolt/methods @{})
(put multi-registry stub v)
v)))
(def methods (or (get mm-var :jolt/methods) (let [m @{}] (put mm-var :jolt/methods m) m)))
# nil is a legal dispatch value (ring's body-string keys a method on it);
# janet tables can't hold nil keys, so it rides the sentinel
(put methods (if (nil? dispatch-val) :jolt/nil-sentinel dispatch-val) impl)
(let [dc (get mm-var :jolt/dispatch-cache)]
(when dc (each k (keys dc) (put dc k nil))))
mm-var)
(defn- hint-cross-ns-key
"Resolve a record-typed field hint (\"Vec3\", \"v/Vec3\", \"rt.vec/Vec3\") to the
home namespace's ctor key (\"rt.vec/->Vec3\") when the type is defined in a
DIFFERENT namespace and referred/aliased into the one being defined. The local
current-ns/->Type lookup misses those; this resolves the hint name through the
ns's :refer/:as bindings to the type var, then maps its root ctor value back to
the home key via the ctor-value index. Using the ctor VALUE, not the var's :ns,
is what makes :refer work — a :refer re-interns a fresh var whose :ns is the
referring ns, but its root is the same shared ctor closure. nil if unresolved."
[ctx t cix]
# Resolve against the COMPILE ns (the user ns being analyzed), not ctx-current-ns
# — during compilation the analyzer rebinds ctx-current-ns to jolt.analyzer, so a
# bare referred name would otherwise miss. Qualified alias/Name resolves the alias
# against the compile ns; a bare name looks up the compile ns's own mappings
# (which include :refer-interned vars).
(def cur-name (or (get (ctx :env) :compile-ns) (ctx-current-ns ctx)))
(def cur-ns (ctx-find-ns ctx cur-name))
(def slash (string/find "/" t))
(def v (when cur-ns
(if slash
(let [a (string/slice t 0 slash) nm (string/slice t (inc slash))
home (or (ns-alias-lookup cur-ns a) (ns-import-lookup cur-ns a))]
(when home (ns-find (ctx-find-ns ctx home) nm)))
(ns-find cur-ns t))))
(when (and v (table? v)) (get cix (v :root))))
(defn record-hint-ctor-key
"Resolve a record-type hint NAME (as written on a ^Type field/param — bare,
aliased, or fully qualified) to its home ctor key in the record-shapes registry
(\"rt.vec/->Vec3\"), or nil if it is not a known record type. Local
current-ns/->Name wins; otherwise cross-ns via the ctor-value index. Public so
the analyzer (through jolt.host) can type a ^Type PARAM hint exactly as a field
hint resolves, which is what carries a record param's type across a namespace
boundary without whole-program inference."
[ctx name]
(def rs (get (ctx :env) :record-shapes))
(when rs
(def cur (or (get (ctx :env) :compile-ns) (ctx-current-ns ctx)))
(def local (string cur "/->" name))
(if (get rs local)
local
(let [cix (get (ctx :env) :record-ctor-index)]
(when cix (hint-cross-ns-key ctx name cix))))))
(defn make-deftype-ctor-impl
"Build a deftype constructor closure. The ns-qualified type tag is baked at
definition time (this runs during the deftype's (def …), in the type's ns), so
instances carry a stable tag matching what extend-type registers methods under.
field-kws is the [:f1 :f2 …] keyword vector; the ctor maps positional args to
those keys. A ctx-capturing closure (make-deftype-ctor) is the public handle."
[ctx type-name-sym field-kws &opt field-tags field-muts]
(def type-tag (string (ctx-current-ns ctx) "." (type-name-sym :name)))
(def kws (d-realize field-kws))
# per-field type hints (jolt-3ko): a tuple parallel to kws — "Vec3" (a record
# type name), "num", or nil. The inference resolves these to the field's exact
# type so reading a field back carries it (a nested record stays typed).
(def tags (if field-tags (d-realize field-tags) (array/new-filled (length kws))))
# jolt-c3q: a type with any ^:unsynchronized-mutable / ^:volatile-mutable field
# is set!-able, so it CAN'T be an immutable shape-rec tuple. Such a type uses
# the mutable :jolt/deftype table form regardless of :shapes? (set! mutates it,
# field reads route through the tagged-table path), and is NOT registered as a
# shape so the inference never emits a bare-index read against the table.
(def mutable? (and field-muts (some |(identity $) (d-realize field-muts))))
# The ctor closure itself. Built FIRST so it can be indexed by value below.
# Records are shape-recs when shapes are active (:shapes? = direct-link, where
# the inference proves the reads) — the whole field-access pipeline handles
# them; otherwise (or when mutable) the original :jolt/deftype tables. Read at
# ctor-BUILD time so a type is consistently one representation or the other.
(def the-ctor
(if (and (get (ctx :env) :shapes?) (not mutable?))
(fn [& args] (make-record type-tag kws args))
(fn [& args]
(var inst @{:jolt/deftype type-tag})
(var i 0) (each kw kws (put inst kw (in args i)) (++ i))
inst)))
# jolt-t34: register this record's ctor return shape (DECLARED field order) so
# the inference types (->Name ...) as a struct of these fields and field reads
# on the result bare-index. Keyed by the ctor var-key "ns/->Name" to match how
# the IR names the call head. Harmless when records aren't shaped (sidx gated).
# Skipped for mutable types — they're tables, not shape-recs (jolt-c3q).
(unless mutable?
(let [rs (or (get (ctx :env) :record-shapes)
(let [t @{}] (put (ctx :env) :record-shapes t) t))
# ctor-value index: maps each ctor closure to its rs key, so a ^Type hint
# in another namespace can resolve home through the type var's root value
# (jolt-3ko cross-ns hints; see hint-cross-ns-key).
cix (or (get (ctx :env) :record-ctor-index)
(let [t @{}] (put (ctx :env) :record-ctor-index t) t))
# resolve a record-typed hint ("Vec3") to its ctor-key ("ns/->Vec3") so
# the inference resolves it with a direct lookup. "num" stays as-is; a
# local def wins; else try cross-ns resolution; an unresolved name (not a
# known record type) stays bare -> :any.
resolved (map (fn [t]
(cond (nil? t) nil
(= t "num") "num"
(let [ck (string (ctx-current-ns ctx) "/->" t)]
(if (get rs ck) ck
(or (hint-cross-ns-key ctx t cix) t)))))
tags)]
(put rs (string (ctx-current-ns ctx) "/->" (type-name-sym :name))
{:fields (tuple ;kws) :type type-tag :tags (tuple ;resolved)})
(put cix the-ctor (string (ctx-current-ns ctx) "/->" (type-name-sym :name)))))
the-ctor)
(defn install-stateful-fns!
"Intern ctx-capturing closures for the stateful primitives into clojure.core, so
both the interpreter and the compiler reach them as ordinary fns. Called by
api/init after init-core! and before the overlay loads (the protocol macros
expand to calls of these)."
[ctx]
(def core (ctx-find-ns ctx "clojure.core"))
# current-ns get/set for compiled code (emit-try restores the ns on a caught
# throw — an interpreted fn that throws leaves ctx-current-ns set to its
# defining ns, since it can't restore on unwind; the interpreted try already
# repairs this, the compiled try did not, leaking the ns past a catch).
(ns-intern core "__current-ns" (fn [] (ctx-current-ns ctx)))
(ns-intern core "__set-current-ns!" (fn [ns-sym] (ctx-set-current-ns ctx ns-sym) nil))
(ns-intern core "protocol-dispatch"
(fn [proto-name method-name obj rest-args]
(protocol-dispatch-impl ctx proto-name method-name obj rest-args)))
# Devirtualization registry (jolt-41m): defprotocol calls this at load so the
# inference can recognize a protocol-method call site. Maps the method's
# var-key "ns/method" -> [proto-name method-name].
(ns-intern core "register-protocol-methods!"
(fn [proto-name method-names]
(def reg (or (get (ctx :env) :protocol-methods)
(let [t @{}] (put (ctx :env) :protocol-methods t) t)))
(def ns (ctx-current-ns ctx))
(each m (d-realize method-names) (put reg (string ns "/" m) (tuple proto-name m)))
nil))
(ns-intern core "extenders"
(fn [proto]
# All type-tags whose registry entry implements this protocol, as symbols
# (closest analog to Clojure's class list); nil when none.
(let [pname (get (get proto :name) :name)
registry (get (ctx :env) :type-registry)
out @[]]
(each tag (keys registry)
(when (get (get registry tag) pname)
(array/push out {:jolt/type :symbol :ns nil :name tag})))
(if (empty? out) nil (tuple ;out)))))
(ns-intern core "register-method"
(fn [type-name proto-name method-name f]
(register-method-impl ctx type-name proto-name method-name f)))
(ns-intern core "make-reified"
(fn [methods-map & proto-names] (make-reified-impl ctx methods-map proto-names)))
# Host-class shim registration, exposed to Clojure so a library can mirror a
# Java class jolt doesn't ship (e.g. reitit.Trie). __register-class-statics!
# makes (Class/method ...) resolve; __register-class-methods! makes (.method
# tagged-value ...) dispatch; __register-class-ctor! makes (Class. ...) build.
# Reader-conditional feature toggle, exposed to Clojure so a namespace can
# load a clj-targeted library (e.g. reitit, under :clj) WITHOUT forcing the
# whole process to :clj — set features, require the lib, restore. Returns the
# previous feature set (a list of name strings) for restoration.
(ns-intern core "__reader-features"
(fn [] (tuple ;(map (fn [k] (string k)) (keys reader-features)))))
(ns-intern core "__reader-features-set!"
(fn [names]
# names arrives as a jolt vector (pvec) or list — coerce to a janet array
(def arr (cond (pvec? names) (pv->array names)
(or (tuple? names) (array? names)) names
@[names]))
(reader-features-set! (map (fn [n] (if (keyword? n) n (string n))) arr))
nil))
(ns-intern core "__register-class-statics!"
(fn [nm tbl] (register-class-statics! nm tbl) nil))
(ns-intern core "__register-class-methods!"
(fn [tag tbl] (register-tagged-methods! tag tbl) nil))
(ns-intern core "__register-class-ctor!"
(fn [nm f] (register-class-ctor! nm f) (ns-intern core nm (class-value-for nm)) nil))
(ns-intern core "require" (fn [& specs] (require-impl ctx ;specs)))
(ns-intern core "in-ns" (fn [sym] (in-ns-impl ctx sym)))
(ns-intern core "use" (fn [& specs] (use-impl ctx ;specs)))
(ns-intern core "import" (fn [& specs] (import-impl ctx ;specs)))
(ns-intern core "refer-clojure" (fn [& args] (refer-clojure-impl ctx ;args)))
(ns-intern core "defmulti-setup" (fn [name-sym dispatch & opts] (defmulti-setup ctx name-sym dispatch ;opts)))
(ns-intern core "defmethod-setup" (fn [mm-sym dval impl] (defmethod-setup ctx mm-sym dval impl)))
(ns-intern core "make-deftype-ctor" (fn [name-sym field-kws &opt field-tags field-muts] (make-deftype-ctor-impl ctx name-sym field-kws field-tags field-muts)))
# Var/namespace lookups that need the ctx (the rest of the var fns — var-get/
# var-set/var?/alter-var-root/alter-meta!/reset-meta! — are plain core-bindings).
(ns-intern core "find-var" (fn [sym] (find-var ctx sym)))
# *ns*: the current-namespace dynamic var. Its root is kept in sync by
# ctx-set-current-ns via the cached var table (env :ns-var); a thread
# binding (binding [*ns* ...]) shadows the root through var-get as usual.
(def ns-var (ns-intern core "*ns*" (ctx-find-ns ctx (ctx-current-ns ctx))))
(put ns-var :dynamic true)
(put (ctx :env) :ns-var ns-var)
(ns-intern core "intern"
(fn [ns-name sym-name &opt val]
(def ns (ctx-find-ns ctx (if (struct? ns-name) (ns-name :name) ns-name)))
(ns-intern ns (if (struct? sym-name) (sym-name :name) sym-name) val)))
# --- ns introspection (Stage 2 tier 6b) — evaluated-arg Clojure semantics.
# A namespace designator is an ns object (passes through) or a symbol/string
# naming one. find-ns is a pure lookup (nil when absent); create-ns creates
# (ctx-find-ns is create-on-demand). The optional-arg forms default to the
# current ns, preserving the prior 0-arg interpreter behavior.
(def ns-name-of (fn [x]
(cond
(and (struct? x) (= :symbol (x :jolt/type))) (x :name)
(string? x) x
(keyword? x) (string x)
nil)))
(def ns-of (fn [x]
(if (= :jolt/namespace (get x :jolt/type))
x
(let [nm (ns-name-of x)]
(if nm (get (get (ctx :env) :namespaces) nm) nil)))))
(def ns-or-current (fn [x]
(if (nil? x)
(ctx-find-ns ctx (ctx-current-ns ctx))
(or (ns-of x) (error (string "No namespace: " (ns-name-of x)))))))
(ns-intern core "find-ns" (fn [x] (ns-of x)))
(ns-intern core "create-ns" (fn [x] (ctx-find-ns ctx (ns-name-of x))))
(ns-intern core "remove-ns" (fn [x] (remove-ns ctx (ns-name-of x))))
(ns-intern core "all-ns" (fn [] (all-ns ctx)))
(ns-intern core "the-ns" (fn [&opt x] (ns-or-current x)))
# interns/imports return a jolt MAP (struct), not the live host table — so
# count/seq/keys work on them, and callers can't mutate the ns through them.
(ns-intern core "ns-interns" (fn [&opt x] (table/to-struct ((ns-or-current x) :mappings))))
# {alias-symbol -> namespace object}, Clojure's shape, from the string store.
(ns-intern core "ns-aliases"
(fn [&opt x]
(def ns (ns-or-current x))
(def out @{})
(eachp [a target] (ns :aliases)
(put out {:jolt/type :symbol :ns nil :name a} (ctx-find-ns ctx target)))
(table/to-struct out)))
(ns-intern core "ns-imports" (fn [&opt x] (table/to-struct ((ns-or-current x) :imports))))
# (ns-resolve ns sym) -> the var or nil. Unqualified syms look in ns's own
# mappings; ns-qualified syms resolve through ns's aliases. (types/ns-resolve
# keys ns-find with the symbol struct instead of its name string, so it never
# finds anything — do the lookup here.)
(ns-intern core "ns-resolve"
(fn [ns-d sym]
(def ns (ns-or-current ns-d))
(def nm (if (struct? sym) (sym :name) (string sym)))
(def nsp (if (struct? sym) (sym :ns) nil))
(if nsp
(let [target (or (ns-alias-lookup ns nsp) nsp)
target-ns (ctx-find-ns ctx target)]
(when target-ns (ns-find target-ns nm)))
(ns-find ns nm))))
(ns-intern core "resolve"
(fn [sym]
(when (and (struct? sym) (= :symbol (sym :jolt/type)))
(def r (protect (resolve-var ctx @{} sym)))
(if (r 0) (r 1) nil))))
# refer: bring another ns's public vars into the current ns. Reuses use-impl's
# refer-all behavior; the :only/:exclude/:rename filters are not yet honored.
(ns-intern core "refer" (fn [ns-sym & filters] (use-impl ctx ns-sym)))
# --- dispatch-table / type fns (Stage 2 tier 6c) ------------------------
# A multimethod's method table lives on its VAR (the value is the dispatch
# closure), so the overlay macros pass the NAME quoted — the defmulti/
# defmethod pattern — and these resolve the var. prefer-method auto-creates
# a missing multimethod (matching the prior interpreter arm).
(def mm-var-of (fn [mm-sym auto-create?]
(def r (protect (resolve-var ctx @{} mm-sym)))
(def found (if (r 0) (r 1) nil))
(if found
found
(when auto-create?
(def ns (ctx-find-ns ctx (ctx-current-ns ctx)))
(def stub (fn [& args] nil))
(def nv (ns-intern ns (mm-sym :name) stub))
(put nv :jolt/methods @{})
(put multi-registry stub nv)
nv))))
(def clear-dispatch-cache! (fn [mm-var]
(let [dc (get mm-var :jolt/dispatch-cache)]
(when dc (each k (keys dc) (put dc k nil))))))
(ns-intern core "prefer-method-setup"
(fn [mm-sym dval-a dval-b]
(def mm-var (mm-var-of mm-sym true))
(def prefs (or (get mm-var :jolt/prefers)
(do (put mm-var :jolt/prefers @{}) (mm-var :jolt/prefers))))
# {x -> {y true ...}}: x is preferred over each y (Clojure's {x #{y}})
(def sub (or (get prefs dval-a)
(do (put prefs dval-a @{}) (get prefs dval-a))))
(put sub dval-b true)
(clear-dispatch-cache! mm-var)
mm-var))
(ns-intern core "remove-method-setup"
(fn [mm-sym dval]
(def dval (if (nil? dval) :jolt/nil-sentinel dval))
(def mm-var (mm-var-of mm-sym false))
(when mm-var
(let [methods (get mm-var :jolt/methods)]
(when methods (put methods dval nil)))
(clear-dispatch-cache! mm-var))
mm-var))
(ns-intern core "remove-all-methods-setup"
(fn [mm-sym]
(def mm-var (mm-var-of mm-sym false))
(when mm-var
# clear IN PLACE: the dispatch closure captured this table at defmulti
# time, so swapping in a fresh one leaves dispatch seeing stale methods
(let [methods (get mm-var :jolt/methods)]
(when methods (each k (keys methods) (put methods k nil))))
(clear-dispatch-cache! mm-var))
mm-var))
(ns-intern core "prefers-setup"
(fn [mm-sym]
(def mm-var (mm-var-of mm-sym false))
(or (and mm-var (get mm-var :jolt/prefers)) {})))
# methods/get-method receive the multimethod VALUE (Clojure semantics): map it
# back to its var via multi-registry. A symbol arg still works (mm-var-of), for
# any caller that passes one.
(def mm-var-of-val (fn [mm]
(if (function? mm) (get multi-registry mm) (mm-var-of mm false))))
(ns-intern core "get-method-setup"
(fn [mm dval]
(def dval (if (nil? dval) :jolt/nil-sentinel dval))
(def mm-var (mm-var-of-val mm))
(when mm-var
(let [methods (get mm-var :jolt/methods)]
(or (get methods dval) (get methods :default))))))
(ns-intern core "methods-setup"
(fn [mm]
(def mm-var (mm-var-of-val mm))
(when mm-var
# a jolt map, not the live host table (and phm so vector dispatch
# values look up by value, same reason build-eval-map promotes)
(var m (make-phm))
(let [tbl (get mm-var :jolt/methods)]
(when tbl (each k (keys tbl) (set m (phm-assoc m k (get tbl k))))))
m)))
# satisfies?: evaluated protocol value + instance. Recognizes a reify the same
# way instance? does — by the protocols it records on itself (a reify's methods
# are instance-local, so they aren't in the global type registry that
# type-satisfies? consults).
(ns-intern core "satisfies?"
(fn [proto obj]
(def pn (proto :name))
(def pn-str (if (struct? pn) (pn :name) pn))
(def protos (if (table? obj) (get obj :jolt/protocols)))
(def type-tag (or (record-tag obj)
(if (and (table? obj) (get obj :jolt/protocol-methods))
(get obj :jolt/deftype))))
(cond
(and protos (string? pn-str)
(truthy? (some (fn [p] (= (last (string/split "." p))
(last (string/split "." pn-str))))
protos))) true
type-tag (type-satisfies? ctx type-tag pn-str)
false)))
# instance?: the overlay macro passes the TYPE NAME quoted (class names don't
# evaluate to values on jolt); the value arg arrives evaluated.
# Generic hook so an external host-shim library (e.g. jolt-lang/http-client)
# can teach instance? about its own shim types without editing core. Each hook
# is (fn [class-name val] -> truthy|nil); consulted when no built-in matches.
(var instance-check-hooks @[])
(ns-intern core "__register-instance-check!"
(fn [f] (array/push instance-check-hooks f) nil))
(ns-intern core "instance-check"
(fn [type-sym val]
(if (record-tag val)
(let [type-tag (record-tag val)
type-name (type-sym :name)]
(or (= type-tag type-name)
(and (> (length type-tag) (length type-name))
(= (string/slice type-tag (- (length type-tag) (length type-name)))
type-name))
# instance? of a PROTOCOL works like satisfies?: a reify implementing
# it is an instance. The reify records every protocol it implements
# (short names); (instance? a.b.Proto x) passes a qualified name, so
# match by short name against any of them. (malli relies on this.)
(let [protos (if (table? val) (get val :jolt/protocols))
tn-short (last (string/split "." type-name))]
(and protos (truthy? (some (fn [p] (= (last (string/split "." p)) tn-short)) protos))))))
(match (type-sym :name)
"Number" (number? val)
"java.lang.Number" (number? val)
"Long" (number? val)
"java.lang.Long" (number? val)
"Integer" (number? val)
"Double" (number? val)
"String" (string? val)
"java.lang.String" (string? val)
# String implements CharSequence — malli's :re validator gates on
# (instance? CharSequence x) before matching (jolt-ltwk).
"CharSequence" (string? val)
"java.lang.CharSequence" (string? val)
"Boolean" (or (= true val) (= false val))
"Keyword" (keyword? val)
# regex patterns (cuerdas-style (instance? Pattern x) checks)
"Pattern" (and (table? val) (= :jolt/regex (val :jolt/type)))
"java.util.regex.Pattern" (and (table? val) (= :jolt/regex (val :jolt/type)))
"Character" (and (struct? val) (= :jolt/char (get val :jolt/type)))
"java.lang.Character" (and (struct? val) (= :jolt/char (get val :jolt/type)))
# java.time shims (host_interop.janet); #inst IS java.util.Date in Clojure
"java.util.Date" (and (struct? val) (= :jolt/inst (get val :jolt/type)))
"Date" (and (struct? val) (= :jolt/inst (get val :jolt/type)))
"Instant" (and (table? val) (= :jolt/instant (get val :jolt/type)))
"java.time.Instant" (and (table? val) (= :jolt/instant (get val :jolt/type)))
"LocalDateTime" (and (table? val) (= :jolt/local-dt (get val :jolt/type)))
"java.time.LocalDateTime" (and (table? val) (= :jolt/local-dt (get val :jolt/type)))
"ZonedDateTime" (and (table? val) (= :jolt/zoned-dt (get val :jolt/type)))
"java.time.ZonedDateTime" (and (table? val) (= :jolt/zoned-dt (get val :jolt/type)))
"LocalTime" false
"LocalDate" false
"java.sql.Time" false
"java.sql.Timestamp" false
"java.sql.Date" false
"DateTimeFormatter" (and (table? val) (= :jolt/dt-formatter (get val :jolt/type)))
"URL" (and (table? val) (= :jolt/url (get val :jolt/type)))
"java.net.URL" (and (table? val) (= :jolt/url (get val :jolt/type)))
# next.jdbc host shim: a wrapped jdbc.core connection (core.janet).
# migratus's do-commands only runs SQL through its (instance? Connection)
# branch, so the wrapped conn must answer true here.
"Connection" (and (table? val) (= :jolt/jdbc-conn (get val :jolt/type)))
"java.sql.Connection" (and (table? val) (= :jolt/jdbc-conn (get val :jolt/type)))
# java.io.File model (jolt-hjw): io/file and (File. …) build :jolt/file,
# so migratus's (instance? File migration-dir) takes the filesystem path.
"File" (and (table? val) (= :jolt/file (get val :jolt/type)))
"java.io.File" (and (table? val) (= :jolt/file (get val :jolt/type)))
# JVM char[] class — (Class/forName "[C"); jolt char arrays are Janet
# arrays of char structs
"[C" (and (array? val)
(or (= 0 (length val))
(and (struct? (val 0)) (= :jolt/char ((val 0) :jolt/type)))))
"clojure.lang.Atom" (and (table? val) (= :jolt/atom (val :jolt/type)))
"clojure.lang.Volatile" (and (table? val) (= :jolt/volatile (val :jolt/type)))
"clojure.lang.Delay" (and (table? val) (= :jolt/delay (val :jolt/type)))
"clojure.lang.IPersistentMap" (or (phm? val) (struct? val))
"clojure.lang.IPersistentVector" (or (tuple? val) (pvec? val))
"clojure.lang.IPersistentSet" (set? val)
"Object" true
# no built-in match — consult library-registered instance? hooks
(do (var r false)
(each h instance-check-hooks
(when (h (type-sym :name) val) (set r true) (break)))
r)))))
# Reader / expansion as plain fns: read-string parses one form; macroexpand-1
# expands a (quoted, already-evaluated) call form once via its macro var.
(ns-intern core "read-string" (fn [s] (parse-string s)))
# Strip the interpreter's {:jolt/type :jolt/exception :value v} throw envelope.
# Compiled catch uses this so a caught value matches what the interpreter binds
# (interpreted throw wraps; compiled throw raises raw) — keeps (class e) /
# ex-message / catch consistent across the compiled⇄interpreted boundary.
(ns-intern core "__unwrap-ex"
(fn [e] (if (and (or (table? e) (struct? e)) (= :jolt/exception (get e :jolt/type)))
(get e :value) e)))
# The *in* reader family's host seams. __stdin-read-line: one line from real
# stdin, newline stripped, nil at EOF. __parse-next: one form off a string ->
# [form rest-of-string], nil when only whitespace remains. *in*, read-line,
# read, with-in-str, and line-seq are Clojure over these (core/50-io.clj).
# The loader's registered source roots (the closest thing to a classpath) —
# io/resource searches these for relative resource paths.
# registered constructor shims: the NAME evaluates to the canonical class
# string (so class-dispatch defmultis match); `new` finds the ctor fn.
(eachp [nm f] class-ctors (ns-intern core nm (class-value-for nm)))
# dispatch-only type names (no ctor): InputStream, File, ISeq, ...
(eachp [nm canon] class-canonical-names
(unless (or (in class-ctors nm) (ns-find core nm))
(ns-intern core nm canon)))
(ns-intern core "__source-roots"
(fn [] (tuple ;(get (ctx :env) :source-paths))))
(ns-intern core "__stdin-read-line"
(fn []
(let [l (file/read stdin :line)]
(if (nil? l) nil
(let [s (string l)]
(if (string/has-suffix? "\n" s) (string/slice s 0 -2) s))))))
(ns-intern core "__parse-next"
(fn [s]
(if (= 0 (length (string/trim s))) nil
(let [r (parse-next s)] (tuple (r 0) (r 1))))))
(def expand-1 (fn [the-form]
(if (and (array? the-form) (> (length the-form) 0)
(struct? (first the-form)) (= :symbol ((first the-form) :jolt/type)))
(let [v (resolve-var ctx @{} (first the-form))]
(if (and v (var-macro? v))
(apply (var-get v) (tuple/slice the-form 1))
the-form))
the-form)))
(ns-intern core "macroexpand-1" expand-1)
# Apply a registered data reader to an already-read form (EDN built-in tags
# #uuid/#inst and any registered reader). Throws on an unknown tag.
(ns-intern core "__read-tagged"
(fn [tag form]
(def data-readers (get (ctx :env) :data-readers))
(def reader-fn (if data-readers (get data-readers tag)))
(if reader-fn
(reader-fn form)
(error (string "No reader function for tag " tag)))))
# macroexpand: expand repeatedly until the head is no longer a macro (the
# form's SUBFORMS are not expanded, matching Clojure).
(ns-intern core "macroexpand"
(fn [the-form]
(var cur the-form)
(var nxt (expand-1 cur))
(while (not= cur nxt) (set cur nxt) (set nxt (expand-1 cur)))
cur))
# alias bookkeeping is UNIFIED (jolt-ark): :aliases (alias-name string ->
# ns-name string) is the one store, read by resolution and ns-aliases;
# :imports holds class imports only.
(ns-intern core "alias"
(fn [alias-sym ns-sym]
(def cur (ctx-find-ns ctx (ctx-current-ns ctx)))
(ns-add-alias cur (alias-sym :name) (ns-sym :name))
nil))
(ns-intern core "ns-unalias"
(fn [ns-d alias-sym]
(def ns (ns-or-current ns-d))
(put (ns :aliases) (alias-sym :name) nil)
nil))
# ns-publics: {symbol -> var} (jolt has no private vars, so publics = interns).
# Keys are symbol structs (value-hashed), matching Clojure's symbol keys.
(def mappings->symbol-map (fn [ns pred]
(var m (make-phm))
(loop [[nm v] :pairs (ns :mappings)]
(when (pred nm v)
(set m (phm-assoc m {:jolt/type :symbol :ns nil :name nm} v))))
m))
(ns-intern core "ns-publics"
(fn [&opt ns-d]
(mappings->symbol-map (ns-or-current ns-d) (fn [nm v] true))))
# ns-map: all mappings (interns + refers; jolt has no class imports in maps).
(ns-intern core "ns-map"
(fn [&opt ns-d]
(mappings->symbol-map (ns-or-current ns-d) (fn [nm v] true))))
# ns-refers: mappings whose var's HOME ns differs from this ns (copied in by
# refer/use/require :refer).
(ns-intern core "ns-refers"
(fn [&opt ns-d]
(def ns (ns-or-current ns-d))
(def my-name (ns :name))
(mappings->symbol-map ns (fn [nm v]
(and (table? v) (not= (get v :ns) my-name))))))
(ns-intern core "ns-unmap"
(fn [ns-d sym]
(def ns (ns-or-current ns-d))
(put (ns :mappings) (if (struct? sym) (sym :name) (string sym)) nil)
nil))
core)
# Dispatch a special form by its string name.

View file

@ -1,780 +0,0 @@
# Jolt Evaluator — special forms (eval-list dispatch)
# Extracted from evaluator.janet (jolt-oudv, phase 2a split).
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./pv)
(use ./plist)
(use ./config)
(use ./reader)
(use ./regex)
(use ./eval_base)
(use ./eval_resolve)
(use ./eval_runtime)
(defn- unwrap-meta-name
"Recursively unwrap (with-meta sym meta) forms to extract the underlying symbol.
Returns the symbol struct, or the original form if it's not a with-meta wrapper."
[form]
(if (and (array? form) (> (length form) 0)
(struct? (in form 0))
(= :symbol ((in form 0) :jolt/type))
(= "with-meta" ((in form 0) :name)))
(unwrap-meta-name (in form 1))
form))
# --- special-form handlers (exploded from eval-list, jolt-oudv) ---
(defn eval-do [ctx bindings form]
(do
(var result nil)
(var i 1)
(let [len (length form)]
(while (< i len)
(set result (eval-form ctx bindings (in form i)))
(++ i)))
result))
(defn eval-if [ctx bindings form]
(do
# 2 or 3 argument forms only (spec 03-special-forms X1)
(when (or (< (length form) 3) (> (length form) 4))
(error (string "Wrong number of args (" (dec (length form)) ") passed to: if")))
(let [test-val (eval-form ctx bindings (in form 1))]
(if (and (not (nil? test-val)) (not (= false test-val)))
(eval-form ctx bindings (in form 2))
(if (> (length form) 3) (eval-form ctx bindings (in form 3)) nil)))))
(defn eval-def [ctx bindings form]
(let [raw-name (in form 1)
name-sym (unwrap-meta-name raw-name)
# Metadata on the name: keyword/type-hint metadata rides on the
# symbol (:meta); a ^{:map} reads as a with-meta form we evaluate.
sym-meta (or (and (struct? name-sym) (get name-sym :meta)) {})
wm-meta (if (and (array? raw-name) (> (length raw-name) 0)
(sym-name? (first raw-name) "with-meta"))
(let [mv (protect (eval-form ctx bindings (last raw-name)))]
(if (and (mv 0) (or (table? (mv 1)) (struct? (mv 1)))) (mv 1) {}))
{})
name-meta (merge wm-meta sym-meta)
dynamic? (truthy? (get name-meta :dynamic))
ns-name (ctx-current-ns ctx)
ns (ctx-find-ns ctx ns-name)
# Create var first (unbound) so self-referencing defs resolve
v (ns-intern ns (name-sym :name))]
# (def name) with no init interns the var and leaves any existing
# root binding alone (Clojure semantics — this is what declare
# expands to, so compiled forward refs bind to the var instead of
# falling through to a like-named host builtin).
(if (= 2 (length form))
(do
(when (not (empty? name-meta))
(put v :meta (merge (or (get v :meta) {}) name-meta)))
(when dynamic? (put v :dynamic true))
v)
(let [# (def name docstring value): docstring form 2, value form 3
has-doc (and (> (length form) 3) (string? (in form 2)))
val-form (in form (if has-doc 3 2))
val (eval-form ctx bindings val-form)]
(bind-root v val)
# Staged bootstrap (jolt-4j3): pre/at-kernel overlay defns load
# interpreted; stash the fn source so backend/recompile-defns! can
# compile them once the analyzer is alive — the defn analog of
# :macro-src. Only set while api/load-core-overlay! loads the early
# tiers (the flag scopes it away from user code).
(when (and (get (ctx :env) :stash-defn-src?)
(function? val)
(array? val-form) (> (length val-form) 0)
(or (sym-name? (first val-form) "fn")
(sym-name? (first val-form) "fn*")))
(put v :defn-src val-form))
(let [extra (if has-doc (merge name-meta {:doc (in form 2)}) name-meta)]
(when (not (empty? extra))
(put v :meta (merge (or (get v :meta) {}) extra))))
(when dynamic?
(put v :dynamic true))
# def returns the var (Clojure semantics); REPL prints #'ns/name
v))))
(defn eval-defmacro [ctx bindings form]
(let [# ^{:map} metadata on the name reads as a (with-meta sym …)
# form (jolt-8w2); unwrap to the bare symbol like def does.
name-sym (unwrap-meta-name (in form 1))
after-name (tuple/slice form 2)
# Skip an optional leading docstring (string) then an optional
# attr-map (a struct that is not a symbol — a map literal reads
# as a struct), matching defn. Real macros use both, e.g.
# (defmacro info "doc" {:arglists '(...)} [& args] …).
a1 (if (and (> (length after-name) 0) (string? (first after-name)))
(tuple/slice after-name 1) after-name)
after-meta (if (and (> (length a1) 0)
(struct? (first a1))
(not= :symbol (get (first a1) :jolt/type)))
(tuple/slice a1 1) a1)
# What remains is either a params VECTOR (tuple) + body, or one
# or more arity CLAUSES (each a list, i.e. a janet array). Build
# a uniform arity list [{:params … :body …} …].
multi? (and (> (length after-meta) 0) (array? (first after-meta)))
arities (if multi?
(map (fn [cl] {:params (first cl) :body (tuple/slice cl 1)})
after-meta)
@[{:params (first after-meta) :body (tuple/slice after-meta 1)}])
defining-ns (ctx-current-ns ctx)]
(def interp-fn (fn [& macro-args]
(def n (length macro-args))
# Pick the arity: an exact fixed-count match wins; otherwise the
# first variadic arity that accepts n args (Clojure fn dispatch).
(var chosen nil)
(each ar arities
(def pi (parse-params (ar :params)))
(when (and (nil? chosen) (not (pi :rest)) (= n (length (pi :fixed))))
(set chosen [pi (ar :body)])))
(when (nil? chosen)
(each ar arities
(def pi (parse-params (ar :params)))
(when (and (nil? chosen) (pi :rest) (>= n (length (pi :fixed))))
(set chosen [pi (ar :body)]))))
(when (nil? chosen)
(error (string "no matching arity for macro " (name-sym :name)
" (" n " args)")))
(def pi (chosen 0))
(def body (chosen 1))
(var new-bindings @{})
(table/setproto new-bindings bindings)
(put new-bindings "&env" @{}) # implicit &env for macro bodies (table — nil-safe)
(var i 0)
# Destructure macro params (like fn), so [& [a & more :as all]]
# and {:keys …} rest forms work in macro arglists.
(each pat (pi :fixed)
(destructure-bind ctx new-bindings pat (macro-args i))
(++ i))
(when (pi :rest)
(destructure-bind ctx new-bindings (pi :rest) (rest-args-val macro-args i)))
# Use defining namespace for symbol resolution
(def saved-ns (ctx-current-ns ctx))
(ctx-set-current-ns ctx defining-ns)
# Plain trailing restore (NOT defer/try — those build a fiber per
# call and blow the C stack on deep interpreted recursion). An
# unwinding throw is repaired once at the TOP-LEVEL boundary
# (loader/eval-toplevel restores the ns on error).
(var result nil)
(each bf body
(set result (eval-form ctx new-bindings bf)))
(ctx-set-current-ns ctx saved-ns)
result))
# A COMPILED expander (native-speed) is only built for the
# single-arity case (the compile hook + recompile path take one
# [args body]); multi-arity macros use the interpreted expander.
(def single? (= 1 (length arities)))
(def args-form (and single? ((first arities) :params)))
(def body (and single? ((first arities) :body)))
(def uses-env (do (var u false)
(each ar arities
(when (or (form-uses-sym? (ar :body) "&env")
(form-uses-sym? (ar :body) "&form"))
(set u true)))
u))
(def compiled-fn
(when (and macro-compile-hook single? (not uses-env))
(macro-compile-hook ctx args-form body)))
(def macro-fn (or compiled-fn interp-fn))
(let [ns-name (ctx-current-ns ctx)
ns (ctx-find-ns ctx ns-name)]
(def v (ns-intern ns (name-sym :name) macro-fn))
(put v :macro true)
# Stash the expander source so backend/recompile-macros! can
# compile it once the analyzer is alive (staged bootstrap): a
# macro defined WHILE the analyzer is still being built gets an
# interpreted closure now, a compiled expander later. uses-env
# macros stay interpreted (the compiled fn* has no &env/&form);
# multi-arity macros keep the interpreted dispatch (no single
# [args body] to recompile).
(when single? (put v :macro-src @[args-form body]))
(put v :macro-uses-env uses-env)
(when compiled-fn (put v :macro-compiled true))
# A (re)defined macro invalidates any cached expansions.
(table/clear macro-cache)
(var-get v))))
(defn eval-fn* [ctx bindings form]
(let [# optional name: (fn* name [args] ...) / (fn* name ([args] ...)...)
named? (and (struct? (in form 1)) (= :symbol ((in form 1) :jolt/type)))
fn-name (if named? ((in form 1) :name) nil)
form (if named? (array/concat @[(in form 0)] (tuple/slice form 2)) form)]
(if (array? (in form 1))
# Multi-arity: (fn* ([args] body...) ([args] body...)...)
(let [pairs (tuple/slice form 1)
arities @{}
defining-ns (ctx-current-ns ctx)]
(var self nil)
# The (single) variadic clause is dispatched separately: it handles
# any arg count >= its fixed count. Storing it in `arities` by
# fixed-count would collide with a same-fixed-count fixed clause and
# only match that exact count.
(var variadic-fn nil)
(var variadic-min 0)
(each pair pairs
(let [args-form (in pair 0)
body (tuple/slice pair 1)
param-info (parse-params args-form)
_ (require-symbol-params param-info)
fixed-pats (param-info :fixed)
rest-pat (param-info :rest)
n-fixed (length fixed-pats)
# recur-entry: where (recur ...) re-enters THIS arity. For
# a fixed arity it's the dispatcher (exact count re-selects
# it). For the VARIADIC arity, recur takes n-fixed + 1 args
# with the LAST bound DIRECTLY as the rest seq (Clojure) —
# re-entering through the varargs collector would wrap it
# in a fresh 1-element rest list and the seq never empties
# (the jolt-4df hang).
recur-entry-box @[nil]
run-clause (fn [fn-bindings]
(put fn-bindings :jolt/loop-fn (in recur-entry-box 0))
(when fn-name (bind-put fn-bindings fn-name self))
# Use defining namespace for symbol resolution
(def saved-ns (ctx-current-ns ctx))
(ctx-set-current-ns ctx defining-ns)
# Plain trailing restore (NOT defer/try — those build a fiber per
# call and blow the C stack on deep interpreted recursion). An
# unwinding throw is repaired once at the TOP-LEVEL boundary
# (loader/eval-toplevel restores the ns on error).
(var result nil)
(each body-form body
(set result (eval-form ctx fn-bindings body-form)))
(ctx-set-current-ns ctx saved-ns)
result)
f (fn [& fn-args]
(var fn-bindings @{})
(table/setproto fn-bindings bindings)
(var i 0)
(each pat fixed-pats
(destructure-bind ctx fn-bindings pat (fn-args i))
(++ i))
(when rest-pat
(destructure-bind ctx fn-bindings rest-pat (rest-args-val fn-args i)))
(run-clause fn-bindings))]
(if rest-pat
(do
(put recur-entry-box 0
(fn [& recur-args]
(var fn-bindings @{})
(table/setproto fn-bindings bindings)
(var i 0)
(each pat fixed-pats
(destructure-bind ctx fn-bindings pat (recur-args i))
(++ i))
(destructure-bind ctx fn-bindings rest-pat (get recur-args i))
(run-clause fn-bindings)))
(set variadic-fn f) (set variadic-min n-fixed))
(do
(put recur-entry-box 0 (fn [& recur-args] (apply self recur-args)))
(put arities n-fixed f)))))
(set self (fn [& fn-args]
(let [n (length fn-args)
f (get arities n)]
(cond
f (apply f fn-args)
(and variadic-fn (>= n variadic-min)) (apply variadic-fn fn-args)
(error (string "Wrong number of args (" n ") passed to: "
(or fn-name "fn")))))))
self)
# Single-arity: (fn* [args] body...)
(let [args-form (in form 1)
body (tuple/slice form 2)
param-info (parse-params args-form)
_ (require-symbol-params param-info)
fixed-pats (param-info :fixed)
rest-pat (param-info :rest)
defining-ns (ctx-current-ns ctx)]
(var self nil)
(var recur-entry nil)
(def run-body (fn [fn-bindings]
(put fn-bindings :jolt/loop-fn recur-entry)
(when fn-name (bind-put fn-bindings fn-name self))
# Use defining namespace for symbol resolution
(def saved-ns (ctx-current-ns ctx))
(ctx-set-current-ns ctx defining-ns)
# Plain trailing restore (NOT defer/try — those build a fiber per
# call and blow the C stack on deep interpreted recursion). An
# unwinding throw is repaired once at the TOP-LEVEL boundary
# (loader/eval-toplevel restores the ns on error).
(var result nil)
(each body-form body
(set result (eval-form ctx fn-bindings body-form)))
(ctx-set-current-ns ctx saved-ns)
result))
(def n-fixed (length fixed-pats))
(set self (fn [& fn-args]
# ArityException semantics (jolt-6xn): a fixed arity takes
# exactly its params, a variadic one at least its fixed params.
# The compiled path enforces this natively (janet fn arity);
# this keeps the interpreter oracle in agreement.
(def n (length fn-args))
(when (if rest-pat (< n n-fixed) (not= n n-fixed))
(error (string "Wrong number of args (" n ") passed to: "
(or fn-name "fn"))))
(var fn-bindings @{})
(table/setproto fn-bindings bindings)
(var i 0)
(each pat fixed-pats
(destructure-bind ctx fn-bindings pat (fn-args i))
(++ i))
(when rest-pat
(destructure-bind ctx fn-bindings rest-pat (rest-args-val fn-args i)))
(run-body fn-bindings)))
# recur re-enters here: for a variadic fn it takes n-fixed + 1
# args, the LAST bound DIRECTLY as the rest seq (Clojure) — going
# back through the varargs collector wrapped the seq in a fresh
# 1-element rest list, so it never emptied (the jolt-4df hang).
(set recur-entry
(if rest-pat
(fn [& recur-args]
(var fn-bindings @{})
(table/setproto fn-bindings bindings)
(var i 0)
(each pat fixed-pats
(destructure-bind ctx fn-bindings pat (recur-args i))
(++ i))
(destructure-bind ctx fn-bindings rest-pat (get recur-args i))
(run-body fn-bindings))
self))
self))))
(defn eval-let* [ctx bindings form]
(let [bind-vec (in form 1)
body (tuple/slice form 2)]
(var new-bindings @{})
(table/setproto new-bindings bindings)
(var i 0)
(let [len (length bind-vec)]
(while (< i len)
(let [pat (bind-vec i)]
# let* is a primitive (the let macro desugars destructuring);
# its binding names must be plain symbols, as in Clojure.
(unless (plain-sym? pat) (error "Bad binding form, expected symbol"))
(def val (eval-form ctx new-bindings (bind-vec (+ i 1))))
(destructure-bind ctx new-bindings pat val)
(+= i 2))))
(var result nil)
(each body-form body
(set result (eval-form ctx new-bindings body-form)))
result))
(defn eval-loop* [ctx bindings form]
(let [bind-vec (in form 1)
body (tuple/slice form 2)
init-vals @[]
patterns @[]
# Inits are evaluated sequentially in an accumulating scope (like
# let*), so a later init can reference an earlier binding —
# matching Clojure's loop.
seq-bindings @{}]
(table/setproto seq-bindings bindings)
(var i 0)
(while (< i (length bind-vec))
# loop* is a primitive (the loop macro desugars destructuring);
# its binding names must be plain symbols, as in Clojure.
(unless (plain-sym? (bind-vec i)) (error "Bad binding form, expected symbol"))
(def v (eval-form ctx seq-bindings (bind-vec (+ i 1))))
(bind-put seq-bindings ((bind-vec i) :name) v)
(array/push init-vals v)
(array/push patterns (bind-vec i))
(+= i 2))
(var loop-fn nil)
(set loop-fn (fn [& args]
(var loop-bindings @{})
(table/setproto loop-bindings bindings)
(var j 0)
(each pat patterns
(destructure-bind ctx loop-bindings pat (in args j))
(++ j))
(put loop-bindings :jolt/loop-fn loop-fn)
(var result nil)
(each body-form body
(set result (eval-form ctx loop-bindings body-form)))
result))
(apply loop-fn init-vals)))
(defn eval-recur [ctx bindings form]
(let [loop-fn (get bindings :jolt/loop-fn)]
(if (nil? loop-fn)
(error "recur used outside of loop* or fn*")
(let [args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
(apply loop-fn args)))))
(defn eval-try [ctx bindings form]
(let [# The body is EVERY form between `try` and the first catch/finally
# clause (not just form 1 — a multi-form body before the clauses,
# e.g. (try (foo) (bar) (catch …)), dropped all but the first).
forms (tuple/slice form 1)
clause? (fn [c]
(and (array? c) (> (length c) 0)
(struct? (first c)) (= :symbol ((first c) :jolt/type))
(or (= "catch" ((first c) :name))
(= "finally" ((first c) :name)))))
split (do (var k 0)
(while (and (< k (length forms)) (not (clause? (in forms k)))) (++ k))
k)
body-forms (tuple/slice forms 0 split)
clauses (tuple/slice forms split)
# 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-body nil)
(var finally-body nil)
(each clause clauses
(when (and (array? clause) (> (length clause) 0))
(let [head (first clause)]
(when (and (struct? head) (= :symbol (head :jolt/type)))
(match (head :name)
"catch" (do
# (catch class binding body*) — binding (3rd elem) must
# be a symbol. Validate up front (even when the body
# never throws) so a malformed clause is a clean error,
# not a silent nil or an internal crash when caught.
(let [b (when (>= (length clause) 3) (in clause 2))]
(unless (and b (struct? b) (= :symbol (b :jolt/type)))
(error "Unable to parse catch clause; expected (catch class binding body*)")))
(set catch-sym (in clause 2))
(set catch-body (tuple/slice clause 3)))
"finally" (set finally-body (tuple/slice clause 1)))))))
(defn eval-body []
(var result nil)
(each bf body-forms (set result (eval-form ctx bindings bf)))
result)
(defn run-finally []
(when finally-body
(each fb finally-body (eval-form ctx bindings fb))))
(defn run-protected []
(if catch-sym
(try
(eval-body)
([err]
(ctx-set-current-ns ctx try-ns)
(var new-bindings @{})
(table/setproto new-bindings bindings)
# bind the originally-thrown value (unwrap the :jolt/exception
# envelope) so (catch … e (throw e)) rethrows the same value
# rather than nesting another envelope
(def caught
(if (and (or (table? err) (struct? err)) (= :jolt/exception (get err :jolt/type)))
(get err :value)
err))
(put new-bindings (catch-sym :name) caught)
(var result nil)
(each cb catch-body
(set result (eval-form ctx new-bindings cb)))
result))
# no catch: restore the ns on an unwinding error, then re-raise
(try (eval-body) ([err] (ctx-set-current-ns ctx try-ns) (error err)))))
# finally ALWAYS runs (success, caught error, or rethrow) — defer so it
# fires even if a catch body throws. Without a finally, just run.
(if finally-body
(defer (run-finally) (run-protected))
(run-protected))))
(defn eval-set! [ctx bindings form]
(let [target (in form 1)
val (eval-form ctx bindings (in form 2))]
# Handle (set! (.-field obj) val) — .-field shorthand as a list
(if (and (array? target) (> (length target) 1)
(struct? (first target)) (= :symbol ((first target) :jolt/type))
(> (length ((first target) :name)) 1)
(= (string/slice ((first target) :name) 0 2) ".-"))
(let [obj (eval-form ctx bindings (in target 1))
field-name (string/slice ((first target) :name) 2)
field-key (keyword field-name)]
(if (get obj :jolt/deftype)
(do (put obj field-key val) val)
(error (string "Can't set! field on non-deftype: " (type obj)))))
# (set! (. obj -field) val) — instance field mutation
(if (and (array? target) (> (length target) 0)
(struct? (first target))
(= :symbol ((first target) :jolt/type))
(= "." ((first target) :name)))
(let [obj (eval-form ctx bindings (in target 1))
field-sym (in target 2)
field-name (field-sym :name)
field-key (keyword (if (and (> (length field-name) 0) (= "-" (string/slice field-name 0 1)))
(string/slice field-name 1)
field-name))]
(if (get obj :jolt/deftype)
(do (put obj field-key val) val)
(error (string "Can't set! field on non-deftype: " (type obj)))))
# (set! var val) — normal var mutation
(let [target-sym target
v (resolve-var ctx bindings target-sym)]
(if v
(do (var-set v val) val)
# Auto-create var if it doesn't exist
(let [ns-name (ctx-current-ns ctx)
ns (ctx-find-ns ctx ns-name)]
(def new-v (ns-intern ns (target-sym :name) val))
val)))))))
(defn eval-new [ctx bindings form]
(let [type-sym (in form 1)
args (map |(eval-form ctx bindings $) (tuple/slice form 2))
ctor (eval-form ctx bindings type-sym)
ctor (if (string? ctor) (or (ctor-for-class-token ctor) ctor) ctor)]
(apply ctor args)))
# Member dispatch shared by the two `.` forms (jolt-eos3). `args` is the
# (possibly empty) tuple of already-evaluated arguments; `has-args` is true for
# the call form `(. obj method arg...)` and false for the bare form
# `(. obj member)`. The two forms agree on the string/number/object/tagged-shim
# dispatch chain (single-sourced here, so an interop change touches one place)
# but diverge in the tail: the call form tries record → native-field →
# coll-interop(args); the bare form tries zero-arg coll-interop → field /
# zero-arg method. The guards that differed between the old copy-pasted arms are
# keyed off `has-args` so behavior is identical (note: the object-methods guard
# checks `table?` only, while tagged dispatch checks table-or-struct — both kept
# verbatim from the original arms).
# A record's own implementation of `field-name` (its instance fn, a reified fn,
# or a protocol method from the type registry), or nil. A deftype/defrecord
# method must win over the generic object-methods table — e.g. a custom
# (Object (toString [_] ...)) over the default toString (jolt-rt6n).
(defn- record-member [ctx target field-name]
(when (record-tag target)
(let [mk (keyword field-name)
own (get target mk)
reified (get (get target :jolt/protocol-methods) mk)]
(cond
(or (function? own) (cfunction? own)) own
(or (function? reified) (cfunction? reified)) reified
(find-method-any-protocol ctx (record-tag target) field-name)))))
(defn dispatch-member [ctx bindings target member-raw member-name field-name args has-args]
(cond
# java.lang.String surface for string/buffer targets
(or (string? target) (buffer? target))
(let [m (get string-methods field-name)]
(if m
(m (string target) ;args)
(if-let [om (get object-methods field-name)]
(om (string target) ;args)
(error (string "Unsupported String method ." field-name)))))
# numeric methods
(and (number? target) (get number-methods field-name))
((get number-methods field-name) target ;args)
# universal object methods — skipped when a shim tag-table owns the member,
# OR when the target is a record that implements the member itself (so a
# deftype's own toString/equals/hashCode wins over the generic one, jolt-rt6n).
# Call form defers to tagged dispatch whenever a tag-table exists; bare form
# only when the tag-table actually carries this member, so zero-arg
# toString/hashCode still reach object-methods on shim objects.
(and (get object-methods field-name)
(not (and (table? target) (get tagged-methods (get target :jolt/type))
(or has-args (get (get tagged-methods (get target :jolt/type)) field-name))))
(not (record-member ctx target field-name)))
((get object-methods field-name) target ;args)
# registered shim objects (java.time etc.): tag-keyed method tables
(and (or (table? target) (struct? target))
(get tagged-methods (get target :jolt/type))
(or has-args (get (get tagged-methods (get target :jolt/type)) field-name)))
(let [m (get (get tagged-methods (get target :jolt/type)) field-name)]
(if m
(m target ;args)
(error (string "Unsupported method ." field-name " on " (string (get target :jolt/type))))))
# --- divergent tail ---
has-args
# (. obj method args...): record protocol dispatch, else native field/method
(if (record-tag target)
# deftype/reify methods live in the protocol registry (or the instance's
# reified-fns table), not on the instance. get is safe on a shape-rec
# tuple (returns nil for the method/protocol keys).
(let [method-key (keyword field-name)
own (get target method-key)
reified (get (get target :jolt/protocol-methods) method-key)
m (cond
(or (function? own) (cfunction? own)) own
(or (function? reified) (cfunction? reified)) reified
(find-method-any-protocol ctx (record-tag target) field-name))]
(if m
(apply m target args)
(error (string "No method ." field-name " on " (record-tag target)))))
# Janet-native interop: try field lookup + call
(if (or (table? target) (struct? target))
(let [method (get target (keyword field-name))]
(if (or (function? method) (cfunction? method))
(method target ;args)
# If stored as fn* form (array), compile to function then call
(if (array? method)
(let [method-fn (eval-form ctx bindings method)]
(if (or (function? method-fn) (cfunction? method-fn))
(method-fn target ;args)
(error (string "Cannot call non-function " field-name " on " (type target)))))
(let [r (if coll-interop (coll-interop target field-name args) :jolt/ci-none)]
(if (= r :jolt/ci-none)
(error (string "Cannot call non-function " field-name " on " (type target)))
r)))))
(error (string "Cannot call method " field-name " on " (type target)))))
# (. obj member) with no extra args: a symbol member naming a function is a
# zero-arg method call (receiver passed as self); a keyword or `-field`
# member is plain field access.
true
# zero-arg Java collection interop (.count/.seq/… on a jolt collection)
# before field lookup — coll-interop returns :jolt/ci-none if not its kind
(let [ci (if coll-interop (coll-interop target field-name @[]) :jolt/ci-none)]
(if (not= ci :jolt/ci-none) ci
(let [v (if (record-tag target)
(coll-lookup target (keyword field-name) nil)
(get target (keyword field-name)))]
(if (and (struct? member-raw) (= :symbol (member-raw :jolt/type))
(not (string/has-prefix? "-" member-name)))
(cond
(or (function? v) (cfunction? v)) (v target)
# zero-arg deftype/reify method via the protocol registry
(record-tag target)
(let [reified (get (get target :jolt/protocol-methods) (keyword field-name))
m (if (or (function? reified) (cfunction? reified)) reified
(find-method-any-protocol ctx (record-tag target) field-name))]
(if m (m target) v))
# value stored as an unevaluated fn* form: compile then call
(array? v) (let [f (eval-form ctx bindings v)]
(if (or (function? f) (cfunction? f)) (f target) f))
v)
v))))))
(defn eval-dot [ctx bindings form]
(let [target (eval-form ctx bindings (in form 1))
member-raw (in form 2)
# Resolve member name: symbols have :name, keywords use string, strings as-is
member-name (if (and (struct? member-raw) (= :symbol (member-raw :jolt/type)))
(member-raw :name)
(if (keyword? member-raw)
(string member-raw)
member-raw))
field-name (if (and (string? member-name) (> (length member-name) 0) (= "-" (string/slice member-name 0 1)))
(string/slice member-name 1)
member-name)
has-args (> (length form) 3)
args (if has-args (map |(eval-form ctx bindings $) (tuple/slice form 3)) @[])]
(dispatch-member ctx bindings target member-raw member-name field-name args has-args)))
(defn eval-list
[ctx bindings form]
(def first-form (first form))
# Safe name extraction: non-symbol heads (e.g. keywords) fall through to default.
# A head qualified to a NON-core namespace (e.g. clojure.edn/read-string) must
# resolve to that var, not the like-named clojure.core special form — so only
# unqualified or clojure.core-qualified heads dispatch as special forms.
(def name (if (and (struct? first-form) (= :symbol (first-form :jolt/type)))
(let [ns (first-form :ns)]
(if (or (nil? ns) (= ns "clojure.core")) (first-form :name) nil))
nil))
(match name
"quote" (in form 1)
# Interpreter builds the form directly (self-contained, no core dependency).
# The COMPILE path instead lowers syntax-quote to construction code (via
# syntax-quote-lower) so a backtick body is compilable; the two are kept in
# sync and cross-checked by conformance (interpret vs compile modes).
"syntax-quote" (syntax-quote* ctx bindings (in form 1))
"unquote" (error "Unquote not valid outside of syntax-quote")
"unquote-splicing" (error "Unquote-splicing not valid outside of syntax-quote")
"eval" (eval-form ctx bindings (eval-form ctx bindings (in form 1)))
# read-string/macroexpand-1 are ctx-capturing clojure.core fns and defonce
# an overlay macro now (Stage 2 tier 6c) — no special-form arms.
"do" (eval-do ctx bindings form)
"if" (eval-if ctx bindings form)
"def" (eval-def ctx bindings form)
"defmacro" (eval-defmacro ctx bindings form)
# ns is now a macro (clojure.core, 30-macros) expanding to in-ns + require/use/
# import/refer-clojure calls — all ctx-capturing fns — so it compiles. No
# special-form arm; an (ns ...) head falls through to the macro-expansion path.
# require / in-ns are now ordinary clojure.core fns (install-stateful-fns!) —
# no special-form arm; they compile + interpret as plain invokes.
# all-ns/the-ns/create-ns/remove-ns/ns-interns/ns-aliases/ns-imports/
# ns-resolve/resolve/find-ns/refer are ctx-capturing clojure.core fns now
# (install-stateful-fns!) with evaluated-arg Clojure semantics — they fall
# through to the function-call default and compile as plain invokes
# (Stage 2 tier 6b).
"fn*" (eval-fn* ctx bindings form)
"let*" (eval-let* ctx bindings form)
"loop*" (eval-loop* ctx bindings form)
"recur" (eval-recur ctx bindings form)
"throw" (let [val (eval-form ctx bindings (in form 1))]
(error {:jolt/type :jolt/exception :value val}))
"try" (eval-try ctx bindings form)
"set!" (eval-set! ctx bindings form)
"var" (let [target-sym (in form 1)
v (resolve-var ctx bindings target-sym)]
(if v v (error (string "Unable to resolve var: " (sym-name-str target-sym) " in var"))))
# var-get/var-set/var?/alter-var-root/alter-meta!/reset-meta! are plain
# clojure.core fns; find-var/intern are ctx-capturing clojure.core fns
# (install-stateful-fns!) — they fall through to the function-call default
# and compile as ordinary invokes (Stage 2 tier 6).
# set?/disj are plain clojure.core fns now (core-set?/core-disj) — no longer
# special-cased here, the analyzer, or compiler.janet (jolt-g3h).
# protocol-dispatch / register-method / make-reified are now ordinary
# clojure.core fns (install-stateful-fns!) — the defprotocol/extend-type/reify
# macros call them with name STRINGS, so they compile + interpret as plain
# invokes (no special-form arms).
# satisfies?/instance?/locking and the multimethod table ops
# (prefer-method/remove-method/remove-all-methods/get-method/methods) are
# clojure.core fns / overlay macros now (Stage 2 tier 6c) — no special arms.
# deftype is now a macro (30-macros) over make-deftype-ctor + extend-type —
# compiles as a plain (do …); no special-form arm.
"new" (eval-new ctx bindings form)
"." (eval-dot ctx bindings form)
# default: function application — check for macros
(if (and (struct? first-form) (= :symbol (first-form :jolt/type)))
(let [sym-name (first-form :name)]
# Handle .-fieldName accessor: (.-cnt obj) → (. obj -cnt)
(if (and (> (length sym-name) 1) (= (string/slice sym-name 0 2) ".-")
(> (length form) 1))
(let [field-name (string/slice sym-name 2)
target (eval-form ctx bindings (in form 1))]
(get target (keyword field-name)))
# (.method obj args...) sugar -> (. obj method args...): desugar and
# re-enter the dot special form (which holds the String surface, the
# deftype method path, and the map-fn fallback).
(if (and (> (length sym-name) 1)
(= (string/slice sym-name 0 1) ".")
(not= sym-name "..")
(> (length form) 1))
(eval-form ctx bindings
(array/concat @[{:jolt/type :symbol :ns nil :name "."}
(in form 1)
{:jolt/type :symbol :ns nil :name (string/slice sym-name 1)}]
(tuple/slice form 2)))
# Handle ClassName. constructor syntax (".." is the member-threading
# macro, not a constructor named ".")
(if (and (> (length sym-name) 1) (not= sym-name "..")
(= (sym-name (- (length sym-name) 1)) 46))
(let [type-name (string/slice sym-name 0 (- (length sym-name) 1))
type-sym {:jolt/type :symbol :ns (first-form :ns) :name type-name}
ctor (eval-form ctx bindings type-sym)
# class names evaluate to canonical-name STRINGS now; the
# constructor itself comes from the ctor registry
ctor (if (string? ctor) (or (ctor-for-class-token ctor) ctor) ctor)
args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
(apply ctor args))
(let [v (resolve-var ctx bindings first-form)]
(if (and v (var-macro? v))
# Expand once (cached by call-form identity), then evaluate the
# macro-free expansion with the current bindings each call.
(let [cached (in macro-cache form)]
(if (not (nil? cached))
(eval-form ctx bindings cached)
(let [expanded (apply (var-get v) (tuple/slice form 1))]
(put macro-cache form expanded)
(eval-form ctx bindings expanded))))
(let [f (eval-form ctx bindings first-form)
args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
(jolt-invoke ctx f args))))))))
(let [f (eval-form ctx bindings first-form)
args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
(jolt-invoke ctx f args)))))
# Build a map value from an array of evaluated [k v k v ...]. A phm (not a Janet
# struct) is used when a key is a collection (value-based hashing) OR a key/value
# is nil (Janet structs drop nil; phm preserves it, matching Clojure). The common
# scalar/nil-free case stays a struct.

View file

@ -1,109 +0,0 @@
# Jolt Evaluator
# Direct interpreter for Clojure forms on Janet.
#
# This file is the AGGREGATOR (jolt-oudv, phase 2a): the interpreter is now split
# into cluster modules, loaded here in dependency order and re-exported
# (:export true) so every consumer keeps a single `(use ./evaluator)`. The
# eval-form entry (set at the bottom) ties resolution, special forms and the
# collection/map literal evaluation together.
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./pv)
(use ./plist)
(use ./config)
(use ./reader)
(use ./regex)
(import ./eval_base :prefix "" :export true)
(import ./eval_resolve :prefix "" :export true)
(import ./eval_runtime :prefix "" :export true)
(import ./eval_special :prefix "" :export true)
(defn- map-needs-phm? [kvs]
(var need false) (var i 0)
(while (< i (length kvs))
(let [k (in kvs i) v (in kvs (+ i 1))]
(when (or (table? k) (array? k) (nil? k) (nil? v)) (set need true) (break)))
(+= i 2))
need)
(defn- build-eval-map [kvs]
(if (map-needs-phm? kvs)
(do (var m (make-phm)) (var j 0)
(while (< j (length kvs)) (set m (phm-assoc m (in kvs j) (in kvs (+ j 1)))) (+= j 2)) m)
(struct ;kvs)))
(set eval-form (fn [ctx bindings form]
(cond
(nil? form) nil
(number? form) form
(string? form) form
(keyword? form) form
(bytes? form) form
(buffer? form) form
(tuple? form)
(let [els (map |(eval-form ctx bindings $) form)]
(if mutable? (array ;els) (pv-from-indexed els)))
(struct? form)
(if (= :symbol (form :jolt/type))
(resolve-sym ctx bindings form)
(if (= :jolt/char (form :jolt/type))
form
# a UUID/inst value flowing back through eval (macro expansion, eval of a
# read form) is a self-evaluating literal, like chars. A namespace object
# does too: `~*ns*` in a syntax-quote (clojure.tools.logging) splices the
# live ns into the expansion.
(if (or (= :jolt/uuid (form :jolt/type)) (= :jolt/inst (form :jolt/type))
(= :jolt/namespace (form :jolt/type)))
form
(if (= :jolt/set (form :jolt/type))
# evaluate each element (set literals like #{(inc 1)} must compute)
(apply make-phs (map |(eval-form ctx bindings $) (form :value)))
(if (= :jolt/tagged (form :jolt/type))
(let [tag (form :tag)
data-readers (get (ctx :env) :data-readers)
reader-fn (if data-readers (get data-readers tag))]
(cond
# #"..." regex literal -> a regex value (Janet PEG-backed)
(= tag :regex) (compile-regex (form :form))
reader-fn (reader-fn (form :form))
(error (string "No reader function for tag " tag))))
(if (get form :jolt/type)
(error (string "Unexpected tagged form: " (form :jolt/type)))
# plain map literal: evaluate keys and values in SOURCE order when
# the reader order rides along (jolt-p3c), hash order otherwise
(let [kvs @[]
order (form-kv-order form)]
(if order
(each x order (array/push kvs (eval-form ctx bindings x)))
(each k (keys form)
(array/push kvs (eval-form ctx bindings k))
(array/push kvs (eval-form ctx bindings (get form k)))))
(build-eval-map kvs))))))))
# A phm map-literal FORM (reader emits one for {:a nil} etc., which a struct
# would have dropped): evaluate its key/value forms and rebuild, preserving nil.
(phm? form)
(let [kvs @[]
order (form-kv-order form)]
(if order
(each x order (array/push kvs (eval-form ctx bindings x)))
(each e (phm-entries form)
(array/push kvs (eval-form ctx bindings (in e 0)))
(array/push kvs (eval-form ctx bindings (in e 1)))))
(build-eval-map kvs))
(array? form)
(if (= 0 (length form))
@[]
(eval-list ctx bindings form))
# A non-array ISeq used as a form is a CALL too (jolt-2rx): cons/concat/list
# and ~@ build a plist or lazy-seq (list?/seq? true, array? false) — without
# this they fell through to self-eval, so (eval (cons '+ '(1 2))) returned the
# list as data instead of 3, and macro output containing such subforms never
# evaluated. d-realize coerces to the element array; an empty list self-evals.
(or (plist? form) (lazy-seq? form))
(let [arr (d-realize form)]
(if (= 0 (length arr)) form (eval-list ctx bindings arr)))
form)))

View file

@ -1,356 +0,0 @@
# Janet implementation of the Jolt host contract (ns `jolt.host`).
#
# This is the seam between the portable jolt-core (analyzer/IR/core, pure Clojure
# under jolt-core/) and the Janet runtime. jolt-core calls ONLY these functions —
# never Janet directly. Re-hosting Jolt to another runtime means reimplementing
# this contract (+ the back end and RT) for that runtime.
#
# Lives in src/jolt/ (with the rest of the Janet host) rather than a separate
# host/janet/ dir: Janet resolves relative imports per-file, so a host/janet
# module importing ../../src/jolt/* loads SECOND instances of compiler/types/core
# (inconsistent state). The portability boundary is the `jolt.host` namespace
# contract + jolt-core/, not the directory.
#
# Two groups:
# 1. Form introspection — reader forms are host-specific (the reader is the
# host's), so shape predicates/accessors live here. Returns jolt values the
# analyzer walks with ordinary Clojure.
# 2. Compile-time environment — resolve symbols to vars/macros, expand macros,
# the current namespace. These take ctx (an opaque host handle).
(use ./types)
(use ./evaluator)
(use ./core)
(import ./phm :as phm)
(import ./pv :as pv)
(import ./reader :as rdr)
# ---------------------------------------------------------------------------
# Form introspection
# ---------------------------------------------------------------------------
(defn h-sym? [form] (and (struct? form) (= :symbol (form :jolt/type))))
(defn h-sym-name [form] (form :name))
(defn h-sym-ns [form] (form :ns))
# Reader metadata on a symbol (e.g. ^:dynamic / ^:redef / ^:private on a def
# name). Returns the meta map (a portable jolt map) or nil. Lets the analyzer
# carry def metadata that the back end applies to the var — without it, compiled
# defs drop all var meta.
#
# The reader builds meta as a Janet TABLE (mutable). Return it as an immutable
# STRUCT so it is a portable jolt value: the Janet back end's merge/get work on a
# struct, AND jolt's own count/map?/keys (used by the portable Clojure emitter,
# jolt.backend-scheme, to emit def metadata) work on a struct but NOT on a raw
# table. A table embedded in the IR is a host value (counter to jolt.ir's "no host
# values embedded") — struct keeps :meta host-neutral. (jolt-me6m)
(defn h-sym-meta [form]
(def m (form :meta))
(if (table? m) (table/to-struct m) m))
(defn h-list? [form] (array? form)) # a call / list (reader: array)
(defn h-vector? [form] (tuple? form)) # a vector literal (reader: tuple)
# A map-literal form is a plain struct, or a phm when the reader preserved a nil
# key/value (Janet structs drop nil). Sets/chars/symbols are tagged structs (have
# :jolt/type); phm carries :jolt/deftype, distinct from those.
(defn h-map? [form]
(or (and (struct? form) (nil? (form :jolt/type)))
(phm/phm? form)))
(defn h-set? [form] (and (struct? form) (= :jolt/set (form :jolt/type))))
(defn h-char? [form] (and (struct? form) (= :jolt/char (form :jolt/type))))
# Codepoint of a char form. Janet rep is a {:ch <cp> :jolt/type :jolt/char} struct;
# the emitter uses this (not a raw :ch read) so the Chez host can answer for its
# native char rep too (form-char-code).
(defn h-char-code [form] (get form :ch))
# A regex literal #"…" reads as a tagged form {:jolt/type :jolt/tagged :tag :regex
# :form "source"}. The analyzer lowers it to a :regex IR node (Chez emits a
# jolt-regex value; the Janet back end punts to the interpreter, which compiles it
# via the seed PEG engine).
(defn h-regex? [form]
(and (struct? form) (= :jolt/tagged (form :jolt/type)) (= :regex (form :tag))))
(defn h-regex-source [form] (form :form))
# #inst / #uuid literals read as tagged forms whose :tag keyword keeps the leading
# # (the data-readers convention; (keyword "#inst") = :#inst). The analyzer lowers
# them to :inst / :uuid IR leaves: the Chez back end emits a runtime inst/uuid
# value; the Janet back end punts to the interpreter (its data-readers parse them).
(defn h-inst? [form]
(and (struct? form) (= :jolt/tagged (form :jolt/type)) (= (keyword "#inst") (form :tag))))
(defn h-inst-source [form] (form :form))
(defn h-uuid? [form]
(and (struct? form) (= :jolt/tagged (form :jolt/type)) (= (keyword "#uuid") (form :tag))))
(defn h-uuid-source [form] (form :form))
(defn h-literal? [form]
(or (nil? form) (boolean? form) (number? form) (string? form)
(keyword? form) (h-char? form)))
# Items of a list/vector as a jolt vector, so the analyzer walks them with Clojure.
(defn h-elements [form] (make-vec form))
(defn h-vector-items [form] (make-vec form))
(defn h-map-pairs [form]
# reader forms carry source order (jolt-p3c) — Clojure evaluates literal
# entries left to right; hash order only for constructed maps
(def order (rdr/form-kv-order form))
(cond
order
(do (def out @[])
(var i 0)
(while (< i (length order))
(array/push out (make-vec [(in order i) (in order (+ i 1))]))
(+= i 2))
(make-vec out))
(phm/phm? form)
(make-vec (map (fn [e] (make-vec [(in e 0) (in e 1)])) (phm/phm-entries form)))
(make-vec (map (fn [k] (make-vec [k (get form k)])) (keys form)))))
(defn h-set-items [form] (make-vec (form :value)))
# ---------------------------------------------------------------------------
# Compile-time environment
# ---------------------------------------------------------------------------
# Names the analyzer must NOT treat as a function call: interpreter special forms
# plus definitional/host macros the compiler doesn't lower. The analyzer handles
# a subset (quote/if/do/def/fn*/let*/loop*/recur/throw/try) and falls back to the
# interpreter for the rest. Kept in sync with evaluator/special-symbol? and
# compiler/uncompilable-heads.
(def- special-names
(let [t @{}]
(each n ["quote" "syntax-quote" "unquote" "unquote-splicing" "do" "if" "def"
"defmacro" "fn*" "let*" "loop*" "recur" "throw" "try" "set!"
# defmulti/defmethod/deftype now compile (macros over *-setup fns).
"eval" "new"
# var-get/var-set/var?/alter-var-root/alter-meta!/reset-meta! are
# plain core fns; find-var/intern are ctx-capturing core fns — all
# compile as ordinary invokes now (Stage 2 tier 6).
"."
# satisfies?/instance?/locking/defonce/read-string/macroexpand-1 and
# the multimethod table ops (prefer-method/remove-method/
# remove-all-methods/get-method/methods) are clojure.core fns /
# overlay macros now (Stage 2 tier 6c) — ordinary invokes.
# create-ns/remove-ns/find-ns/all-ns/the-ns/resolve/ns-resolve/
# ns-aliases/ns-imports/ns-interns/refer are ctx-capturing
# clojure.core fns now (compile as plain invokes — tier 6b), like
# ns/require/in-ns/use/import/refer-clojure before them.
# defprotocol/extend-type/extend-protocol/reify/defrecord now expand to
# plain def + protocol-dispatch/register-method/make-reified/deftype.
"gen-class"
# letfn stays: its let* expansion needs letrec semantics (mutual
# recursion between the fns), which compiled sequential let* lacks.
"monitor-enter" "monitor-exit" "letfn"]
(put t n true))
t))
# Interop-shaped heads the interpreter lowers but the back end doesn't model:
# (.method obj …) / (.-field obj) — member access (name starts with ".")
# (Foo. …) — constructor (name ends with "." )
# Treated as special so the analyzer marks them uncompilable and falls back.
(defn- interop-head? [name]
(def n (length name))
(and (> n 1)
(or (= (string/slice name 0 1) ".")
(= (string/slice name (- n 1)) "."))))
(defn h-special? [name]
(if (or (get special-names name) (interop-head? name)) true false))
# The namespace being compiled. NOT ctx-current-ns directly: the interpreter
# rebinds current-ns to a fn's defining ns while that fn runs, so an interpreted
# analyzer (defined in jolt.analyzer) would otherwise see jolt.analyzer. The back
# end stashes the real compile ns in :compile-ns before invoking the analyzer.
(defn h-current-ns [ctx] (or (get (ctx :env) :compile-ns) (ctx-current-ns ctx)))
(defn h-macro? [ctx sym]
(let [v (resolve-var ctx @{} sym)]
(if (and v (var-macro? v)) true false)))
(defn h-expand-1 [ctx form]
(let [head (in form 0)
v (resolve-var ctx @{} head)
macro-fn (var-get v)]
(apply macro-fn (tuple/slice form 1))))
# Classify a global (non-local) symbol reference:
# {:kind :var :ns NS :name NAME} — a Jolt var (current ns / clojure.core)
# {:kind :host :name NAME} — resolves only via the host env (+, int?, …),
# same fallback the interpreter's resolve-sym uses
# {:kind :unresolved :name NAME} — not yet defined (forward reference)
(defn h-resolve-global [ctx sym]
# Unqualified syms must resolve against the COMPILE ns (h-current-ns), not
# ctx-current-ns: the analyzer runs interpreted in jolt.analyzer, so during
# analysis resolve-var's unqualified branch looks in the wrong namespace.
# Before jolt-2o7.3 the analyzer's lenient fallthrough masked this (it
# emitted a var-ref against compile-ns for everything 'unresolved').
(def v (if (sym :ns)
(resolve-var ctx @{} sym)
(let [cns (ctx-find-ns ctx (h-current-ns ctx))]
(or (and cns (ns-find cns (sym :name)))
(ns-find (ctx-find-ns ctx "clojure.core") (sym :name))))))
(if v
{:kind :var :ns (var-ns v) :name (var-name v)}
(let [nm (sym :name)
entry (in (fiber/getenv (fiber/current)) (symbol nm))]
(if (not (nil? entry))
{:kind :host :name nm}
{:kind :unresolved :name nm}))))
(defn h-intern! [ctx ns-name nm]
(ns-intern (ctx-find-ns ctx ns-name) nm)
nil)
# Open-world (late-binding) analysis: when set on the ctx env, an unresolved
# symbol emits a var-ref against the compile ns (resolved at runtime) instead of
# punting to the interpreter. The Chez back end (host/chez/driver) sets it: it has
# no interpreter to punt to, and a forward reference to a runtime-interned var
# (e.g. defmulti's setup call) must lower to a var-deref. Off everywhere else, so
# the normal compiler keeps its strict 'Unable to resolve symbol' behavior.
(defn h-late-bind? [ctx] (truthy? (get (ctx :env) :late-bind-unresolved?)))
# ---------------------------------------------------------------------------
# Installation: bind these fns as vars in the `jolt.host` namespace so jolt-core
# can call them. Idempotent per context.
# ---------------------------------------------------------------------------
# Form predicates use `form-*` names (not list?/vector?/map?/set?/char?) so the
# analyzer can refer them unqualified without the bootstrap's core-renames
# intercepting them as the value-level predicates.
# Lower a syntax-quote's inner form to construction code (so the analyzer can
# compile it). The portable analyzer calls this and analyzes the result.
(defn h-syntax-quote-lower [ctx inner]
(syntax-quote-lower ctx inner))
# Runtime host primitive: set a key on a mutable reference cell (an atom, the
# watches sub-table, ...). The minimal mutation kernel the overlay can't express
# over core fns — putting nil removes the key (Janet table semantics). Returns the
# table so callers can thread; overlay wrappers return the Clojure-meaningful value.
(defn h-ref-put! [tab key val] (put tab key val) tab)
# Runtime host primitive: mint a fresh tagged host table — the value-layer
# constructor the overlay uses to define host-dispatched values (sorted colls).
# Fields are attached with ref-put!.
(defn h-tagged-table [kw] @{:jolt/type kw})
# Raw field read on a host table, BYPASSING collection semantics. The overlay
# needs this to read its own wrappers' fields: plain (get sorted-coll k) is the
# comparator lookup (it dispatches back into the overlay), so reading :entries
# with it would recurse forever.
(defn h-ref-get [tab key] (get tab key))
# Absolute source offset of a list FORM (jolt-fqy), or nil. The analyzer stamps
# it onto :invoke nodes so the success checker can report file:line:col.
(defn h-form-position [form] (rdr/form-pos form))
# ---------------------------------------------------------------------------
# Inline registry (jolt-87f, Route 1 AOT escape analysis). The inline pass
# (jolt.passes) is portable Clojure and can't read Janet var cells, so it asks
# the host whether a given global is inline-eligible and, if so, for its body IR.
# ---------------------------------------------------------------------------
# Is inlining enabled for the unit currently being compiled? :inline? is OFF for
# all of init (core tiers + self-hosted compiler recompile) and flipped on at the
# end of init to the user direct-linking setting, so core compiles exactly as
# before (const-fold only) and only opted-in user code inlines.
(defn h-inline-enabled? [ctx] (if (get (ctx :env) :inline?) true false))
# The stashed inline body for global ns/name, or nil. Returns it only when the
# target is inline-SAFE: a defined var that won't be redefined (not ^:redef /
# ^:dynamic) and that carries a stashed :inline-ir (set by the back end when the
# var's defn compiled as a small, side-effect-bounded fn). The stash is
# {:params [name ...] :body <ir>} — a single fixed arity.
(defn h-inline-ir [ctx ns-name nm]
(when (get (ctx :env) :inline?)
(def cell (let [n (ctx-find-ns ctx ns-name)] (and n (ns-find n nm))))
(when (and cell (table? cell)
(not (cell :dynamic))
(not (let [m (cell :meta)] (and m (get m :redef)))))
(cell :inline-ir))))
# Is `name` (a bare type-name string, e.g. "Vec3") a defrecord/deftype? Both
# expand to define a ->Name positional constructor var (30-macros.clj), so its
# presence is the marker. Lets the analyzer resolve a ^Record type hint to the
# struct fast path: record instances are tables tagged :jolt/deftype (NOT
# :jolt/type), so a raw keyword get is correct for them (jolt-94n).
(defn h-record-ctor-key [ctx name]
# The home ctor key ("ns/->Name") for a ^Type hint, resolving cross-ns
# (referred/aliased) records too — nil if `name` is not a known record type.
(record-hint-ctor-key ctx name))
(defn h-record-type? [ctx name]
# A ^Type hint names a record iff it resolves to a ctor key (local OR cross-ns,
# via record-hint-ctor-key — so a referred/aliased foreign record is recognized,
# not just one whose ->Name happens to be interned in the current ns).
(if (record-hint-ctor-key ctx name) true false))
# The record-ctor shape registry ("ns/->Name" -> {:fields (:k ..) :type tag ..}),
# or an empty table. Lets scalar-replace (jolt-15jq) recognize a (->Rec ..) call
# and map its positional args to the declared field order — the record analogue
# of the inline map-literal keys the map fold already sees in the IR.
(defn h-record-shapes [ctx]
(or (get (ctx :env) :record-shapes) @{}))
# Reader form CONSTRUCTORS (Chez Phase 3, jolt-cf1q.4). The portable Clojure
# reader (jolt.reader) holds all the LEXING/PARSING logic but must DELEGATE form
# construction to the host — a Clojure source file cannot write a {:jolt/type
# :symbol} literal (it parses as a tagged reader form, CLAUDE.md), and the
# concrete representation (Janet struct/array/tuple here; something else on Chez)
# is the host's to own. Same split the analyzer already uses for form-* readers.
(defn h-make-symbol [name] (rdr/make-symbol name))
(defn h-make-char [code] (make-char code))
(defn h-char-from-name [name] (char-from-name name))
# Parse a numeric literal substring to a number. The reader keeps the radix/ratio/
# sign/exponent LOGIC portable; only the final string->number bottoms out here
# (Janet scan-number; Chez string->number). Returns nil on a non-number.
(defn h-scan-number [str] (scan-number str))
# Collection form constructors. The portable reader accumulates items in a jolt
# vector and hands it here; the host builds its native form representation.
(defn- jvec->array [v]
(cond
(pv/pvec? v) (pv/pv->array v)
(indexed? v) (array ;v)
(array)))
(defn h-make-list [items] (jvec->array items)) # list form = Janet array
(defn h-make-vector [items] (tuple/slice (jvec->array items))) # vector form = Janet tuple
(defn h-make-map [kvs] (rdr/reader-map (jvec->array kvs))) # map form (+ kv-order)
# Attach/merge reader metadata onto a symbol FORM (^hint sym): keeps it a bare
# symbol carrying :meta, so type hints are transparent in every position.
(defn h-sym-merge-meta [sym m]
(struct ;(kvs sym) :meta (merge (or (sym :meta) {}) m)))
(defn h-make-set [items] # set form
{:jolt/type :jolt/set :value (tuple/slice (jvec->array items))})
(defn h-make-tagged [tag form] # tagged form (#inst/#uuid/#regex/#foo)
{:jolt/type :jolt/tagged :tag tag :form form})
# A fresh unique name string for #() auto-gensym params (the reader appends '#').
(defn h-gensym-name [] (string (gensym)))
(def- exports
{"form-sym?" h-sym? "form-sym-name" h-sym-name "form-sym-ns" h-sym-ns
"form-make-symbol" h-make-symbol "form-make-char" h-make-char
"form-char-from-name" h-char-from-name "form-scan-number" h-scan-number
"form-make-list" h-make-list "form-make-vector" h-make-vector
"form-make-map" h-make-map "form-sym-merge-meta" h-sym-merge-meta
"form-make-set" h-make-set "form-make-tagged" h-make-tagged
"form-gensym-name" h-gensym-name
"ref-put!" h-ref-put!
"ref-get" h-ref-get
"tagged-table" h-tagged-table
"form-sym-meta" h-sym-meta
"form-list?" h-list? "form-vec?" h-vector? "form-map?" h-map?
"form-set?" h-set? "form-char?" h-char? "form-char-code" h-char-code "form-literal?" h-literal?
"form-regex?" h-regex? "form-regex-source" h-regex-source
"form-inst?" h-inst? "form-inst-source" h-inst-source
"form-uuid?" h-uuid? "form-uuid-source" h-uuid-source
"form-elements" h-elements "form-vec-items" h-vector-items
"form-map-pairs" h-map-pairs "form-set-items" h-set-items
"form-special?" h-special? "compile-ns" h-current-ns "form-macro?" h-macro?
"form-expand-1" h-expand-1 "resolve-global" h-resolve-global
"form-syntax-quote-lower" h-syntax-quote-lower
"host-intern!" h-intern!
"inline-enabled?" h-inline-enabled? "inline-ir" h-inline-ir
"record-type?" h-record-type? "form-position" h-form-position
"record-ctor-key" h-record-ctor-key "record-shapes" h-record-shapes
"late-bind?" h-late-bind?})
(defn install! [ctx]
(def ns (ctx-find-ns ctx "jolt.host"))
(eachp [nm f] exports (ns-intern ns nm f))
ns)

View file

@ -1,19 +0,0 @@
# Host interop aggregator (jolt-jx5l, phase 1 subdir split).
#
# The JVM class/method shim surface that portable cljc libraries call now lives in
# src/jolt/interop/ split by area: java_base (java.lang statics + java.time +
# shared coercion helpers), host_io (java.io/util/net/sql/text), collections
# (the late-bound .iterator/.nth/... hooks over jolt values). The registry
# MACHINERY (class-statics / tagged-methods / register-*!) still lives in the
# evaluator, which loads before this module — so the registries exist before any
# install! runs here.
#
# api imports this module (import ./host_interop) to trigger registration at load.
# Adding a JDK-area shim is now a one-file change under interop/ plus a line here.
(use ./interop/java_base)
(use ./interop/host_io)
(use ./interop/collections)
(install!)
(install-io!)
(install-collections!)

View file

@ -1,49 +0,0 @@
# Host interop — collection interop: wires the evaluator's late-bound hooks to
# core's collection ops so Java-style interop (.iterator/.nth/.count/.seq/...)
# works over jolt values. Split from host_interop.janet (jolt-jx5l, phase 1).
# Late-bound because the evaluator loads before core.
(use ../evaluator)
(use ../core)
(use ../types)
(use ../lazyseq)
(use ../pv)
(use ../plist)
(import ../phm)
# Collection interop: wires the evaluator's late-bound hooks to core's
# collection ops so Java-style interop works over jolt values (moved here from
# api — jolt-jx5l). Late-bound because the evaluator loads before core.
(defn install-collections! []
# (.iterator coll) -> a jolt iterator over any seqable (hiccup's iterate!).
(set-coll-realizer! realize-for-iteration)
# clj-targeted libs build collections via JVM statics in their :clj branches;
# malli's entry parser uses these. createOwning takes ownership of an object
# array -> a vector; createWithCheck builds a map and throws on duplicate keys
# (malli catches that), detected by the built map being smaller than the kvs.
(register-class-statics! "LazilyPersistentVector"
@{"createOwning" (fn [arr] (make-vec arr))})
(register-class-statics! "PersistentArrayMap"
@{"createWithCheck"
(fn [arr]
(def m (phm/make-phm arr))
(if (= (* 2 (phm/phm-count m)) (length arr)) m
(error "PersistentArrayMap: duplicate key")))})
# .nth/.count/.valAt/.get/.seq/.containsKey on a jolt collection -> the
# clojure.core equivalent. :jolt/ci-none means "not a collection method here".
(set-coll-interop!
(fn [target name args]
(if-not (or (pvec? target) (phm/phm? target) (plist? target) (lazy-seq? target)
(and (table? target) (= :jolt/set (get target :jolt/type)))
(shape-rec? target) # map-as-tuple record
(and (struct? target) (nil? (get target :jolt/type)))) # plain map literal
:jolt/ci-none
(cond
(= name "nth") (if (>= (length args) 2) (core-nth target (in args 0) (in args 1))
(core-nth target (in args 0)))
(= name "count") (core-count target)
(or (= name "valAt") (= name "get"))
(if (>= (length args) 2) (core-get target (in args 0) (in args 1))
(core-get target (in args 0)))
(= name "seq") (core-seq target)
(= name "containsKey") (core-contains? target (in args 0))
:jolt/ci-none)))))

View file

@ -1,537 +0,0 @@
# Host interop — java.io / java.util / java.net / java.sql / java.text shims that
# carry portable cljc libraries (Selmer's char-by-char reader, ring-codec,
# migratus, yogthos/config). Split from host_interop.janet (jolt-jx5l, phase 1).
# install-io! registers them; depends on java_base for the shared chr/pad2
# coercion helpers and the java.time `formatter` (SimpleDateFormat et al.).
(use ../evaluator)
(use ../regex)
(use ../core)
(use ../pv)
(use ../plist)
(use ../types)
(use ../lazyseq)
(import ../phm)
(use ./java_base)
# --- java.io / java.lang shims (Selmer's template reader) ---------------------
(defn- string-reader [src]
# :close makes with-open's __close happy (it calls (get x :close) when
# present); :read-line-fn matches the 50-io reader convention so line-seq
# works over readers io/reader hands back
(def self @{:jolt/type :jolt/jio-string-reader :s (string src) :pos 0
:close (fn [] nil)})
(put self :read-line-fn
(fn []
(def {:s s :pos pos} self)
(when (< pos (length s))
(def i (string/find "\n" s pos))
(if (nil? i)
(do (put self :pos (length s)) (string/slice s pos))
(do (put self :pos (+ i 1)) (string/slice s pos i))))))
self)
(defn- string-builder [&opt init]
# a numeric arg is a CAPACITY (java.lang.StringBuilder(int)), not content
@{:jolt/type :jolt/string-builder
:buf (cond (nil? init) @"" (number? init) (buffer/new init) (buffer init))})
(defn make-string-writer []
# :close lets with-open close the writer (core-close-resource calls :close);
# it's a no-op so .toString after with-open still sees the buffer.
@{:jolt/type :jolt/writer :buf @"" :sink nil :close (fn [] nil)})
(defn make-out-writer []
@{:jolt/type :jolt/writer :buf nil :sink prin})
(defn- render-piece [x]
(cond
(nil? x) "null"
(and (struct? x) (= :jolt/char (get x :jolt/type))) (string/from-bytes (x :ch))
(string x)))
# Writer.write(int) writes the CHAR for that code (unlike StringBuilder.append(int),
# which appends the int's digits). jolt chars are bytes, so this round-trips UTF-8
# byte-for-byte with readLine.
(defn- writer-piece [x]
(if (number? x) (string/from-bytes (math/trunc x)) (render-piece x)))
# Read one unit from any reader-ish value: our shims dispatch through their
# tagged "read"; a janet core/file reads one byte. -1 at EOF.
(defn- read-unit [r]
(cond
(and (or (table? r) (struct? r)) (get r :jolt/type))
(((get tagged-methods (r :jolt/type)) "read") r)
(= :core/file (type r))
(let [b (file/read r 1)] (if (or (nil? b) (= 0 (length b))) -1 (b 0)))
(error (string "not a reader: " (type r)))))
(defn- pushback-reader [rdr]
# java.io.PushbackReader: read delegates to the wrapped reader unless
# something was unread; unread takes a char (or char code) and pushes it back
(def self @{:jolt/type :jolt/pushback-reader :rdr rdr :pushed @[]
:close (fn [] nil)})
self)
(defn install-io! []
(register-tagged-methods! :jolt/pushback-reader
@{"read" (fn [self]
(if (> (length (self :pushed)) 0)
(array/pop (self :pushed))
(read-unit (self :rdr))))
"unread" (fn [self ch]
(array/push (self :pushed)
(if (number? ch) ch (get ch :ch)))
nil)
"close" (fn [self] nil)})
(register-tagged-methods! :jolt/jio-string-reader
@{"read" (fn [self]
(if (>= (self :pos) (length (self :s)))
-1
(let [b ((self :s) (self :pos))]
(put self :pos (+ 1 (self :pos)))
b)))
"readLine" (fn [self] ((self :read-line-fn)))
"mark" (fn [self &opt limit] (put self :marked (self :pos)) nil)
"reset" (fn [self] (put self :pos (or (self :marked) 0)) nil)
"skip" (fn [self n] (put self :pos (min (length (self :s)) (+ (self :pos) n))) n)
"close" (fn [self] nil)})
# java.io.Writer / StringWriter — the print-method protocol surface
# (jolt-g1r). A writer either pushes to a sink fn (stdout/custom) or
# accumulates in a buffer (StringWriter). write/append coerce chars the
# same way StringBuilder does.
(register-tagged-methods! :jolt/writer
@{"write" (fn [self x]
(if (self :sink)
((self :sink) (writer-piece x))
(buffer/push-string (self :buf) (writer-piece x)))
nil)
"append" (fn [self x]
(if (self :sink)
((self :sink) (render-piece x))
(buffer/push-string (self :buf) (render-piece x)))
self)
"flush" (fn [self] nil)
"close" (fn [self] nil)
"toString" (fn [self] (string (or (self :buf) "")))})
(register-tagged-methods! :jolt/string-builder
@{"append" (fn [self x] (buffer/push (self :buf) (render-piece x)) self)
"toString" (fn [self] (string (self :buf)))
"length" (fn [self] (length (self :buf)))
"setLength" (fn [self n]
(def buf (self :buf))
(if (< n (length buf))
(buffer/popn buf (- (length buf) n))
(while (< (length buf) n) (buffer/push buf "\0")))
nil)
"charAt" (fn [self i] {:jolt/type :jolt/char :ch ((self :buf) i)})})
(each nm ["File" "java.io.File"]
(register-class-statics! nm @{"separator" "/" "separatorChar" {:jolt/type :jolt/char :ch 47}}))
(register-class-statics! "Boolean"
@{"parseBoolean" (fn [s] (= "true" (string/ascii-lower (string s))))
"TRUE" true "FALSE" false})
(register-class-statics! "Class"
@{"forName" (fn [nm] @{:jolt/type :jolt/class :name nm})})
(each nm ["StringReader" "java.io.StringReader"]
(register-class-ctor! nm string-reader))
(each nm ["StringBuilder" "java.lang.StringBuilder"]
(register-class-ctor! nm string-builder))
(each nm ["StringWriter" "java.io.StringWriter"]
(register-class-ctor! nm make-string-writer))
# --- java.net / java.util surface for ring-codec (ring-core enablement) ---
# URLEncoder/URLDecoder: www-form-urlencoded (space <-> +, %XX bytes,
# [A-Za-z0-9.*_-] literal). Charset args are accepted and ignored beyond
# the name (everything is UTF-8 bytes here).
(defn- url-unreserved? [b]
(or (and (>= b 48) (<= b 57)) (and (>= b 65) (<= b 90))
(and (>= b 97) (<= b 122)) (= b 46) (= b 42) (= b 95) (= b 45)))
(defn- url-encode-www [s & _]
(def out @"")
(each b (string/bytes (string s))
(cond
(url-unreserved? b) (buffer/push-byte out b)
(= b 32) (buffer/push-string out "+")
(buffer/push-string out (string/format "%%%02X" b))))
(string out))
(defn- hexv [b]
(cond (and (>= b 48) (<= b 57)) (- b 48)
(and (>= b 65) (<= b 70)) (- b 55)
(and (>= b 97) (<= b 102)) (- b 87)
(error "URLDecoder: malformed escape")))
(defn- url-decode-www [s & _]
(def bytes (string/bytes (string s)))
(def n (length bytes))
(def out @"")
(var i 0)
(while (< i n)
(def b (in bytes i))
(cond
(= b 43) (do (buffer/push-string out " ") (++ i))
(= b 37) (if (< (+ i 2) n)
(do (buffer/push-byte out (+ (* 16 (hexv (in bytes (+ i 1)))) (hexv (in bytes (+ i 2)))))
(+= i 3))
(error "URLDecoder: incomplete escape"))
(do (buffer/push-byte out b) (++ i))))
(string out))
(each nm ["URLEncoder" "java.net.URLEncoder"]
(register-class-statics! nm @{"encode" url-encode-www}))
(each nm ["URLDecoder" "java.net.URLDecoder"]
(register-class-statics! nm @{"decode" url-decode-www}))
(each nm ["Charset" "java.nio.charset.Charset"]
(register-class-statics! nm @{"forName" (fn [nm*] @{:jolt/type :jolt/charset :name nm*})}))
# Base64 (RFC 4648): encoder/decoder singletons with encode/decode methods.
(def- b64-alphabet "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
(defn- b64-encode [bs]
(def bytes (if (bytes? bs) bs (string bs)))
(def n (length bytes))
(def out @"")
(var i 0)
(while (< i n)
(def b0 (in bytes i))
(def b1 (if (< (+ i 1) n) (in bytes (+ i 1))))
(def b2 (if (< (+ i 2) n) (in bytes (+ i 2))))
(buffer/push-byte out (in b64-alphabet (brshift b0 2)))
(buffer/push-byte out (in b64-alphabet (bor (blshift (band b0 3) 4) (brshift (or b1 0) 4))))
(buffer/push-string out (if (nil? b1) "=" (string/from-bytes (in b64-alphabet (bor (blshift (band b1 0xf) 2) (brshift (or b2 0) 6))))))
(buffer/push-string out (if (nil? b2) "=" (string/from-bytes (in b64-alphabet (band b2 0x3f)))))
(+= i 3))
(string out))
(def- b64-rev (do (def t @{}) (eachp [i c] b64-alphabet (put t c i)) t))
(defn- b64-decode [s]
(def cleaned (string/replace-all "=" "" (string s)))
(def out @"")
(var acc 0) (var bits 0)
(each c (string/bytes cleaned)
(def v (get b64-rev c))
(when (nil? v) (error "Base64: illegal character"))
(set acc (bor (blshift acc 6) v))
(+= bits 6)
(when (>= bits 8)
(-= bits 8)
(buffer/push-byte out (band (brshift acc bits) 0xff))))
out)
(register-tagged-methods! :jolt/base64-encoder @{"encode" (fn [self bs] (b64-encode bs)) "encodeToString" (fn [self bs] (b64-encode bs))})
(register-tagged-methods! :jolt/base64-decoder @{"decode" (fn [self s] (b64-decode s))})
(each nm ["Base64" "java.util.Base64"]
(register-class-statics! nm
@{"getEncoder" (fn [] @{:jolt/type :jolt/base64-encoder})
"getDecoder" (fn [] @{:jolt/type :jolt/base64-decoder})}))
# Integer statics: valueOf with optional radix. Returns a plain number —
# byteValue/intValue live on the number method surface in the evaluator.
(register-class-statics! "Integer"
@{"valueOf" (fn [x &opt radix]
(cond
(number? x) x
(nil? radix) (or (scan-number (string x)) (error (string "NumberFormatException: " x)))
(= radix 16) (or (scan-number (string "16r" x)) (error (string "NumberFormatException: " x)))
(= radix 8) (or (scan-number (string "8r" x)) (error (string "NumberFormatException: " x)))
(= radix 2) (or (scan-number (string "2r" x)) (error (string "NumberFormatException: " x)))
(error (string "Integer/valueOf: unsupported radix " radix))))
"parseInt" (fn [x &opt radix]
(or (scan-number (string (case radix 16 "16r" 8 "8r" 2 "2r" "") x))
(error (string "NumberFormatException: " x))))
"MAX_VALUE" 2147483647
"MIN_VALUE" -2147483648})
# StringTokenizer: eager split on any delimiter char, empty tokens skipped.
(defn- tokenize [s delims]
(def dset @{})
(each b (string/bytes delims) (put dset b true))
(def toks @[])
(def cur @"")
(each b (string/bytes (string s))
(if (get dset b)
(when (> (length cur) 0) (array/push toks (string cur)) (buffer/clear cur))
(buffer/push-byte cur b)))
(when (> (length cur) 0) (array/push toks (string cur)))
toks)
(register-tagged-methods! :jolt/string-tokenizer
@{"hasMoreTokens" (fn [self] (< (self :pos) (length (self :toks))))
"countTokens" (fn [self] (- (length (self :toks)) (self :pos)))
"nextToken" (fn [self]
(if (< (self :pos) (length (self :toks)))
(let [t (in (self :toks) (self :pos))]
(put self :pos (+ 1 (self :pos))) t)
(error "NoSuchElementException")))})
(each nm ["StringTokenizer" "java.util.StringTokenizer"]
(register-class-ctor! nm (fn [s &opt delims]
@{:jolt/type :jolt/string-tokenizer
:toks (tokenize s (or delims " \t\n\r\f"))
:pos 0})))
# clojure.lang.MapEntry: a 2-tuple, jolt's map-entry representation.
# java.util.HashMap: a mutable wrapper over a janet table, keyed by canonical
# key (so jolt collection keys compare by value). reitit uses it as a fast
# read cache: (HashMap. m) copies a map's entries, (.get hm k) reads.
# raw value-keys: reitit's HashMap keys are strings/keywords/tuples, all of
# which janet tables key by value — no canonicalization needed here.
(defn- hm-entries [m]
(cond (phm/phm? m) (phm/phm-entries m)
(struct? m) (pairs m)
(table? m) (pairs m)
@[]))
(register-tagged-methods! :jolt/hashmap
@{"get" (fn [self k] (get (self :tbl) k))
"put" (fn [self k v] (put (self :tbl) k v) v)
"putAll" (fn [self m] (each pair (hm-entries m) (put (self :tbl) (in pair 0) (in pair 1))) nil)
"containsKey" (fn [self k] (not (nil? (get (self :tbl) k))))
"size" (fn [self] (length (self :tbl)))})
(each nm ["HashMap" "java.util.HashMap"]
# java.util.HashMap constructors: (), (initialCapacity), (initialCapacity,
# loadFactor) all start empty; (Map m) copies entries. A numeric first arg is
# a capacity (sizing hint we ignore), not content.
(register-class-ctor! nm
(fn [& args]
(def tbl @{})
(def init (if (> (length args) 0) (in args 0) nil))
(when (and init (not (number? init)))
(each pair (hm-entries init) (put tbl (in pair 0) (in pair 1))))
@{:jolt/type :jolt/hashmap :tbl tbl})))
(each nm ["MapEntry" "clojure.lang.MapEntry"]
(register-class-ctor! nm (fn [k v] [k v])))
# (String. bytes) / (String. bytes charset): UTF-8 bytes to string.
(each nm ["String" "java.lang.String"]
(register-class-ctor! nm (fn [x &opt charset] (string x))))
# String statics. valueOf(Object) yields "null" for nil, else toString —
# hiccup's compiler emits (String/valueOf (or arg "")) to stringify attribute
# values; core-str gives jolt's str semantics for keywords/numbers/etc.
(each nm ["String" "java.lang.String"]
(register-class-statics! nm
@{"valueOf" (fn [x &opt off cnt] (if (nil? x) "null" (core-str x)))}))
# java.net.URL: enough for selmer's template cache — file: URLs only.
# A protocol-less spec throws (selmer catches MalformedURLException and
# prepends file:///), and getPath hands back a stat-able filesystem path.
(defn url-path [spec]
(var p (if (string/has-prefix? "file:" spec) (string/slice spec 5) spec))
(while (and (> (length p) 1) (string/has-prefix? "//" p))
(set p (string/slice p 1)))
p)
(register-tagged-methods! :jolt/url
@{"getPath" (fn [self] (url-path (self :spec)))
"getFile" (fn [self] (url-path (self :spec)))
"toString" (fn [self] (self :spec))
"toExternalForm" (fn [self] (self :spec))})
(each nm ["URL" "java.net.URL"]
(register-class-ctor! nm
(fn [spec & _]
(def s (string spec))
(def colon (string/find ":" s))
(if (or (nil? colon) (= colon 0)
(string/find "/" (string/slice s 0 colon)))
(error (string "MalformedURLException: no protocol: " s))
@{:jolt/type :jolt/url :spec s}))))
(each nm ["PushbackReader" "java.io.PushbackReader"]
(register-class-ctor! nm (fn [rdr &opt size] (pushback-reader rdr))))
(each nm ["BigInteger" "java.math.BigInteger"]
(register-class-ctor! nm
(fn [v]
(or (scan-number (string/trim (string v)))
(error (string "NumberFormatException: For input string: \"" v "\""))))))
(each nm ["Locale" "java.util.Locale"]
(register-class-ctor! nm (fn [id &opt _country] @{:jolt/type :jolt/locale :id (string id)})))
# java.util.regex.Pattern statics: Pattern/compile, Pattern/quote, Pattern/MULTILINE.
# Pattern/compile returns jolt's native :jolt/regex compiled value so that
# str/replace, re-matches, .split etc accept it transparently.
(defn- pattern-quote [s]
(def meta "\\.[]{}()*+-?^$|&")
(def buf @"")
(var i 0)
(while (< i (length s))
(def c (s i))
(if (string/find (string/from-bytes c) meta)
(buffer/push buf (chr "\\")))
(buffer/push buf (string/from-bytes c))
(++ i))
(string buf))
(def pattern-multiline 8)
(each nm ["Pattern" "java.util.regex.Pattern"]
(register-class-statics! nm
@{"compile" (fn [s &opt flags]
(if (and flags (= (band flags pattern-multiline) pattern-multiline))
(re-pattern (string "(?m)" s))
(re-pattern s)))
"quote" (fn [s] (pattern-quote s))
"MULTILINE" pattern-multiline}))
# .split on compiled regex values: delegates to re-split, drops trailing empties
(register-tagged-methods! :jolt/regex
@{"split" (fn [self s &opt limit]
(def parts (re-split self s))
(while (and (> (length parts) 0) (= "" (last parts)))
(array/pop parts))
parts)})
# JVM exception constructors: (Exception. msg), (IllegalArgumentException. msg),
# (InterruptedException. msg). Return the message string so getMessage works.
(each nm ["Exception" "java.lang.Exception"]
(register-class-ctor! nm (fn [msg] (string msg))))
(each nm ["IllegalArgumentException" "java.lang.IllegalArgumentException"]
(register-class-ctor! nm (fn [msg] (string msg))))
(each nm ["InterruptedException" "java.lang.InterruptedException"]
(register-class-ctor! nm (fn [msg] (string msg))))
# Character class statics (ASCII — the engine is byte-based).
(register-class-statics! "Character"
@{"isUpperCase" (fn [ch] (and (>= (ch :ch) 65) (<= (ch :ch) 90)))
"isLowerCase" (fn [ch] (and (>= (ch :ch) 97) (<= (ch :ch) 122)))})
# java.net.URI constructor: stores the spec string, rounds-trips through str.
(each nm ["URI" "java.net.URI"]
(register-class-ctor! nm (fn [spec] (string spec))))
# java.util.Date: millis-valued instants; no-arg = now.
(defn- make-date [&opt ms]
@{:jolt/type :jolt/date :ms (or ms (math/floor (* 1000 (os/clock :realtime))))})
(each nm ["Date" "java.util.Date"]
(register-class-ctor! nm make-date))
(register-tagged-methods! :jolt/date
@{"getTime" (fn [self] (self :ms))
"toString" (fn [self] (string (self :ms)))})
# java.util.TimeZone: getTimeZone(id) -> a tz value.
(defn- make-tz [id] @{:jolt/type :jolt/tz :id (string id)})
(register-class-statics! "TimeZone"
@{"getTimeZone" (fn [id] (make-tz id))})
(register-class-statics! "java.util.TimeZone"
@{"getTimeZone" (fn [id] (make-tz id))})
# java.text.SimpleDateFormat: minimal formatter supporting y M d H m s tokens.
# (pad2 is the module-level helper above.)
(defn- sdf-format [pattern ms utc?]
(def d (os/date (math/floor (/ ms 1000)) (not utc?)))
(def out @"")
(var i 0)
(while (< i (length pattern))
(def c (pattern i))
(def k (do (var j i)
(while (and (< j (length pattern)) (= (pattern j) c)) (++ j))
(- j i)))
(cond
(= c (chr "y")) (do (buffer/push out (if (>= k 4) (string (d :year)) (pad2 (mod (d :year) 100)))) (+= i k))
(= c (chr "M")) (do (buffer/push out (if (= k 1) (string (+ 1 (d :month))) (pad2 (+ 1 (d :month))))) (+= i k))
(= c (chr "d")) (do (buffer/push out (if (= k 1) (string (+ 1 (d :month-day))) (pad2 (+ 1 (d :month-day))))) (+= i k))
(= c (chr "H")) (do (buffer/push out (if (= k 1) (string (d :hours)) (pad2 (d :hours)))) (+= i k))
(= c (chr "m")) (do (buffer/push out (if (= k 1) (string (d :minutes)) (pad2 (d :minutes)))) (+= i k))
(= c (chr "s")) (do (buffer/push out (if (= k 1) (string (d :seconds)) (pad2 (d :seconds)))) (+= i k))
(do (buffer/push out (string/from-bytes c)) (++ i))))
(string out))
(defn- make-sdf [pattern]
@{:jolt/type :jolt/sdf :pattern pattern :utc true})
(each nm ["SimpleDateFormat" "java.text.SimpleDateFormat"]
(register-class-ctor! nm make-sdf))
(register-tagged-methods! :jolt/sdf
@{"setTimeZone" (fn [self tz]
(put self :utc (= "UTC" (get tz :id)))
self)
"format" (fn [self date]
(sdf-format (self :pattern) (date :ms) (self :utc)))})
# Thread stub: getContextClassLoader returns a stub so migratus jar/create code
# that walks Thread/currentThread doesn't crash.
(register-tagged-methods! :jolt/thread
@{"getContextClassLoader" (fn [self] @{:jolt/type :jolt/classloader})})
# ClassLoader degrade (jolt-hjw): there is no classpath, so getResource returns
# nil and migratus's find-migration-dir falls through to the filesystem branch
# (resources/<dir>). getSystemClassLoader yields the same stub.
(each nm ["ClassLoader" "java.lang.ClassLoader"]
(register-class-statics! nm @{"getSystemClassLoader" (fn [] @{:jolt/type :jolt/classloader})}))
# No STM on jolt: a transaction is never running, so logging libraries that
# gate agent-vs-direct on it (clojure.tools.logging/log*) always log directly.
(register-class-statics! "clojure.lang.LockingTransaction" @{"isRunning" (fn [] false)})
(register-tagged-methods! :jolt/classloader
@{"getResource" (fn [self path] nil)
"getResources" (fn [self path] nil)
"getResourceAsStream" (fn [self path] nil)})
# next.jdbc host shims (paired with the __jdbc-* builtins in core.janet and the
# instance? Connection case in evaluator.janet). The wrapped connection carries
# a clj :exec callback (run one SQL string) and a :close callback.
# java.sql.Timestamp: migratus builds (Timestamp. millis) for the applied
# column; represent it as the millis number so it stores and sorts directly.
(each nm ["Timestamp" "java.sql.Timestamp"]
(register-class-ctor! nm (fn [ms] ms)))
(register-tagged-methods! :jolt/jdbc-conn
@{"setAutoCommit" (fn [self _] self)
"isClosed" (fn [self] (get (get self :closed) 0))
"close" (fn [self]
(unless (get (get self :closed) 0)
((get self :close))
(put (get self :closed) 0 true))
nil)
"getMetaData" (fn [self] @{:jolt/type :jolt/jdbc-meta :product (get self :product)})})
(register-tagged-methods! :jolt/jdbc-meta
@{"getDatabaseProductName" (fn [self] (get self :product))})
# Statement batch: addBatch accumulates SQL strings, executeBatch runs each via
# the connection's clj :exec callback (which executes inside the transaction).
(register-tagged-methods! :jolt/jdbc-stmt
@{"addBatch" (fn [self sql] (array/push (get self :cmds) sql) nil)
"executeBatch" (fn [self]
(def out @[])
(each c (get self :cmds) (array/push out ((get self :exec) c)))
out)
"close" (fn [self] nil)})
# java.io.File model (jolt-hjw). A :jolt/file carries its path; io/file and the
# File. ctor build it (see core.janet's __make-file). The method surface below
# is backed by os/ and file/. listFiles returns child :jolt/file values so
# file-seq (File-aware) yields :jolt/file leaves migratus can call methods on.
(defn- jfile-path [x]
(if (and (table? x) (= :jolt/file (get x :jolt/type))) (get x :path) (string x)))
(defn- make-jfile [path &opt child]
@{:jolt/type :jolt/file
:path (if child (string (jfile-path path) "/" (jfile-path child)) (jfile-path path))})
(defn- last-slash [p]
(var idx nil) (var i 0)
(while (< i (length p)) (when (= (p i) 47) (set idx i)) (++ i))
idx)
(defn- jfile-name [p]
(if-let [i (last-slash p)] (string/slice p (+ i 1)) p))
(defn- jfile-abs [p]
(if (string/has-prefix? "/" p) p (string (os/cwd) "/" p)))
(each nm ["File" "java.io.File"]
(register-class-ctor! nm make-jfile))
(register-tagged-methods! :jolt/file
@{"getPath" (fn [self] (get self :path))
"toString" (fn [self] (get self :path))
"getName" (fn [self] (jfile-name (get self :path)))
"getParent" (fn [self] (let [p (get self :path)]
(if-let [i (last-slash p)] (string/slice p 0 i) nil)))
"getAbsolutePath" (fn [self] (jfile-abs (get self :path)))
"getCanonicalPath" (fn [self] (jfile-abs (get self :path)))
"getAbsoluteFile" (fn [self] (make-jfile (jfile-abs (get self :path))))
"exists" (fn [self] (not (nil? (os/stat (get self :path)))))
"isFile" (fn [self] (= :file (os/stat (get self :path) :mode)))
"isDirectory" (fn [self] (= :directory (os/stat (get self :path) :mode)))
"canRead" (fn [self] (not (nil? (os/stat (get self :path)))))
# listFiles: child File values, or nil when not a directory (Clojure null)
"listFiles" (fn [self]
(let [p (get self :path)]
(when (= :directory (os/stat p :mode))
(map (fn [e] (make-jfile p e)) (os/dir p)))))
"list" (fn [self]
(let [p (get self :path)]
(when (= :directory (os/stat p :mode)) (os/dir p))))
"toPath" (fn [self] @{:jolt/type :jolt/nio-path :s (get self :path)})
"toURI" (fn [self] (string "file:" (jfile-abs (get self :path))))
"toURL" (fn [self] @{:jolt/type :jolt/url :spec (string "file:" (jfile-abs (get self :path)))})
"delete" (fn [self] (let [r (protect (os/rm (get self :path)))] (truthy? (r 0))))
"mkdir" (fn [self] (truthy? ((protect (os/mkdir (get self :path))) 0)))
"mkdirs" (fn [self] (truthy? ((protect (os/mkdir (get self :path))) 0)))
"createNewFile" (fn [self]
(let [p (get self :path)]
(if (os/stat p) false
(do (def f (file/open p :w)) (file/close f) true))))
"equals" (fn [self o] (and (table? o) (= (get self :path) (get o :path))))
"hashCode" (fn [self] (hash (get self :path)))})
# java.nio.file degrade for migratus's script-excluded? glob check: just enough
# of Path / FileSystem / PathMatcher to match a filename against a glob, with a
# simple recursive * / ? matcher (no path-segment semantics — filenames only).
(defn- glob-matches? [glob s]
(defn m [gi si]
(cond
(= gi (length glob)) (= si (length s))
(= (glob gi) 42) # *
(or (m (+ gi 1) si)
(and (< si (length s)) (m gi (+ si 1))))
(and (< si (length s))
(or (= (glob gi) 63) (= (glob gi) (s si)))) # ? or literal
(m (+ gi 1) (+ si 1))
false))
(m 0 0))
(register-tagged-methods! :jolt/nio-path
@{"getFileSystem" (fn [self] @{:jolt/type :jolt/nio-fs})
"toString" (fn [self] (get self :s))})
(register-tagged-methods! :jolt/nio-fs
@{"getPath" (fn [self s & _] @{:jolt/type :jolt/nio-path :s s})
"getPathMatcher" (fn [self spec]
@{:jolt/type :jolt/nio-matcher
:glob (if (string/has-prefix? "glob:" spec)
(string/slice spec 5) spec)})})
(register-tagged-methods! :jolt/nio-matcher
@{"matches" (fn [self path] (glob-matches? (get self :glob) (get path :s)))}))

View file

@ -1,250 +0,0 @@
# Host interop — java.lang static surfaces (Math/Thread/System/Long) + the
# java.time shim (epoch-ms values + a DateTimeFormatter pattern subset). Split
# from host_interop.janet (jolt-jx5l, phase 1). Everything registers through the
# evaluator's class-statics / tagged-methods registries via install!. Also the
# home of the shared coercion helpers (chr/pad2) that host_io reuses.
(use ../evaluator)
(use ../regex)
(use ../core)
(use ../pv)
(use ../plist)
(use ../types)
(use ../lazyseq)
(import ../phm)
(defn chr [s] (get s 0))
# --- java.lang static surfaces (Math/Thread/System/Long) ----------------------
# Registered through the generic class-statics registry below, same as every
# other class — there is no special-case dispatch (jolt-jx5l).
(def- math-statics
@{"sqrt" math/sqrt "pow" math/pow "floor" math/floor "ceil" math/ceil
"abs" (fn [x] (if (< x 0) (- x) x))
"round" (fn [x] (math/round x))
"sin" math/sin "cos" math/cos "tan" math/tan
"asin" math/asin "acos" math/acos "atan" math/atan
"log" math/log "log10" math/log10 "exp" math/exp
"max" (fn [a b] (if (> a b) a b)) "min" (fn [a b] (if (< a b) a b))
"signum" (fn [x] (cond (< x 0) -1.0 (> x 0) 1.0 0.0))
"PI" math/pi "E" math/e
"random" (fn [&] (math/random))})
# sleep parks the CURRENT thread's event loop — inside a future body that's the
# worker OS thread (ev/spawn-thread gives each worker its own loop), so a
# sleeping future doesn't block the parent.
(def- thread-statics
{"sleep" (fn [ms] (ev/sleep (/ ms 1000)) nil)
"yield" (fn [] (ev/sleep 0) nil)
"interrupted" (fn [] false)
"currentThread" (fn [] @{:jolt/type :jolt/thread :id "main"})})
# wall/monotonic clocks + properties/env (what portable timing/config code uses).
(def- system-statics
# realtime clock (sub-ms float epoch seconds) — os/time is whole seconds,
# which quantized every elapsed-time measurement to 1000ms.
{"currentTimeMillis" (fn [] (math/floor (* 1000 (os/clock :realtime))))
"nanoTime" (fn [] (math/floor (* 1e9 (os/clock :monotonic))))
"getProperty" (fn [k &opt dflt]
(case k
"os.name" (case (os/which)
:windows "Windows" :macos "Mac OS X" "Linux")
"line.separator" "\n"
"file.separator" "/"
"user.dir" (os/cwd)
"user.home" (os/getenv "HOME")
"java.io.tmpdir" (or (os/getenv "TMPDIR") "/tmp")
dflt))
# JOLT_BAKE_ENV_ALLOWLIST (jolt-s3j): during an image bake (jpm build of a
# native executable, set by the project's build.sh) the env snapshot that
# libraries like config.core capture at load gets MARSHALED INTO THE BINARY
# — GitHub push protection once flagged real API tokens inside an example's
# build output. With the var set, System/getenv serves only the listed
# comma-separated names (single-var reads of unlisted names return nil), so
# nothing secret can bake. Unset (the normal runtime case), reads are live
# and unfiltered.
"getenv" (fn [&opt k]
(def allow (os/getenv "JOLT_BAKE_ENV_ALLOWLIST"))
(if (nil? allow)
(if k (os/getenv k) (os/environ))
(let [names (string/split "," allow)
ok @{}]
(each n names (put ok (string/trim n) true))
(if k
(when (get ok k) (os/getenv k))
(let [e (os/environ) out @{}]
(eachp [ek ev] e (when (get ok ek) (put out ek ev)))
out)))))
# the property subset getProperty serves, as an iterable map
"getProperties" (fn []
{"os.name" (case (os/which)
:windows "Windows" :macos "Mac OS X" "Linux")
"line.separator" "\n"
"file.separator" "/"
"user.dir" (os/cwd)
"user.home" (or (os/getenv "HOME") "")
"java.io.tmpdir" (or (os/getenv "TMPDIR") "/tmp")})
# terminate the process with the given status code
"exit" (fn [&opt status] (os/exit (if (nil? status) 0 status)))})
# sentinels portable code compares against. jolt numbers are doubles, so these
# are the f64 approximations.
(def- long-statics
{"MAX_VALUE" 9223372036854775807
"MIN_VALUE" -9223372036854775808
"parseLong" (fn [s &opt radix]
(def n (scan-number (string/trim (string s)) (or radix 10)))
(if (and n (= n (math/floor n)))
n
(error (string "NumberFormatException: For input string: \"" s "\""))))
"valueOf" (fn [s &opt radix]
(def n (scan-number (string/trim (string s)) (or radix 10)))
(if (and n (= n (math/floor n)))
n
(error (string "NumberFormatException: For input string: \"" s "\""))))})
# --- values -------------------------------------------------------------------
(defn- instant [ms] @{:jolt/type :jolt/instant :ms ms})
(defn- zoned [ms zone] @{:jolt/type :jolt/zoned-dt :ms ms :zone zone})
(defn- local-dt [ms] @{:jolt/type :jolt/local-dt :ms ms})
(defn formatter [pattern &opt locale] @{:jolt/type :jolt/dt-formatter :pattern pattern :locale locale})
(def- zone-default @{:jolt/type :jolt/zone-id :id "system"})
# ms of any date-ish shim value (or a :jolt/inst)
(defn- ms-of [d]
(cond
(number? d) d
(and (or (table? d) (struct? d))
(or (= :jolt/inst (get d :jolt/type))
(= :jolt/instant (get d :jolt/type))
(= :jolt/zoned-dt (get d :jolt/type))
(= :jolt/local-dt (get d :jolt/type))))
(get d :ms)
(error (string "not a date value: " (type d)))))
# --- formatting ----------------------------------------------------------------
(def- month-names ["January" "February" "March" "April" "May" "June" "July"
"August" "September" "October" "November" "December"])
(def- day-names ["Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"])
(defn pad2 [n] (if (< n 10) (string "0" n) (string n)))
# Format epoch-ms with a (subset of the) JVM DateTimeFormatter pattern:
# yyyy yy MMMM MMM MM M dd d EEEE EEE HH H hh h mm m ss s a, quoted literals
# with '...'. Unknown letters pass through.
(defn- format-ms [pattern ms]
(def d (os/date (math/floor (/ ms 1000)) true))
(def out @"")
(var i 0)
(def n (length pattern))
(defn run-len [c]
(var j i)
(while (and (< j n) (= (pattern j) c)) (++ j))
(- j i))
(while (< i n)
(def c (pattern i))
(def k (run-len c))
(cond
(= c (chr "'"))
# quoted literal up to the closing quote ('' = literal quote)
(if (and (< (+ i 1) n) (= (pattern (+ i 1)) (chr "'")))
(do (buffer/push out "'") (+= i 2))
(let [close (string/find "'" pattern (+ i 1))]
(buffer/push out (string/slice pattern (+ i 1) close))
(set i (+ close 1))))
(= c (chr "y"))
(do (buffer/push out (if (>= k 4) (string (d :year))
(pad2 (mod (d :year) 100))))
(+= i k))
(= c (chr "M"))
(do (buffer/push out (case k
1 (string (+ 1 (d :month)))
2 (pad2 (+ 1 (d :month)))
3 (string/slice (in month-names (d :month)) 0 3)
(in month-names (d :month))))
(+= i k))
(= c (chr "d"))
(do (buffer/push out (if (= k 1) (string (+ 1 (d :month-day))) (pad2 (+ 1 (d :month-day)))))
(+= i k))
(= c (chr "E"))
(do (buffer/push out (if (>= k 4) (in day-names (d :week-day))
(string/slice (in day-names (d :week-day)) 0 3)))
(+= i k))
(= c (chr "H"))
(do (buffer/push out (if (= k 1) (string (d :hours)) (pad2 (d :hours)))) (+= i k))
(= c (chr "h"))
(let [h12 (let [h (mod (d :hours) 12)] (if (= h 0) 12 h))]
(buffer/push out (if (= k 1) (string h12) (pad2 h12))) (+= i k))
(= c (chr "m"))
(do (buffer/push out (if (= k 1) (string (d :minutes)) (pad2 (d :minutes)))) (+= i k))
(= c (chr "s"))
(do (buffer/push out (if (= k 1) (string (d :seconds)) (pad2 (d :seconds)))) (+= i k))
(= c (chr "a"))
(do (buffer/push out (if (< (d :hours) 12) "AM" "PM")) (+= i k))
(do (buffer/push out (string/from-bytes c)) (++ i))))
(string out))
# Localized FormatStyle approximations (no locale database on this host).
(def- style-patterns
{[:date :short] "M/d/yy" [:date :medium] "MMM d, yyyy"
[:date :long] "MMMM d, yyyy" [:date :full] "EEEE, MMMM d, yyyy"
[:time :short] "h:mm a" [:time :medium] "h:mm:ss a"
[:time :long] "h:mm:ss a" [:time :full] "h:mm:ss a"
[:datetime :short] "M/d/yy, h:mm a"
[:datetime :medium] "MMM d, yyyy, h:mm:ss a"
[:datetime :long] "MMMM d, yyyy, h:mm:ss a"
[:datetime :full] "EEEE, MMMM d, yyyy, h:mm:ss a"})
(defn- style-fmt [kind style]
(formatter (get style-patterns [kind (get style :style)] "yyyy-MM-dd HH:mm:ss")))
# --- registration --------------------------------------------------------------
(defn install! []
# java.lang statics through the generic registry (no resolve-sym special-case).
(register-class-statics! "Math" math-statics)
(register-class-statics! "Thread" thread-statics)
(register-class-statics! "System" system-statics)
(register-class-statics! "Long" long-statics)
(def fs (fn [style] @{:jolt/type :jolt/format-style :style style}))
(register-class-statics! "FormatStyle"
@{"SHORT" (fs :short) "MEDIUM" (fs :medium) "LONG" (fs :long) "FULL" (fs :full)})
(register-class-statics! "DateTimeFormatter"
@{"ofPattern" (fn [p &opt locale] (formatter p locale))
"ISO_LOCAL_DATE" (formatter "yyyy-MM-dd")
"ISO_LOCAL_DATE_TIME" (formatter "yyyy-MM-dd'T'HH:mm:ss")
"ofLocalizedDate" (fn [style] (style-fmt :date style))
"ofLocalizedTime" (fn [style] (style-fmt :time style))
"ofLocalizedDateTime" (fn [style] (style-fmt :datetime style))})
(register-class-statics! "Instant"
@{"ofEpochMilli" (fn [ms] (instant ms))
"now" (fn [] (instant (math/floor (* 1000 (os/clock :realtime)))))})
(register-class-statics! "ZoneId"
@{"systemDefault" (fn [] zone-default)})
(register-class-statics! "LocalDateTime"
@{"ofInstant" (fn [inst zone] (local-dt (ms-of inst)))
"now" (fn [] (local-dt (math/floor (* 1000 (os/clock :realtime)))))})
(let [locale-statics @{"getDefault" (fn [] @{:jolt/type :jolt/locale :id "default"})
"ENGLISH" @{:jolt/type :jolt/locale :id "en"}
"US" @{:jolt/type :jolt/locale :id "en-US"}
"ROOT" @{:jolt/type :jolt/locale :id "root"}}]
(each nm ["Locale" "java.util.Locale"]
(register-class-statics! nm locale-statics)))
(register-tagged-methods! :jolt/instant
@{"atZone" (fn [self zone] (zoned (self :ms) zone))
"toEpochMilli" (fn [self] (self :ms))})
(register-tagged-methods! :jolt/zoned-dt
@{"toLocalDateTime" (fn [self] (local-dt (self :ms)))
"toInstant" (fn [self] (instant (self :ms)))})
(register-tagged-methods! :jolt/local-dt
@{"atZone" (fn [self zone] (zoned (self :ms) zone))})
# a :jolt/inst (#inst — Clojure's java.util.Date) supports the Date methods
# Selmer's fix-date path calls
(register-tagged-methods! :jolt/inst
@{"toInstant" (fn [self] (instant (self :ms)))
"getTime" (fn [self] (self :ms))})
(register-tagged-methods! :jolt/dt-formatter
@{"withLocale" (fn [self locale] (formatter (self :pattern) locale))
"format" (fn [self d] (format-ms (self :pattern) (ms-of d)))}))

View file

@ -1,86 +0,0 @@
# LazySeq — cell-by-cell lazy sequence (Clojure-compatible)
#
# Model: a thunk returns nil (empty) or a [first-val, rest-thunk] pair; each
# step produces one element + a thunk for the rest. Supports self-referencing
# sequences like fib-seq. Self-contained (janet builtins only) — the Clojure
# seq layer (core_coll.janet) and the interpreter build on these primitives.
#
# REP vs API: this file is ONLY the lazy-cell representation (ls-* primitives).
# The Clojure-facing seq ops (first/rest/seq/cons/count dispatch, realization)
# live in core_coll.janet, branching on `:jolt/type :jolt/lazy-seq`.
#
# Extracted from phm.janet (jolt-bvek): a lazy sequence has nothing to do with
# hash maps; both were tagged tables, which is why they shared a file.
(defn lazy-seq?
"Check if x is a LazySeq."
[x]
(and (table? x) (= :jolt/lazy-seq (x :jolt/type))))
(defn make-lazy-seq [thunk]
@{:jolt/type :jolt/lazy-seq :fn thunk :realized false :val nil})
(defn realize-ls
"Force a LazySeq cell. Returns nil (empty) or [first-val, rest-thunk].
If the thunk returns another lazy-seq, recursively realize it.
Uses :jolt/pending sentinel to detect self-referencing cycles."
[ls]
(if (get ls :realized)
(ls :val)
(do
(put ls :val :jolt/pending)
(put ls :realized true)
(let [raw ((ls :fn))
v (if (lazy-seq? raw) (realize-ls raw) raw)]
(put ls :val v)
v))))
(defn ls-first [ls]
(let [cell (realize-ls ls)]
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell))) nil (in cell 0))))
# The memoized rest wrapper for a node whose cell yielded rest-thunk rt.
# EVERY walk must go through this (not a fresh make-lazy-seq) or independent
# walks re-run the shared thunks and side effects duplicate.
(defn ls-rest-cached [ls rt]
(or (get ls :rest-ls)
(let [w (make-lazy-seq rt)]
(put ls :rest-ls w)
w)))
(defn ls-rest [ls]
(let [cell (realize-ls ls)]
(if (or (nil? cell) (= 0 (length cell))) nil
(let [rt (in cell 1)]
(if (nil? rt) nil
# Memoized wrapper (see ls-rest-cached): a fresh table per call gave
# every independent walk its own realization state, so the shared
# rest-thunks re-ran — duplicating side effects (a doall'd seq of
# futures re-spawned them on the deref walk, serializing pmap).
(ls-rest-cached ls rt))))))
(defn ls-seq [ls]
(var result @[])
(var cur ls)
(while (not (nil? cur))
(let [cell (realize-ls cur)]
(if (nil? cell) (break))
(array/push result (in cell 0))
(set cur (ls-rest cur))))
(if (= 0 (length result)) nil result))
(defn ls-count [ls]
(var cnt 0)
(var cur ls)
(while (not (nil? cur))
(let [cell (realize-ls cur)]
(if (nil? cell) (break))
(++ cnt)
(set cur (ls-rest cur))))
cnt)
(defn lazy-cons
"Returns a LazySeq whose first element is x and whose rest is produced by
rest-thunk (a 0-arg function returning nil or a LazySeq)."
[x rest-thunk]
(make-lazy-seq (fn [] @[x rest-thunk])))

View file

@ -1,136 +0,0 @@
# Jolt Loader
# Namespace loading with optional compilation.
# Supports in-memory bytecode caching when :compile? is enabled.
(use ./reader)
(use ./types)
(use ./evaluator)
(import ./backend :as backend)
# Stateful / context-modifying forms always interpret: they mutate the context
# (namespaces, macros, types, multimethods, dynamic vars, …) in ways the compiler
# doesn't model. Kept here so the compile/interpret routing lives in one place,
# used by both load-ns and the public eval-one. Shrinking toward the frozen
# host-coupled set (Stage 2 jolt-eaa): forms move off this list as they gain a
# compile path; syntax-quote already compiles via the analyzer's `handled` set.
(defn- stateful-head? [head-name]
(or (= head-name "defmacro")
(= head-name "set!")
(= head-name ".") (= head-name "new")
(= head-name "eval")))
(defn- form-head-name [form]
(when (array? form)
(let [ff (first form)]
(when (and (struct? ff) (= :symbol (ff :jolt/type))) (ff :name)))))
(defn- eval-toplevel-1
[ctx form]
# Repair point for the interpreted-fn ns swap: a body runs with current-ns
# rebound to its defining ns and restores it on normal return; an UNWINDING
# throw skips those restores (they're plain trailing calls — defer/try per
# call would cost a fiber per frame and blow the C stack on deep recursion).
# So save here, and on error put the entry ns back before re-raising — the
# ctx never leaks a callee's ns across top-level forms.
(def entry-ns (ctx-current-ns ctx))
(defn- run []
(defn try-compile [] (backend/compile-and-eval ctx form))
(if (get (ctx :env) :compile?)
(if (array? form)
# A call/list: compile it unless its head is a stateful special form.
(let [hn (form-head-name form)]
(if (and hn (stateful-head? hn))
(eval-form ctx @{} form)
(try-compile)))
# A bare symbol or vector literal compiles; anything else interprets.
(if (or (and (struct? form) (= :symbol (form :jolt/type))) (tuple? form))
(try-compile)
(eval-form ctx @{} form)))
(eval-form ctx @{} form)))
(try
(run)
([err fib]
(ctx-set-current-ns ctx entry-ns)
# Stash the full trace TEXT at this innermost boundary: janet's
# debug/stacktrace walks the propagation chain (fiber->child, no public
# accessor), so this is the only place the user's compiled frames
# (in _r$ns/fn--N ...) are reachable. Innermost capture wins; the CLI's
# report-error filters + demangles it. (jolt-2o7.1/2)
(when (nil? (get (ctx :env) :error-trace))
(def buf @"")
(with-dyns [:err buf] (debug/stacktrace fib err ""))
(put (ctx :env) :error-trace (string buf)))
# propagate (not error): re-raising with `error` discards the failing
# fiber's stack
(propagate err fib))))
(defn eval-toplevel
"Evaluate one top-level form for ctx, honoring :compile?. Stateful forms always
interpret; otherwise the form runs through the self-hosted compile pipeline
(portable Clojure analyzer -> IR -> Janet back end), which falls back to the
interpreter for forms it can't compile. Only the compile step is guarded —
runtime errors in compiled code propagate (no double-eval, no hidden errors)."
[ctx form]
# Clojure's top-level `do` rule: children are compiled AND evaluated one at
# a time, so a child's runtime effects (defmulti's var intern, requires, …)
# are visible while the NEXT child compiles. Without the split, (do
# (defmulti area …) (area …)) can't analyze — `area` only exists once the
# defmulti has RUN, and unresolved symbols are analysis errors now
# (jolt-2o7.3).
(if (and (array? form) (= "do" (form-head-name form)))
(do
(var res nil)
(each child (array/slice form 1) (set res (eval-toplevel ctx child)))
res)
(eval-toplevel-1 ctx form)))
(defn eval-forms-positioned
"Evaluate parsed [form line] pairs, recording WHERE an error happened: the
innermost failing form's {:file :line} goes to (env :error-pos) and each
file unwound through joins the (env :error-loading) chain — the CLI's
report-error prints 'at file:line' and 'while loading …' from these.
(jolt-2o7.4)"
[ctx pairs file]
(var res nil)
(each [form line] pairs
(try
(set res (eval-toplevel ctx form))
([err fib]
(def env (ctx :env))
(when (nil? (get env :error-pos))
(put env :error-pos {:file file :line line}))
(when (nil? (get env :error-loading)) (put env :error-loading @[]))
(def chain (get env :error-loading))
(when (not= (last chain) file) (array/push chain file))
(propagate err fib))))
res)
(defn load-ns
"Load a Clojure namespace from a .clj file. Per-form routing (compile-or-
interpret, stateful forms interpret) is shared with eval-one via eval-toplevel.
(load-ns ctx filepath) → namespace symbol string"
[ctx filepath]
(def source (slurp filepath))
(when (or (checker-enabled?) (get (ctx :env) :inline?))
(track-positions! true)
(put (ctx :env) :tc-source source)
(put (ctx :env) :tc-file filepath))
(def pairs (parse-all-positioned source filepath))
(var ns-name nil)
(each [form _] pairs
# Extract ns name from the first ns form
(when (and (nil? ns-name)
(array? form)
(> (length form) 0)
(and (struct? (first form))
(= :symbol ((first form) :jolt/type))
(= "ns" ((first form) :name))))
(let [name-form (in form 1)]
(set ns-name (if (struct? name-form) (name-form :name) (string name-form))))))
(when (nil? ns-name)
(error (string "No ns form found in " filepath)))
(eval-forms-positioned ctx pairs filepath)
ns-name)

View file

@ -1,826 +0,0 @@
# Jolt REPL
# Read-eval-print loop for Clojure expressions.
(use ./api)
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./pv)
(use ./plist)
(use ./config)
(use ./reader)
(import ./core :as jcore)
(import ./deps :as deps)
(import ./cgen_build :as cgen-build)
(def jolt-version "0.1.0")
# Compile by default: the shipped runtime runs each form through the self-hosted
# pipeline (portable Clojure analyzer -> IR -> Janet back end) to native bytecode
# (hybrid — forms the analyzer can't compile fall back to the interpreter, so the
# result always matches the interpreter; see backend.janet / loader/eval-toplevel).
# Set JOLT_INTERPRET=1 to force the tree-walking interpreter (debugging / A-B).
(def compile-default? (not (= "1" (os/getenv "JOLT_INTERPRET"))))
# A var, not a def: a -m run may replace it with a forked deps-image ctx that
# already has every dependency compiled (see run-main / the deps image cache).
(var ctx (init {:compile? compile-default?}))
(ctx-set-current-ns ctx "user")
(defn read-line [prompt]
(prin prompt)
(flush)
(let [line (file/read stdin :line)]
(if line (string/trim line) nil)))
# Forward declaration for mutual recursion
(var write-value nil)
(defn- push-str [buf s]
(buffer/push-string buf s))
(defn- write-collection [v buf]
(cond
(pvec? v)
(do
(push-str buf "[")
(let [a (pv->array v) n (pv-count v)]
(var i 0)
(while (< i n)
(write-value (in a i) buf)
(when (< (+ i 1) n) (push-str buf " "))
(++ i)))
(push-str buf "]"))
(plist? v)
(do
(push-str buf "(")
(let [a (pl->array v) n (length a)]
(var i 0)
(while (< i n)
(write-value (in a i) buf)
(when (< (+ i 1) n) (push-str buf " "))
(++ i)))
(push-str buf ")"))
(tuple? v)
(do
(push-str buf "[")
(var i 0)
(let [n (length v)]
(while (< i n)
(write-value (in v i) buf)
(when (< (+ i 1) n) (push-str buf " "))
(++ i)))
(push-str buf "]"))
# LazySeq — realize the cell chain and print as a list. Capped to avoid
# hanging on infinite sequences; prints "..." when truncated.
(and (table? v) (= :jolt/lazy-seq (v :jolt/type)))
(do
(push-str buf "(")
(var cur v)
(var i 0)
(var go true)
(while (and go (< i 1000))
(let [cell (realize-ls cur)]
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell)))
(set go false)
(do
(when (> i 0) (push-str buf " "))
(write-value (in cell 0) buf)
(++ i)
(let [rt (in cell 1)]
(if (nil? rt) (set go false) (set cur (ls-rest-cached cur rt))))))))
(when (and go (>= i 1000)) (push-str buf " ..."))
(push-str buf ")"))
(array? v)
(do
# mutable mode: arrays are vectors -> [] ; immutable: arrays are lists -> ()
(push-str buf (if mutable? "[" "("))
(var i 0)
(let [n (length v)]
(while (< i n)
(write-value (in v i) buf)
(when (< (+ i 1) n) (push-str buf " "))
(++ i)))
(push-str buf (if mutable? "]" ")")))
(and (table? v) (= :jolt/set (v :jolt/type)))
(do
(push-str buf "#{")
(var first? true)
(each k (phs-seq v)
(if first? (set first? false) (push-str buf " "))
(write-value k buf))
(push-str buf "}"))
(and (table? v) (= :jolt/transient (v :jolt/type)))
(push-str buf (string "#<transient " (v :kind) ">"))
(and (table? v) (= :jolt/chan (v :jolt/type)))
(push-str buf "#<channel>")
(phm? v)
(do
(push-str buf "{")
(var first? true)
(each pair (phm-entries v)
(if first? (set first? false) (push-str buf ", "))
(write-value (in pair 0) buf) (push-str buf " ") (write-value (in pair 1) buf))
(push-str buf "}"))
(and (table? v) (= :jolt/regex (v :jolt/type)))
(do (push-str buf "#\"") (push-str buf (v :source)) (push-str buf "\""))
# sorted colls: their comparator-ordered entries, materialized from the
# red-black tree via the value's own :entries op (jolt-0hbr), is all the
# printer reads.
(and (table? v) (= :jolt/sorted-map (v :jolt/type)))
(do
(push-str buf "{")
(var first? true)
(each e (let [ef (let [o (v :ops)] (and o (o :entries))) es (if ef (ef v) (v :entries))] (if (pvec? es) (pv->array es) es))
(if first? (set first? false) (push-str buf ", "))
(write-value (if (pvec? e) (pv-nth e 0) (in e 0)) buf)
(push-str buf " ")
(write-value (if (pvec? e) (pv-nth e 1) (in e 1)) buf))
(push-str buf "}"))
(and (table? v) (= :jolt/sorted-set (v :jolt/type)))
(do
(push-str buf "#{")
(var first? true)
(each x (let [ef (let [o (v :ops)] (and o (o :entries))) es (if ef (ef v) (v :entries))] (if (pvec? es) (pv->array es) es))
(if first? (set first? false) (push-str buf " "))
(write-value x buf))
(push-str buf "}"))
(and (table? v) (get v :jolt/deftype))
(do
(push-str buf "{")
(var first? true)
(each [k val] (pairs v)
(when (and (not= k :jolt/deftype) (not= k :cnt) (not= k :buckets)
(not= k :_meta) (not= k :jolt/type) (not= k :phm))
(if first? (set first? false) (push-str buf " "))
(write-value k buf)
(push-str buf " ")
(write-value val buf)))
(push-str buf "}"))
(struct? v)
(do
(push-str buf "{")
(var first? true)
(each [k val] (pairs v)
(if first? (set first? false) (push-str buf " "))
(write-value k buf)
(push-str buf " ")
(write-value val buf))
(push-str buf "}"))
(table? v)
(do
(push-str buf "{")
(var first? true)
(each [k val] (pairs v)
(when (not= k :jolt/type)
(if first? (set first? false) (push-str buf " "))
(write-value k buf)
(push-str buf " ")
(write-value val buf)))
(push-str buf "}"))))
(set write-value (fn [v buf]
(cond
(nil? v) (push-str buf "nil")
(= true v) (push-str buf "true")
(= false v) (push-str buf "false")
(number? v) (push-str buf (string v))
(string? v) (push-str buf v)
(keyword? v) (do (push-str buf ":") (push-str buf (string v)))
(and (struct? v) (= :jolt/char (get v :jolt/type)))
(do (push-str buf "\\")
(push-str buf (case (v :ch)
10 "newline" 32 "space" 9 "tab" 13 "return"
12 "formfeed" 8 "backspace" 0 "nul"
(string/from-bytes (v :ch)))))
(and (struct? v) (= :symbol (get v :jolt/type)))
(let [ns (get v :ns) name (get v :name)]
(if ns
(push-str buf (string ns "/" name))
(push-str buf name)))
(and (table? v) (= :jolt/var (v :jolt/type)))
(push-str buf (string "#'" (ctx-current-ns ctx) "/" (var-name v)))
(or (tuple? v) (array? v) (struct? v) (table? v))
(write-collection v buf)
true (push-str buf (string v)))))
(defn print-value [v]
(def buf @"")
(write-value v buf)
(print (string buf)))
(defn- err-message [err]
(cond
(string? err) err
(and (or (table? err) (struct? err)) (= :jolt/exception (get err :jolt/type)))
(err-message (get err :value))
(and (or (table? err) (struct? err)) (= :jolt/ex-info (get err :jolt/type)))
(let [m (get err :message) d (get err :data)]
(if (and d (not (empty? d))) (string m " " (string/format "%q" d)) (string m)))
(string? err) err
(string/format "%q" err)))
# --- error presentation (jolt-2o7.2, rephrase-inspired) ----------------------
# Host messages rewritten into Clojure-shaped ones; stack frames filtered to
# the USER'S code (compiled jolt fns carry _r$ns/name--N janet names — round
# 2o7.1). JOLT_DEBUG=1 restores the raw janet trace for jolt development.
(defn- demangle
"_r$app.deep/level3--105 -> app.deep/level3 (nil for non-jolt names)."
[nm]
(when (string/has-prefix? "_r$" nm)
(def s (string/slice nm 3))
# the counter suffix is the LAST --N run
(var cut (length s))
(var i (string/find "--" s))
(while (not (nil? i))
(set cut i)
(set i (string/find "--" s (+ i 1))))
(string/slice s 0 cut)))
(defn- fmt-val [x]
(cond
(string? x) (string `"` x `"`)
(nil? x) "nil"
(string x)))
(def- op-words
{"+" "add" "-" "subtract" "*" "multiply" "/" "divide"
"<" "compare" ">" "compare" "<=" "compare" ">=" "compare"})
(defn- rewrite-message
"Host/janet error text -> a user-facing message. Unknown messages verbatim."
[msg]
(def msg (string msg))
(cond
# janet polymorphic arithmetic. Binary: "could not find method :+ for 1 or
# :r+ for "a"". Unary (inc/dec/-): "could not find method :+ for "x"" — no
# "or :r" clause, so orpos is nil; handle both without crashing the reporter.
(string/has-prefix? "could not find method :" msg)
(let [rest* (string/slice msg (length "could not find method :"))
sp (string/find " " rest*)
op (string/slice rest* 0 sp)
tail (string/slice rest* (+ sp (length " for ")))
orpos (string/find " or :r" tail)]
(if (nil? orpos)
# unary form: one operand
(string "Cannot " (get op-words op op) " " tail
" — " op " expects numbers")
(let [a (string/slice tail 0 orpos)
forpos (string/find " for " tail (+ orpos 1))
b (string/slice tail (+ forpos 5))]
(string "Cannot " (get op-words op op) " " a " and " b
" — " op " expects numbers"))))
# janet fixed-arity: <function _r$ns/f--N> called with 2 arguments, expected 1
(and (string/has-prefix? "<function " msg) (string/find "> called with " msg))
(let [nm-end (string/find ">" msg)
nm (string/slice msg (length "<function ") nm-end)
pretty (or (demangle nm) nm)
tail (string/slice msg (+ nm-end (length "> called with ")))
n-end (string/find " " tail)
n (string/slice tail 0 n-end)
exp (if-let [i (string/find "expected " tail)]
(string " (expected " (string/slice tail (+ i 9)) ")") "")]
(string "Wrong number of args (" n ") passed to: " pretty exp))
# a typo'd symbol compiles to nil today (round 2o7.3 will fix resolution)
(= msg "Cannot call nil as a function")
(string msg " — often an undefined (misspelled?) symbol")
msg))
(defn- print-user-trace
"Filter the stashed janet trace down to frames a jolt user can act on:
compiled jolt fns (the _r$ns/name--N janet names, demangled) and frames
from non-jolt source files. Internal janet/jolt frames are dropped."
[trace]
(var shown 0)
(each line (string/split "\n" trace)
(def t (string/trim line))
(when (string/has-prefix? "in " t)
(def rest* (string/slice t 3))
(def sp (or (string/find " " rest*) (length rest*)))
(def nm (string/slice rest* 0 sp))
(cond
(string/has-prefix? "_r$" nm)
(do (eprint " at " (demangle nm)) (++ shown))
# a frame from a real source file outside jolt internals
(and (string/find "[" rest*)
(not (string/find "src/jolt/" rest*))
(not (string/find "boot.janet" rest*))
(not (string/find "[eval]" rest*)))
(do (eprint " " t) (++ shown))
nil)))
shown)
(defn- report-error [err fib]
(eprint "Error: " (rewrite-message (err-message err)))
(def env (ctx :env))
(def stashed (get env :error-trace))
(def pos (get env :error-pos))
(def chain (get env :error-loading))
(put env :error-trace nil)
(put env :error-pos nil)
(put env :error-loading nil)
# <eval>:1 is the synthetic require/apply string the CLI feeds itself (and
# any one-line -e) — no information there
(when (and pos (not (and (= (pos :file) "<eval>") (= (pos :line) 1))))
(eprint " at " (pos :file) ":" (pos :line)))
(cond
(os/getenv "JOLT_DEBUG")
(if stashed (eprin stashed) (when fib (debug/stacktrace fib "")))
stashed (print-user-trace stashed)
fib (when fib nil))
# requires unwound through, innermost first; the failing file itself is
# already on the 'at' line
(when chain
(each f chain
(unless (or (= f "<eval>") (and pos (= f (pos :file))))
(eprint " while loading " f)))))
(defn- run-repl []
(print "Jolt — Clojure on Janet")
(print "Type (exit) to quit.\n")
(var running true)
(var pending "") # accumulates a form split across multiple input lines
(while running
(let [prompt (if (= pending "") (string (ctx-current-ns ctx) "=> ") " #_=> ")
line (read-line prompt)]
(cond
(nil? line) (set running false)
(let [input (if (= pending "") line (string pending "\n" line))
trimmed (string/trim input)]
(cond
(= trimmed "(exit)") (set running false)
(= trimmed "") (set pending "")
# Try to parse the accumulated input; if it's an incomplete form
# (unterminated list/vector/map/string), keep reading more lines.
(let [parsed (protect (parse-string input))]
(if (and (= (parsed 0) false)
(string/find "nterminated" (string (parsed 1))))
(set pending input)
(do
(set pending "")
(try
(print-value (eval-string ctx input))
([err fib] (report-error err fib))))))))))))
(defn- set-command-line-args [argv]
# bind clojure.core/*command-line-args* to a vector of the remaining args
(ns-intern (ctx-find-ns ctx "clojure.core") "*command-line-args*"
(tuple/slice (tuple ;argv))))
(defn- run-file [path argv]
(set-command-line-args argv)
(ns-intern (ctx-find-ns ctx "clojure.core") "*file*" path)
(if (not (os/stat path))
(do (eprint "Error: file not found: " path) (os/exit 1))
(let [src (slurp path)]
(try
(load-string ctx src path)
([err fib] (report-error err fib) (os/exit 1))))))
(defn- run-eval [expr argv]
(set-command-line-args argv)
(try
(let [v (load-string ctx expr)]
(when (not (nil? v)) (print-value v)))
([err fib] (report-error err fib) (os/exit 1))))
(defn- ensure-nrepl-loaded []
# jolt.nrepl is part of the baked-in stdlib, so require finds it anywhere.
(eval-string ctx "(require '[jolt.nrepl])"))
(defn- run-nrepl [argv]
# addr is [host:]port; bare number is a port. Default 127.0.0.1:7888.
(def addr (get argv 0))
(var host "127.0.0.1")
(var port 7888)
(when addr
(if-let [i (string/find ":" addr)]
(do (when (> i 0) (set host (string/slice addr 0 i)))
(set port (scan-number (string/slice addr (+ i 1)))))
(set port (scan-number addr))))
(ensure-nrepl-loaded)
(eval-string ctx (string "(jolt.nrepl/start-server! {:host \"" host "\" :port " port "})"))
# Editors auto-discover the port from this file (nREPL convention).
(spit ".nrepl-port" (string port))
# Remove .nrepl-port on exit — on a clean unwind (defer) and on Ctrl-C/SIGTERM
# (signal handlers). A hard SIGKILL can't be caught, so it may still be left.
(def cleanup (fn [&] (protect (os/rm ".nrepl-port"))))
(os/sigaction :int (fn [&] (cleanup) (os/exit 0)) true)
(os/sigaction :term (fn [&] (cleanup) (os/exit 0)) true)
(print "Jolt nREPL server started on " host ":" port)
(print "Wrote .nrepl-port — connect your editor; Ctrl-C to stop.")
(flush)
# Keep the main fiber alive so the event loop serves connections.
(defer (cleanup)
(forever (ev/sleep 60))))
(defn- print-version []
(print "jolt v" jolt-version))
# --- Deps image cache: compile the dependencies ONCE -------------------------
# The baked binary loads core in ~10ms (it's marshaled in), but a program still
# re-compiles ALL of its dependency namespaces (reitit, ring, …) from source on
# every run — seconds of analyzer->IR->emit that never change between runs. Like
# Clojure's AOT or Stalin's compile-once model, snapshot the ctx AFTER the
# require chain and reuse it: the first run compiles + caches, later runs fork
# the image (~10ms) and skip compilation entirely. The key includes the jolt
# version, entry ns, source roots, and the compile flags; the image carries a
# manifest of every loaded source file's mtime, so any source edit (app or dep)
# invalidates it. JOLT_NO_DEPS_CACHE=1 disables.
(defn- deps-image-path [ns-name]
(def dir (or (os/getenv "JOLT_IMAGE_CACHE_DIR") (os/getenv "TMPDIR") "/tmp"))
# Key on the jolt version + entry ns + every ctx-shaping env var (ctx-cache-key,
# jolt-q5ql): the run-mode flags this image bakes are all derived from those
# vars, so keying on the canonical list can't miss one the way the old
# hand-built positional key could.
(def key (ctx-cache-key [:jolt-version jolt-version :ns ns-name]))
(string dir "/jolt-deps-" (band (hash key) 0x7FFFFFFF) ".jimg"))
(defn- manifest-of [files]
(def m @{})
(each f files (when-let [st (os/stat f)] (put m f (st :modified))))
m)
(defn- manifest-current? [manifest]
(var ok true)
(eachp [f mt] manifest
(def st (os/stat f))
(unless (and st (= (st :modified) mt)) (set ok false)))
ok)
(defn- try-load-deps-image [path]
(unless (os/getenv "JOLT_NO_DEPS_CACHE")
# load-ctx-image forks + validates + rewires (shared with init-cached, jolt-q5ql);
# the deps-specific validity check is the source-mtime manifest.
(load-ctx-image path jcore/install-print-method-cb!
(fn [c] (let [m (get (c :env) :deps-manifest)]
(and m (manifest-current? m)))))))
(defn- save-deps-image [c path]
(unless (os/getenv "JOLT_NO_DEPS_CACHE")
(put (c :env) :deps-manifest (manifest-of (or (get (c :env) :loaded-files) @[])))
(save-ctx-image c path)))
(defn- run-main [ns-name argv]
(when (nil? ns-name) (eprint "Error: -m/--main requires a namespace") (os/exit 1))
(try
(do
(def path (deps-image-path ns-name))
(if-let [cached (try-load-deps-image path)]
# cache hit: every dependency is already compiled in the image
(do (set ctx cached) (ctx-set-current-ns ctx "user"))
# cache miss: compile the requires, then snapshot for next time. Track the
# loaded files for the manifest.
(do
(put (ctx :env) :loaded-files @[])
(load-string ctx (string "(require '[" ns-name "])"))
# whole-program (jolt-t34): every unit is loaded now — run the one
# closed-world fixpoint over all of them before -main.
(when (get (ctx :env) :whole-program?)
(when-let [ip (get (ctx :env) :infer-program!)] (protect (ip ctx)))
(put (ctx :env) :infer-program-done? true))
(save-deps-image ctx path)))
# Bind *command-line-args* on the FINAL ctx, AFTER any cache swap: a cache
# hit replaces ctx with the saved image, which carries the args baked when
# it was saved — the current run's argv must win (jolt-4mui).
(set-command-line-args argv)
(load-string ctx (string "(apply " ns-name "/-main *command-line-args*)")))
([err fib] (report-error err fib) (os/exit 1))))
# --- uberscript dead-code elimination (jolt-atg) ---------------------------
# A bundle is closed-world: everything it needs is inlined and nothing is
# required afterward, so a user `defn` unreachable from the entry's reference
# graph can be dropped. Conservative + sound: only plain defn/defn- are
# prunable; a defn is kept if its (bare or ns-qualified) name appears in any
# kept form, the closure runs to a fixpoint, and any use of dynamic resolution
# disables pruning entirely. The drop is by exact source span, so formatting
# and reader macros in the surviving code are untouched.
(defn- dce-sym? [x] (and (struct? x) (= :symbol (get x :jolt/type))))
(def- dce-bailout-syms
{"resolve" true "ns-resolve" true "requiring-resolve" true "find-var" true
"intern" true "eval" true "load-string" true})
(defn- dce-collect-syms [form acc]
(cond
(dce-sym? form) (do (put acc (get form :name) true)
(when (get form :ns)
(put acc (string (get form :ns) "/" (get form :name)) true)))
(indexed? form) (each x form (dce-collect-syms x acc))
(or (struct? form) (table? form)) (each k (keys form)
(dce-collect-syms k acc)
(dce-collect-syms (get form k) acc)))
acc)
(defn- dce-defn-name [form]
(when (and (indexed? form) (>= (length form) 2) (dce-sym? (get form 0))
(let [nm (get (in form 0) :name)] (or (= nm "defn") (= nm "defn-")))
(dce-sym? (get form 1)))
(get (in form 1) :name)))
(defn- dce-strip-spans [src dead]
(if (empty? dead) src
(do
(sort dead (fn [a b] (< (a 0) (b 0))))
(def buf @"")
(var cur 0)
(each [s e] dead
(when (> s cur) (buffer/push-string buf (string/slice src cur s)))
(set cur (max cur e)))
(buffer/push-string buf (string/slice src cur))
(string buf))))
(defn- run-uberscript [out main-ns]
# Bundle main-ns and everything it requires (from JOLT_PATH roots) into one
# .clj that runs on a plain jolt — no deps, no jpm. We require the entry and
# collect the load order the loader records (deps before dependents).
(when (or (nil? out) (nil? main-ns))
(eprint "Usage: jolt uberscript OUT.clj -m NS") (os/exit 1))
(put (ctx :env) :loaded-files @[])
(try
(load-string ctx (string "(require '[" main-ns "])"))
([err fib] (report-error err fib) (os/exit 1)))
(def seen @{})
(def files @[])
(each f (get (ctx :env) :loaded-files)
(unless (get seen f) (put seen f true) (array/push files f)))
# read every file's source and parse it into top-level forms with byte spans;
# if ANY file fails to parse, fall back to verbatim bundling (DCE off) so the
# uberscript stays exactly as robust as a plain concatenation.
(def srcs @{})
(def file-forms @{})
(def all-forms @[])
(var dce-ok true)
(each f files
(def src (slurp f))
(put srcs f src)
(def lst @[])
(try
(each [form s e] (parse-all-spans src f)
(def entry @{:start s :end e :dname (dce-defn-name form) :form form})
(array/push lst entry)
(array/push all-forms entry))
([_err _fib] (set dce-ok false)))
(put file-forms f lst))
# disable DCE if the bundle resolves names dynamically (a defn could be
# reached by a string/symbol the reference scan can't see).
(when dce-ok
(def allsyms @{})
(each e all-forms (dce-collect-syms (e :form) allsyms))
(each nm (keys allsyms) (when (get dce-bailout-syms nm) (set dce-ok false))))
# reachability: seed from the entry (-main) and every non-prunable form, then
# close over the bodies of defns that become live.
(def live @{})
(when dce-ok
(def referenced @{"-main" true})
(each e all-forms (unless (e :dname) (dce-collect-syms (e :form) referenced)))
(var changed true)
(while changed
(set changed false)
(each e all-forms
(when (and (e :dname) (not (get live e)) (get referenced (e :dname)))
(put live e true)
(dce-collect-syms (e :form) referenced)
(set changed true)))))
(var dropped 0)
(def buf @"")
(buffer/push-string buf (string ";; Generated by `jolt uberscript` — " (length files) " namespace(s)\n\n"))
(each f files
(buffer/push-string buf (string ";; --- " f " ---\n"))
(def src (get srcs f))
(if-not dce-ok
(buffer/push-string buf src)
(do
(def dead @[])
(each e (get file-forms f)
(when (and (e :dname) (not (get live e)))
(++ dropped)
(array/push dead [(e :start) (e :end)])))
(buffer/push-string buf (dce-strip-spans src dead))))
(buffer/push-string buf "\n"))
(buffer/push-string buf (string "\n(apply " main-ns "/-main *command-line-args*)\n"))
(spit out (string buf))
(print "Wrote " out " (" (length files) " namespace(s)"
(if (and dce-ok (> dropped 0)) (string ", " dropped " dead fn(s) dropped") "") ")"))
(defn- run-cgen-build [argv]
# jolt cgen-build -m NS -o OUT [--keep]: fuse the app's hot numeric-leaf fns
# (compiled to native code) + the app source into one static executable.
(var main-ns nil)
(var out nil)
(var keep false)
(var i 0)
(while (< i (length argv))
(def a (argv i))
(cond
(or (= a "-m") (= a "--main")) (do (set main-ns (get argv (+ i 1))) (+= i 2))
(or (= a "-o") (= a "--out")) (do (set out (get argv (+ i 1))) (+= i 2))
(= a "--keep") (do (set keep true) (++ i))
(++ i)))
(when (or (nil? main-ns) (nil? out))
(eprint "Usage: jolt cgen-build -m NS -o OUT [--keep]")
(os/exit 1))
(try
(let [r (cgen-build/build-app {:main main-ns :out out :keep keep})]
(print "Wrote " (r :out) " (" (r :native-count) " native fn(s) linked)"))
([err fib] (report-error err fib) (os/exit 1))))
(defn- print-help []
(print "Jolt — a Clojure interpreter on Janet\n")
(print "Usage: jolt [opt] [args]\n")
(print " (no args), repl Start a REPL")
(print " FILE [args] Run a Clojure file (binds *command-line-args*, *file*)")
(print " - Run a program read from stdin")
(print " -e, --eval EXPR Evaluate EXPR and print the result")
(print " -f, --file FILE Run a Clojure file")
(print " -m, --main NS [args] Require NS and call its -main with the remaining args")
(print " nrepl-server [addr] Start an nREPL server (addr = [host:]port, default 7888)")
(print " (aliases: --nrepl-server, nrepl)")
(print " uberscript OUT -m NS Bundle NS + its required namespaces into one .clj")
(print " cgen-build -m NS -o OUT Build NS into a single static binary (hot fns")
(print " compiled to native code, no toolchain to run)")
(print " --version, version Print the Jolt version")
(print " -h, --help, help Show this help\n")
(print "Dependencies (deps.edn, git + :local deps — resolved into JOLT_PATH):")
(print " -M:a[:b] [args] Run the alias(es) :main-opts ++ args")
(print " -A:a[:b] CMD [args] Run CMD (repl/-m/nrepl-server/…) with the alias paths")
(print " run FILE [args] Run FILE with deps.edn resolved")
(print " path Print the resolved source roots (':'-joined)")
(print " tasks List :tasks from deps.edn")
(print " task NAME [args] Run a :tasks entry (shell string or :main-opts)")
(print " A deps.edn in the working dir is auto-resolved for repl/-m/-e/nrepl-server/FILE.\n")
(print "Running a program (a file, -m/-M) direct-links by default; type inference")
(print "and specialization are opt-in via JOLT_OPTIMIZE (the cost is paid once, then")
(print "cached). The repl, -e, and nrepl-server stay open so you can redefine vars.")
(print "Environment:")
(print " JOLT_OPTIMIZE=1 type-infer + specialize (whole-program over app nses)")
(print " JOLT_NO_DIRECT_LINK=1 keep a program run open/redefinable (no optimization)")
(print " JOLT_NO_WHOLE_PROGRAM=1 direct-link but skip the cross-namespace pass")
(print " JOLT_DIRECT_LINK=1 force direct-linking + optimization on (e.g. for -e)")
(print " JOLT_WHOLE_PROGRAM=1 force the whole-program pass on")
(print " JOLT_INTERPRET=1 run the tree-walking interpreter\n")
(print " JOLT_PATH=dir1:dir2 extra source roots (set automatically by deps resolution)"))
(def- help-flags {"-h" true "--help" true "help" true "-?" true})
(def- version-flags {"--version" true "version" true})
(def- nrepl-flags {"nrepl-server" true "--nrepl-server" true "nrepl" true})
(def- eval-flags {"-e" true "--eval" true})
(def- file-flags {"-f" true "--file" true})
(def- main-flags {"-m" true "--main" true})
# --- deps.edn integration (the old jolt-deps tool, folded in) ----------------
# The runtime stays deps-agnostic: it reads source roots from JOLT_PATH (applied
# in `main`, below). This layer resolves a deps.edn into those roots IN-PROCESS,
# so a single `jolt` binary both resolves dependencies and runs code. ./deps
# loads jpm (git fetch + cache) lazily — only at an actual resolve — so a run
# with no deps.edn never pulls it in, and an app baked from its own entry (which
# imports jolt/api, not this CLI) never links the resolver at all.
(defn- parse-alias-flag
``"-A:dev:test" / "-M:dev" -> [:dev :test].``
[arg]
(map keyword (filter |(not= "" $) (string/split ":" (string/slice arg 2)))))
(defn- deps-roots [aliases]
(if (os/stat "deps.edn") (deps/resolve-deps-cached "deps.edn" nil aliases) @[]))
(defn- set-deps-env! [aliases]
# Prepend the resolved roots to any existing JOLT_PATH; `main` (below) applies
# them to the ctx source-paths. JOLT_APP_PATHS scopes whole-program inference
# to the project's own namespaces — deps load-infer per-ns instead (jolt-87e).
(def rs (string/join (deps-roots aliases) ":"))
(def existing (os/getenv "JOLT_PATH"))
(os/setenv "JOLT_PATH" (if (and existing (> (length existing) 0)) (string rs ":" existing) rs))
(when (os/stat "deps.edn")
(os/setenv "JOLT_APP_PATHS" (string/join (deps/project-source-roots "deps.edn" aliases) ":"))))
(defn- resolve-deps-argv
``Resolve deps.edn (when relevant) and de-sugar the deps subcommands into a
plain runtime argv that `main` then dispatches on. The pure deps queries
(path/tasks/task) print and exit. A runnable command resolves a deps.edn in
cwd when one is present — so `jolt repl` / `-m` / `nrepl-server` pick up the
project and its dependencies — and is a no-op (resolver untouched) with none.``
[argv]
# leading -A:alias flags apply to whatever command follows
(var aliases nil)
(var rest argv)
(while (string/has-prefix? "-A" (or (get rest 0) ""))
(set aliases (array/concat (or aliases @[]) (parse-alias-flag (get rest 0))))
(set rest (array/slice rest 1)))
(def cmd (get rest 0))
(cond
# pure deps queries — produce output and exit, never start the runtime
(= cmd "path")
(do (print (string/join (deps-roots aliases) ":")) (os/exit 0))
(= cmd "tasks")
(do (each row (deps/tasks "deps.edn")
(print (row 0) (if (row 1) (string "\t" (row 1)) "")))
(os/exit 0))
(= cmd "task")
(let [name (get rest 1)
spec (when name (deps/task-spec "deps.edn" name))]
(cond
(nil? name) (do (eprint "jolt: task needs a name") (os/exit 1))
(nil? spec) (do (eprint "jolt: no such task: " name) (os/exit 1))
(= :shell (spec :type))
(os/exit (os/execute ["sh" "-c" (string/join [(spec :cmd) ;(array/slice rest 2)] " ")] :p))
(do (set-deps-env! aliases)
(array/concat @[] (spec :argv) (array/slice rest 2)))))
# -M:aliases — resolve and run the alias's :main-opts ++ extra args
(and cmd (string/has-prefix? "-M" cmd))
(let [als (array/concat (or aliases @[]) (parse-alias-flag cmd))
mo (deps/alias-main-opts "deps.edn" als)]
(if mo
(do (set-deps-env! als) (array/concat @[] mo (array/slice rest 1)))
(do (eprint "jolt: no :main-opts in alias(es) " (string/format "%j" (map string als)))
(os/exit 1))))
# explicit `run FILE` — resolve, then run the file
(= cmd "run")
(do (set-deps-env! aliases) (array/slice rest 1))
# any runnable command: resolve a deps.edn if present (or if -A forced it);
# help/version never resolve, and with no deps.edn + no -A the resolver and
# jpm are never touched.
(and (not (help-flags cmd)) (not (version-flags cmd))
(or aliases (os/stat "deps.edn")))
(do (set-deps-env! aliases) rest)
# nothing deps-related — argv unchanged
true rest))
(defn main [&]
(def args (or (dyn :args) @[])) # @["jolt" arg1 arg2 ...]
# Resolve deps.edn + de-sugar deps subcommands (jolt-deps folded in): sets
# JOLT_PATH/JOLT_APP_PATHS in our env (read just below) and rewrites argv to a
# plain runtime command. A no-deps invocation passes through untouched.
(def argv (resolve-deps-argv (if (> (length args) 1) (array/slice args 1) @[])))
(ctx-set-current-ns ctx "user")
# JOLT_PATH must be applied at runtime: this `ctx` is built into the image at
# build time, so its source-paths can't capture the runtime environment.
# resolve-deps-argv (above) sets it from the resolved deps.edn source roots.
(when-let [jp (os/getenv "JOLT_PATH")]
(each p (string/split ":" jp)
(when (> (length p) 0) (array/push (get (ctx :env) :source-paths) p))))
# JOLT_APP_PATHS (jolt-87e), likewise applied at runtime: the project's own
# source roots, set by jolt-deps. Whole-program inference is scoped to
# namespaces under these (deps compile at default cost), so a dep-heavy app's
# optimize-mode startup doesn't re-infer every transitive dependency. Read here
# for the same baked-at-build-time reason as JOLT_PATH above.
(let [aps @[]]
(when-let [jap (os/getenv "JOLT_APP_PATHS")]
(each p (string/split ":" jap) (when (> (length p) 0) (array/push aps p))))
(put (ctx :env) :app-source-paths aps))
# JOLT_FEATURES, likewise, must be applied at runtime: reader-features-set!
# runs at module load, which for a baked binary is BUILD time — so a process
# that sets JOLT_FEATURES (e.g. to read a clj-targeted lib's :clj branches)
# would otherwise be ignored, and unmatched #?(...) forms silently splice to
# nothing. Re-read it here so the env wins in the running process.
(when-let [jf (os/getenv "JOLT_FEATURES")]
(reader-features-set! (filter |(> (length $) 0) (string/split "," jf))))
# Linking default depends on the run MODE. Running a PROGRAM (a file, -f, -m/-M,
# stdin) is a closed world — all code is loaded, then it executes to completion
# — so it direct-links by default: user code gets inlining + shapes + the type
# inference's specialization (jolt-87f). INTERACTIVE modes (repl, -e, the nREPL
# server) stay indirect/open so redefinition works — direct-linking would seal
# callers against a redef. Explicit env always wins: JOLT_NO_DIRECT_LINK forces
# the open path even for a program run (runtime redefinition / hot-reload),
# JOLT_DIRECT_LINK forces it on even for -e. Core is already compiled into the
# image; this only governs user code compiled at runtime.
(def open-mode?
(or (empty? argv)
(help-flags (argv 0)) (version-flags (argv 0))
(= (argv 0) "repl") (nrepl-flags (argv 0)) (eval-flags (argv 0))
(= (argv 0) "uberscript")))
(def main-entry? (and (not (empty? argv)) (main-flags (argv 0))))
# Run-mode policy lives in config/resolve-run-mode now (jolt-q5ql) — unit-
# testable without the CLI, and the same canonical knob list backs the cache
# keys. Install the resolved knobs onto the runtime env (the baked ctx computed
# them at build time; a program run recomputes from the live env).
(let [mode (resolve-run-mode open-mode? main-entry?)]
(eachp [k v] mode (put (ctx :env) k v)))
(cond
(empty? argv) (run-repl)
(help-flags (argv 0)) (print-help)
(version-flags (argv 0)) (print-version)
(= (argv 0) "repl") (run-repl)
(nrepl-flags (argv 0)) (run-nrepl (array/slice argv 1))
(eval-flags (argv 0)) (run-eval (get argv 1 "") (array/slice argv 2))
(file-flags (argv 0)) (run-file (get argv 1) (array/slice argv 2))
(main-flags (argv 0)) (run-main (get argv 1) (array/slice argv 2))
(= (argv 0) "uberscript")
(let [out (get argv 1)
rest (array/slice argv 2)
mi (or (index-of "-m" rest) (index-of "--main" rest))]
(run-uberscript out (if mi (get rest (+ mi 1)) nil)))
(= (argv 0) "cgen-build") (run-cgen-build (array/slice argv 1))
(= (argv 0) "-") (run-file "/dev/stdin" (array/slice argv 1))
(run-file (argv 0) (array/slice argv 1))))

View file

@ -1,381 +0,0 @@
# PersistentHashMap for Jolt — a HAMT (hash array mapped trie), the structure
# Clojure/ClojureScript/jank use. 32-way branching, 5 hash bits per level, with
# structural sharing: assoc/dissoc/get are O(log32 n) ~ effectively constant.
# Replaces the old flat copy-on-write bucket array, which was O(n) per assoc
# (O(n^2) to build — jolt-684u). Translated from the ClojureScript
# PersistentHashMap (cljs.core: BitmapIndexedNode / ArrayNode / HashCollisionNode).
#
# REP vs API: this file is ONLY the map representation (phm-* primitives). The
# Clojure-facing map ops (assoc/dissoc/get/conj/count/seq dispatch, nil-key,
# merge) live in core_coll.janet / core_types.janet, which recognize phm by its
# `:jolt/deftype` string and call these primitives. PersistentHashSet is layered
# on top in phs.janet. Transients are a separate mutable-table rep (core_types),
# so they don't touch this file.
#
# Node representation (Janet tuples, tagged at index 0; their arrays are built
# fresh on every modify and never mutated in place, so sharing is safe):
# [:bin bitmap arr] bitmap-indexed: arr is [k v k v ...]; a nil k means v is
# a sub-node (the slot recurses one level deeper).
# [:an cnt arr] array-node: arr is 32 slots of sub-node-or-nil.
# [:hcn hash cnt arr] hash-collision: arr is [k v k v ...] of same-hash keys.
# The map itself stays a table tagged :jolt/phm with :cnt (read directly by
# core), plus :root (the trie, nil when empty) and :has-nil/:nil-val (Clojure
# maps allow a nil key, which the trie can't store because a nil k marks a
# sub-node).
#
# Note on Janet `=`: it is STRUCTURAL on tuples, not identity, so cljs's
# `(identical? n node)` "nothing changed" early-outs are dropped here — we just
# rebuild the O(log32 n) path (an extra alloc only when overwriting an identical
# value). nil returns (a node that became empty) are kept; those are real nils.
(def- DEFTYPE "jolt.lang.persistent-hash-map.PersistentHashMap")
(defn phm? [x]
(and (table? x) (= DEFTYPE (x :jolt/deftype))))
# Keys are hashed and compared by VALUE. Scalars are value-hashable in Janet;
# collection keys (a phm/pvec/plist/vector) are Janet tables hashed by identity,
# so they're canonicalized to a value-hashable struct/tuple first.
# `canonicalize-key` is injected by core (which knows those types); phm stays
# dependency-free. Keys are STORED as-is, so retrieval/iteration return originals.
(var canonicalize-key nil)
(defn set-canonicalize-key!
"Install the value-canonicalizer for collection keys (called by core)."
[f]
(set canonicalize-key f))
(defn- ck [k]
(if (and canonicalize-key (or (table? k) (struct? k) (array? k) (tuple? k)))
(canonicalize-key k)
k))
(defn canon
"Public canonicalizer: maps a key to its value-hashable form (identity for
scalars). Used by callers that index the same canonicalized tables phm uses."
[k] (ck k))
# Identity/scalar equality first (the common case), then value equality.
(defn- key= [a b] (or (= a b) (= (ck a) (ck b))))
# Janet bit ops are 32-bit and split awkwardly: band/bor/bxor want SIGNED
# operands, brushift wants UNSIGNED — and `hash` is a signed 32-bit int. So we
# carry the hash as an UNSIGNED 32-bit value, extract the 5-bit level index with
# arithmetic (mask), and test/count bits with band against (1<<i) only — never
# brushift on a possibly-negative bitmap.
(defn- khash [k] (let [h (hash (ck k))] (if (< h 0) (+ h 0x100000000) h)))
# --- HAMT node machinery (translated from cljs.core) ------------------------
(def- EMPTY-BIN [:bin 0 (tuple)])
(defn- mask [h shift] (% (math/floor (/ h (blshift 1 shift))) 32))
(defn- bitpos [h shift] (blshift 1 (mask h shift)))
# popcount of a 32-bit bitmap; bidx = popcount of the bits below level index m.
(defn- popcount [bm]
(var c 0) (var i 0)
(while (< i 32) (when (not= 0 (band bm (blshift 1 i))) (++ c)) (++ i))
c)
(defn- bidx [bitmap m]
(var c 0) (var i 0)
(while (< i m) (when (not= 0 (band bitmap (blshift 1 i))) (++ c)) (++ i))
c)
(defn- cset1 [arr i a] (def n (array ;arr)) (put n i a) n)
(defn- cset2 [arr i a j b] (def n (array ;arr)) (put n i a) (put n j b) n)
# remove the pair at pair-index p (elements 2p, 2p+1)
(defn- remove-pair [arr p]
(def out (array)) (def a (* 2 p)) (def b (+ a 1)) (def L (length arr))
(var x 0) (while (< x L) (unless (or (= x a) (= x b)) (array/push out (in arr x))) (++ x))
out)
# element-index of key k in a collision arr [k v k v ...], or -1
(defn- hcn-find [arr cnt k]
(var i 0) (def L (* 2 cnt)) (var r -1)
(while (< i L) (when (key= k (in arr i)) (set r i) (break)) (+= i 2))
r)
# rebuild a bin-node from an array-node's slots when it shrinks (<=8) — each
# surviving sub-node becomes a nil-key slot.
(defn- pack-array-node [arr removed-idx]
(def out (array)) (var bitmap 0) (var i 0)
(while (< i 32)
(when (and (not= i removed-idx) (not (nil? (in arr i))))
(array/push out nil) (array/push out (in arr i))
(set bitmap (bor bitmap (blshift 1 i))))
(++ i))
[:bin bitmap out])
# mutual recursion across node types
(var node-assoc nil) (var node-lookup nil) (var node-without nil) (var create-node nil)
(defn- bin-assoc [node shift h key val added]
(def bitmap (in node 1)) (def arr (in node 2))
(def m (mask h shift)) (def bit (blshift 1 m))
(def idx (bidx bitmap m))
(if (zero? (band bitmap bit))
(let [nkeys (popcount bitmap)]
(if (>= nkeys 16)
# expand to an array-node
(let [nodes (array/new-filled 32 nil)
jdx (mask h shift)]
(put nodes jdx (bin-assoc EMPTY-BIN (+ shift 5) h key val added))
(var i 0) (var j 0)
(while (< i 32)
(unless (zero? (band bitmap (blshift 1 i)))
(let [ek (in arr (* 2 j)) ev (in arr (+ 1 (* 2 j)))]
(put nodes i (if (nil? ek) ev
(bin-assoc EMPTY-BIN (+ shift 5) (khash ek) ek ev added)))
(++ j)))
(++ i))
[:an (+ nkeys 1) nodes])
# insert (key val) into the bin arr at pair-index idx
(let [out (array)]
(array/concat out (array/slice arr 0 (* 2 idx)))
(array/push out key) (array/push out val)
(array/concat out (array/slice arr (* 2 idx)))
(put added 0 true)
[:bin (bor bitmap bit) out])))
# slot occupied
(let [kon (in arr (* 2 idx)) von (in arr (+ 1 (* 2 idx)))]
(cond
(nil? kon)
[:bin bitmap (cset1 arr (+ 1 (* 2 idx)) (node-assoc von (+ shift 5) h key val added))]
(key= key kon)
[:bin bitmap (cset1 arr (+ 1 (* 2 idx)) val)]
(do (put added 0 true)
[:bin bitmap (cset2 arr (* 2 idx) nil (+ 1 (* 2 idx))
(create-node (+ shift 5) kon von h key val))])))))
(defn- an-assoc [node shift h key val added]
(def cnt (in node 1)) (def arr (in node 2))
(def idx (mask h shift)) (def sub (in arr idx))
(if (nil? sub)
[:an (+ cnt 1) (cset1 arr idx (bin-assoc EMPTY-BIN (+ shift 5) h key val added))]
[:an cnt (cset1 arr idx (node-assoc sub (+ shift 5) h key val added))]))
(defn- hcn-assoc [node shift h key val added]
(def chash (in node 1)) (def cnt (in node 2)) (def arr (in node 3))
(if (= h chash)
(let [idx (hcn-find arr cnt key)]
(if (= idx -1)
(let [out (array ;arr)] (array/push out key) (array/push out val)
(put added 0 true) [:hcn chash (+ cnt 1) out])
(if (= (in arr (+ 1 idx)) val) node
[:hcn chash cnt (cset1 arr (+ 1 idx) val)])))
# different hash at this level: wrap in a bin node, then assoc
(bin-assoc [:bin (bitpos chash shift) (array nil node)] shift h key val added)))
(set create-node (fn [shift k1 v1 k2h k2 v2]
(def k1h (khash k1))
(if (= k1h k2h)
[:hcn k1h 2 (array k1 v1 k2 v2)]
(let [added @[false]
n1 (bin-assoc EMPTY-BIN shift k1h k1 v1 added)]
(bin-assoc n1 shift k2h k2 v2 added)))))
(set node-assoc (fn [node shift h key val added]
(case (in node 0)
:bin (bin-assoc node shift h key val added)
:an (an-assoc node shift h key val added)
:hcn (hcn-assoc node shift h key val added))))
(defn- bin-lookup [node shift h key nf]
(def bitmap (in node 1)) (def arr (in node 2))
(def m (mask h shift)) (def bit (blshift 1 m))
(if (zero? (band bitmap bit)) nf
(let [idx (bidx bitmap m) kon (in arr (* 2 idx)) von (in arr (+ 1 (* 2 idx)))]
(cond
(nil? kon) (node-lookup von (+ shift 5) h key nf)
(key= key kon) von
nf))))
(defn- an-lookup [node shift h key nf]
(def sub (in (in node 2) (mask h shift)))
(if (nil? sub) nf (node-lookup sub (+ shift 5) h key nf)))
(defn- hcn-lookup [node shift h key nf]
(def idx (hcn-find (in node 3) (in node 2) key))
(if (< idx 0) nf (in (in node 3) (+ 1 idx))))
(set node-lookup (fn [node shift h key nf]
(case (in node 0)
:bin (bin-lookup node shift h key nf)
:an (an-lookup node shift h key nf)
:hcn (hcn-lookup node shift h key nf))))
(defn- bin-without [node shift h key]
(def bitmap (in node 1)) (def arr (in node 2))
(def m (mask h shift)) (def bit (blshift 1 m))
(if (zero? (band bitmap bit)) node
(let [idx (bidx bitmap m) kon (in arr (* 2 idx)) von (in arr (+ 1 (* 2 idx)))]
(cond
(nil? kon)
(let [nn (node-without von (+ shift 5) h key)]
(cond (not (nil? nn)) [:bin bitmap (cset1 arr (+ 1 (* 2 idx)) nn)]
(= bitmap bit) nil
[:bin (bxor bitmap bit) (remove-pair arr idx)]))
(key= key kon)
(if (= bitmap bit) nil [:bin (bxor bitmap bit) (remove-pair arr idx)])
node))))
(defn- an-without [node shift h key]
(def cnt (in node 1)) (def arr (in node 2))
(def idx (mask h shift)) (def sub (in arr idx))
(if (nil? sub) node
(let [nn (node-without sub (+ shift 5) h key)]
(cond
(nil? nn) (if (<= cnt 8) (pack-array-node arr idx) [:an (- cnt 1) (cset1 arr idx nil)])
[:an cnt (cset1 arr idx nn)]))))
(defn- hcn-without [node shift h key]
(def chash (in node 1)) (def cnt (in node 2)) (def arr (in node 3))
(def idx (hcn-find arr cnt key))
(cond (= idx -1) node
(= cnt 1) nil
[:hcn chash (- cnt 1) (remove-pair arr (brshift idx 1))]))
(set node-without (fn [node shift h key]
(case (in node 0)
:bin (bin-without node shift h key)
:an (an-without node shift h key)
:hcn (hcn-without node shift h key))))
# depth-first walk: call (f k v) for every entry (the nil key is handled at the
# map level, not in the trie).
(defn- node-each [node f]
(case (in node 0)
:bin (let [arr (in node 2) L (length arr)]
(var i 0)
(while (< i L)
(let [k (in arr i) v (in arr (+ i 1))]
(if (nil? k) (when v (node-each v f)) (f k v)))
(+= i 2)))
:an (let [arr (in node 2)]
(var i 0) (while (< i 32) (when (in arr i) (node-each (in arr i) f)) (++ i)))
:hcn (let [arr (in node 3) L (* 2 (in node 2))]
(var i 0) (while (< i L) (f (in arr i) (in arr (+ i 1))) (+= i 2)))))
# --- map value + public API -------------------------------------------------
(defn- mk [cnt root has-nil nil-val meta]
@{:jolt/type :jolt/phm :jolt/deftype DEFTYPE
:cnt cnt :root root :has-nil has-nil :nil-val nil-val :_meta meta})
(defn phm-get [m k &opt default]
(default default nil)
(if (nil? k)
(if (m :has-nil) (m :nil-val) default)
(let [root (m :root)]
(if root (node-lookup root 0 (khash k) k default) default))))
(def- NF (gensym))
(defn phm-contains? [m k]
(if (nil? k) (truthy? (m :has-nil))
(let [root (m :root)]
(if root (not= (node-lookup root 0 (khash k) k NF) NF) false))))
(defn phm-assoc [m k v]
(if (nil? k)
(mk (if (m :has-nil) (m :cnt) (+ (m :cnt) 1)) (m :root) true v (m :_meta))
(let [added @[false]
root (or (m :root) EMPTY-BIN)
nroot (node-assoc root 0 (khash k) k v added)]
(mk (if (in added 0) (+ (m :cnt) 1) (m :cnt)) nroot (m :has-nil) (m :nil-val) (m :_meta)))))
(defn phm-dissoc [m k]
(if (nil? k)
(if (m :has-nil) (mk (- (m :cnt) 1) (m :root) false nil (m :_meta)) m)
(let [root (m :root)]
(if (and root (phm-contains? m k))
(mk (- (m :cnt) 1) (node-without root 0 (khash k) k) (m :has-nil) (m :nil-val) (m :_meta))
m))))
(defn phm-entries [m]
(def out @[])
(when (m :has-nil) (array/push out [nil (m :nil-val)]))
(when (m :root) (node-each (m :root) (fn [k v] (array/push out [k v]))))
out)
(defn phm-to-struct [m]
# a Janet struct can't hold a nil key (matches Clojure struct/keys behavior);
# every other entry carries over.
(def t @{})
(when (m :root) (node-each (m :root) (fn [k v] (put t k v))))
(table/to-struct t))
(defn phm-count [m] (m :cnt))
(defn phm-has-nil? [m] (truthy? (m :has-nil)))
# --- bulk bottom-up build (jolt-5vsp collections) ---------------------------
# Build the HAMT in one pass from a native array of entries, instead of n
# incremental phm-assoc calls (each of which rebuilt the O(log32 n) path with
# fresh arrays AND allocated a fresh map wrapper). The structure produced is
# IDENTICAL to the incremental one: the trie shape is a function of the key set
# (hash-partitioned, with the same bin<=16 / array-node>=17 promotion threshold
# at each level), so nth/lookup/assoc/without/each read it unchanged. Only a
# hash-collision node's internal order is insertion-dependent, and we preserve
# insertion order there too. Validated against phm-assoc across the size and
# branching boundaries (see the throwaway script in the PR).
#
# Entries are @[h key val] triples (h = khash key). build-node takes a non-empty
# group all destined for the same parent slot and returns its node.
(defn- all-same-hash? [entries]
(def h0 (in (in entries 0) 0))
(var same true) (var i 1) (def L (length entries))
(while (< i L) (when (not= (in (in entries i) 0) h0) (set same false) (break)) (++ i))
same)
(defn- build-node [entries shift]
(if (and (> (length entries) 1) (all-same-hash? entries))
# all keys share a full 32-bit hash -> collision node (insertion order)
(let [arr (array)]
(each e entries (array/push arr (in e 1)) (array/push arr (in e 2)))
[:hcn (in (in entries 0) 0) (length entries) arr])
# partition by the 5-bit mask at this level; iterate slots 0..31 ascending
# so the bin arr / array-node slots land in canonical (bidx) order.
(let [groups @{}]
(each e entries
(def m (mask (in e 0) shift))
(if-let [g (in groups m)] (array/push g e) (put groups m @[e])))
(def occupied (array))
(var i 0) (while (< i 32) (when (in groups i) (array/push occupied i)) (++ i))
(def s (length occupied))
(if (>= s 17)
# array-node: every occupied slot is a sub-node (a lone key becomes its
# own single-key bin node at shift+5, matching the expand path).
(let [slots (array/new-filled 32 nil)]
(each m occupied (put slots m (build-node (in groups m) (+ shift 5))))
[:an s slots])
# bitmap-indexed node: lone key -> leaf pair, group -> nil + sub-node.
(let [arr (array)]
(var bitmap 0)
(each m occupied
(def g (in groups m))
(set bitmap (bor bitmap (blshift 1 m)))
(if (= 1 (length g))
(do (array/push arr (in (in g 0) 1)) (array/push arr (in (in g 0) 2)))
(do (array/push arr nil) (array/push arr (build-node g (+ shift 5))))))
[:bin bitmap arr])))))
(defn phm-from-pairs [pairs &opt meta]
"Bulk-build a phm from an indexed collection of native [k v] pairs. Duplicate
keys follow assoc semantics (last value wins, first-seen key object kept). The
caller must pass native tuples/arrays (phm stays free of the value layer)."
(default meta nil)
(def entries @[])
(def seen @{}) # canon-key -> index into entries (dedup)
(var has-nil false) (var nil-val nil)
(each p pairs
(def k (in p 0)) (def v (in p 1))
(if (nil? k)
(do (set has-nil true) (set nil-val v))
(let [c (ck k)]
(if-let [idx (in seen c)]
(put (in entries idx) 2 v) # last value wins
(do (put seen c (length entries))
(array/push entries @[(khash k) k v]))))))
(def cnt (+ (length entries) (if has-nil 1 0)))
(def root (if (= 0 (length entries)) nil (build-node entries 0)))
(mk cnt root has-nil nil-val meta))
(defn make-phm [&opt kvs]
(default kvs nil)
(if (or (nil? kvs) (= 0 (length kvs)))
(mk 0 nil false nil nil)
# pair up the flat [k0 v0 k1 v1 ...] array and bulk-build
(let [pairs (array) n (length kvs)]
(var i 0) (while (< i n) (array/push pairs [(in kvs i) (in kvs (+ i 1))]) (+= i 2))
(phm-from-pairs pairs))))

View file

@ -1,60 +0,0 @@
# PersistentHashSet — a set backed by a PersistentHashMap (members are keys
# mapped to true). Extracted from phm.janet (jolt-bvek) so phm.janet is purely
# the hash map; the set is a thin layer over it.
#
# REP vs API: this file is ONLY the set representation (phs-* primitives). The
# Clojure-facing set ops (conj/disj/contains?/count/seq dispatch, set-as-fn
# membership) live in core_coll.janet / core_types.janet, branching on
# `:jolt/type :jolt/set`.
(use ./phm)
(defn set?
"Check if x is a PersistentHashSet."
[x]
(and (table? x) (= :jolt/set (x :jolt/type))))
(defn phs-from-seq [xs]
"Bulk-build a PersistentHashSet from an indexed collection of members. Members
back a phm as keys mapped to true, so the bulk HAMT builder (phm-from-pairs,
which dedups by canonical key) gives set semantics in one pass instead of an
immutable phm-assoc per element."
(def pairs @[])
(each x xs (array/push pairs [x true]))
(def m (phm-from-pairs pairs))
@{:jolt/type :jolt/set :phm m :cnt (phm-count m)})
(defn make-phs [& xs]
"Create a PersistentHashSet from items."
(phs-from-seq xs))
(defn phs-conj [s & xs]
(var m (s :phm))
(each x xs (set m (phm-assoc m x true)))
@{:jolt/type :jolt/set :phm m :cnt (phm-count m)})
(defn phs-disj [s & xs]
(var m (s :phm))
(each x xs (set m (phm-dissoc m x)))
@{:jolt/type :jolt/set :phm m :cnt (phm-count m)})
(defn phs-contains? [s x]
(phm-contains? (s :phm) x))
(defn phs-count [s]
(s :cnt))
(defn phs-empty? [s]
(= 0 (s :cnt)))
(defn phs-seq [s]
# phm-to-struct drops a nil member (a Janet struct can't hold a nil key), so
# re-attach it from the phm's has-nil slot. Keep the struct-key order for the
# non-nil members so set printing stays stable.
(def m (s :phm))
(def ks (keys (phm-to-struct m)))
(if (phm-has-nil? m) (tuple nil ;ks) (tuple ;ks)))
(defn phs-get [s x &opt default]
(default default nil)
(if (phm-contains? (s :phm) x) x default))

View file

@ -1,76 +0,0 @@
# Persistent list — an immutable singly-linked cons cell, modeled on Clojure's
# PersistentList. The whole point is O(1) prepend (conj/cons): a new node simply
# points at the existing list as its tail, sharing all of it, so building a list
# with repeated conj is O(n) total instead of O(n²) array copies.
#
# A node is:
# @{:jolt/type :jolt/plist
# :first x head element
# :rest r tail: another plist, or a Janet array/tuple (a list that
# was conj'd onto), or nil for the empty tail
# :count n} element count, or nil when unknown (cons onto a lazy tail)
#
# `:rest` may be a plain array/tuple so `(conj some-list x)` needn't copy the
# original list — the node just references it. pl->array materializes the chain.
#
# REP vs API: this file is ONLY the cons-list representation (pl-* primitives).
# The Clojure-facing list/seq ops (conj/cons/first/rest/count/seq dispatch) live
# in core_coll.janet, which branches on `:jolt/type :jolt/plist`.
(defn plist? [x]
(and (table? x) (= :jolt/plist (get x :jolt/type))))
(defn- counted
"Count of a tail value if known in O(1), else nil."
[r]
(cond
(nil? r) 0
(plist? r) (get r :count)
(or (array? r) (tuple? r) (string? r) (buffer? r)) (length r)
nil))
(defn pl-cons
"Prepend x onto tail r (a plist / array / tuple / nil). O(1)."
[x r]
(def c (counted r))
@{:jolt/type :jolt/plist :first x :rest r :count (if c (+ c 1) nil)})
(def EMPTY-PLIST @{:jolt/type :jolt/plist :first nil :rest nil :count 0})
(defn pl-empty? [p] (= 0 (get p :count)))
(defn pl-first [p] (get p :first))
(defn pl->array
"Materialize the cons chain to a fresh Janet array."
[p]
(def out @[])
(var cur p)
(while (plist? cur)
(if (= 0 (get cur :count))
(set cur nil)
(do (array/push out (get cur :first)) (set cur (get cur :rest)))))
# cur is now a non-plist tail (array/tuple) or nil
(when (and (not (nil? cur)) (or (array? cur) (tuple? cur)))
(each x cur (array/push out x)))
out)
(defn pl-count [p]
(def c (get p :count))
(if (nil? c) (length (pl->array p)) c))
(defn pl-rest
"The tail as a seqable (array). Returns an empty array for a one-element list."
[p]
(if (or (= 0 (get p :count)) (nil? (get p :rest)))
@[]
(get p :rest)))
(defn pl-from-indexed
"Build a plist from a Janet array/tuple, preserving order. O(n)."
[xs]
(var p EMPTY-PLIST)
(var i (- (length xs) 1))
(while (>= i 0)
(set p (pl-cons (in xs i) p))
(-- i))
p)

View file

@ -1,104 +0,0 @@
# Minimal PNG encoder (the host side of `jolt.png`). 8-bit truecolour RGB
# (colour type 2), no interlace, filter 0 (None), and STORED (uncompressed)
# DEFLATE blocks — a valid zlib stream that needs no compressor. Files are larger
# than a compressed PNG but decode everywhere.
#
# Lives in Janet, not Clojure: jolt's per-op cost makes per-byte work (CRC32 over
# every byte) impractical in the overlay, so the whole encode runs here natively
# and jolt calls `write` once with a raw-RGB buffer. Exposed to jolt as
# `janet.png/...` via eval_base's module-load-env (see eval_base.janet).
#
# Janet's bit ops are 32-bit SIGNED (0xFFFFFFFF overflows), so the 32-bit
# unsigned arithmetic of CRC32/Adler32 is done with plain number ops (doubles
# hold 2^32 exactly) plus byte-level bxor, whose operands are always 0..255.
(defn- u8 [v] (% v 256)) # low byte
(defn- shr [v n] (math/floor (/ v n))) # logical shift-right by a power of two
# 32-bit xor via 4 byte-wise xors (each operand 0..255, safely in 32-bit range).
(defn- xor32 [a b]
(var r 0)
(var m 1)
(for i 0 4
(set r (+ r (* (bxor (% (shr a m) 256) (% (shr b m) 256)) m)))
(set m (* m 256)))
r)
(def- crc-table
(let [t (array/new-filled 256 0)]
(for n 0 256
(var c n)
(for _ 0 8
(set c (if (= 1 (% c 2)) (xor32 0xEDB88320 (shr c 2)) (shr c 2))))
(put t n c))
t))
(defn- crc32 [buf]
(var c 0xFFFFFFFF)
(def n (length buf))
(for i 0 n
(set c (xor32 (get crc-table (bxor (% c 256) (get buf i))) (shr c 256))))
(- 0xFFFFFFFF c)) # final xor with all-ones = complement
(defn- adler32 [buf]
(var a 1)
(var b 0)
(def n (length buf))
(for i 0 n
(set a (% (+ a (get buf i)) 65521))
(set b (% (+ b a) 65521)))
(+ (* b 65536) a))
(defn- push-u32be [out v]
(buffer/push-byte out (% (shr v 16777216) 256) (% (shr v 65536) 256)
(% (shr v 256) 256) (% v 256)))
(defn- push-chunk [out typ data]
(push-u32be out (length data))
(def tagged (buffer typ data)) # CRC covers type + data
(buffer/push-string out tagged)
(push-u32be out (crc32 tagged)))
# zlib stream of `raw` as stored DEFLATE blocks (<=65535 bytes each).
(defn- zlib-store [raw]
(def out (buffer/new (+ (length raw) 64)))
(buffer/push-byte out 0x78 0x01) # zlib header: CM=8 CINFO=7, FCHECK ok
(def n (length raw))
(var pos 0)
(while (< pos n)
(def len (min 65535 (- n pos)))
(def final (if (>= (+ pos len) n) 1 0))
(def nlen (- 65535 len)) # one's-complement of len in 16 bits
(buffer/push-byte out final (% len 256) (shr len 256) (% nlen 256) (shr nlen 256))
(buffer/push-string out (buffer/slice raw pos (+ pos len)))
(set pos (+ pos len)))
(push-u32be out (adler32 raw))
out)
(defn encode
"Encode a w*h*3 raw-RGB buffer (row-major, top row first) as a PNG buffer."
[w h rgb]
(assert (= (length rgb) (* w h 3)) "png/encode: rgb length != w*h*3")
(def out (buffer/new (+ 64 (* w h 3))))
(buffer/push-string out "\x89PNG\r\n\x1a\n")
(def ihdr (buffer/new 13))
(push-u32be ihdr w)
(push-u32be ihdr h)
(buffer/push-byte ihdr 8 2 0 0 0) # bit depth 8, colour type 2 (RGB), …
(push-chunk out "IHDR" ihdr)
# scanlines, each prefixed with filter byte 0 (None)
(def stride (* w 3))
(def raw (buffer/new (* h (+ 1 stride))))
(for y 0 h
(buffer/push-byte raw 0)
(def off (* y stride))
(buffer/push-string raw (buffer/slice rgb off (+ off stride))))
(push-chunk out "IDAT" (zlib-store raw))
(push-chunk out "IEND" (buffer/new 0))
out)
(defn write
"Encode and write a w*h*3 raw-RGB buffer to `path` as a PNG."
[path w h rgb]
(spit path (encode w h rgb))
path)

View file

@ -1,189 +0,0 @@
# Persistent vector — a 32-way branching trie with a tail buffer, modeled on
# Clojure's PersistentVector. Immutable: every update returns a new vector that
# structurally shares unchanged subtrees with the old one, so conj/assoc/pop are
# O(log32 n) and share memory instead of copying the whole vector.
#
# Layout:
# @{:jolt/type :jolt/pvec
# :cnt n number of elements
# :shift s bits to shift the index for the root level (5 * depth)
# :root node trie root: a tuple of up to 32 children
# :tail tail} a tuple of up to 32 trailing elements (append fast-path)
#
# Trie nodes are immutable tuples so unchanged subtrees are shared by identity.
#
# REP vs API: this file is ONLY the trie representation (pv-* primitives). The
# Clojure-facing vector ops (nth/conj/assoc/subvec, the count/seq/first/rest
# dispatch, and tuple↔pvec polymorphism) live in core_coll.janet /
# core_types.janet, which dispatch on `:jolt/type :jolt/pvec`.
(def- bits 5)
(def- width 32) # 2^bits
(def- mask 31) # width - 1
(def empty-node [])
(defn pvec? [x]
(and (table? x) (= :jolt/pvec (get x :jolt/type))))
(defn make-pv [cnt shift root tail]
@{:jolt/type :jolt/pvec :cnt cnt :shift shift :root root :tail tail})
(def EMPTY (make-pv 0 bits empty-node []))
(defn pv-count [pv] (get pv :cnt))
# Index of the first element held in the tail (everything before lives in the trie).
(defn- tail-offset [cnt]
(if (< cnt width) 0 (blshift (brshift (- cnt 1) bits) bits)))
# Return the 32-element leaf array containing index i.
(defn- leaf-for [pv i]
(if (>= i (tail-offset (get pv :cnt)))
(get pv :tail)
(do
(var node (get pv :root))
(var level (get pv :shift))
(while (> level 0)
(set node (get node (band (brshift i level) mask)))
(set level (- level bits)))
node)))
(defn pv-nth [pv i &opt dflt]
(if (and (>= i 0) (< i (get pv :cnt)))
(get (leaf-for pv i) (band i mask))
dflt))
# --- conj -------------------------------------------------------------------
# Push the full tail down into the trie, returning a new root node array.
(defn- push-tail [level parent tail cnt]
(def sub-idx (band (brshift (- cnt 1) level) mask))
(def child
(if (= level bits)
tail
(let [c (get parent sub-idx)]
(if c
(push-tail (- level bits) c tail cnt)
(push-tail (- level bits) empty-node tail cnt)))))
(def arr (array/slice parent))
(put arr sub-idx child)
(tuple/slice arr))
(defn pv-conj [pv val]
(def cnt (get pv :cnt))
(def tail (get pv :tail))
(if (< (length tail) width)
# Room in the tail: just append to it.
(make-pv (+ cnt 1) (get pv :shift)
(get pv :root)
(tuple/slice (tuple ;tail val)))
# Tail is full: push it into the trie, start a fresh tail with val.
(let [shift (get pv :shift)
root (get pv :root)]
(if (> (brshift cnt bits) (blshift 1 shift))
# Root overflow: grow the trie one level taller.
(let [new-root (tuple root (push-tail shift empty-node tail cnt))]
(make-pv (+ cnt 1) (+ shift bits) new-root [val]))
(make-pv (+ cnt 1) shift (push-tail shift root tail cnt) [val])))))
# --- assoc ------------------------------------------------------------------
(defn- assoc-in-node [level node i val]
(def arr (array/slice node))
(if (= level 0)
(put arr (band i mask) val)
(let [sub-idx (band (brshift i level) mask)]
(put arr sub-idx (assoc-in-node (- level bits) (get node sub-idx) i val))))
(tuple/slice arr))
(defn pv-assoc [pv i val]
(def cnt (get pv :cnt))
(cond
(= i cnt) (pv-conj pv val)
(and (>= i 0) (< i cnt))
(if (>= i (tail-offset cnt))
(let [tail (array/slice (get pv :tail))]
(put tail (band i mask) val)
(make-pv cnt (get pv :shift) (get pv :root) (tuple/slice tail)))
(make-pv cnt (get pv :shift)
(assoc-in-node (get pv :shift) (get pv :root) i val)
(get pv :tail)))
(error (string "Index " i " out of bounds for vector of length " cnt))))
# --- pop --------------------------------------------------------------------
(defn- pop-tail [level node cnt]
(def sub-idx (band (brshift (- cnt 2) level) mask))
(cond
(> level bits)
(let [child (pop-tail (- level bits) (get node sub-idx) cnt)]
(if (and (nil? child) (= sub-idx 0))
nil
(let [arr (array/slice node)] (put arr sub-idx child) (tuple/slice arr))))
(= sub-idx 0) nil
(let [arr (array/slice node)] (put arr sub-idx nil) (tuple/slice arr))))
(defn pv-pop [pv]
(def cnt (get pv :cnt))
(cond
(= cnt 0) (error "Can't pop empty vector")
(= cnt 1) EMPTY
(> (- cnt (tail-offset cnt)) 1)
# More than one element in the tail: drop the last tail element.
(let [tail (get pv :tail)]
(make-pv (- cnt 1) (get pv :shift) (get pv :root)
(tuple/slice tail 0 (- (length tail) 1))))
# Tail has one element: the new tail is the last leaf of the trie.
(let [shift (get pv :shift)
new-tail (leaf-for pv (- cnt 2))
new-root-raw (pop-tail shift (get pv :root) cnt)
new-root (if (nil? new-root-raw) empty-node new-root-raw)]
(if (and (> shift bits) (nil? (get new-root 1)))
# Trie lost a level: promote the single remaining child to root.
(make-pv (- cnt 1) (- shift bits) (get new-root 0) new-tail)
(make-pv (- cnt 1) shift new-root new-tail)))))
# --- conversions ------------------------------------------------------------
(defn pv->array [pv]
(def out @[])
(def cnt (get pv :cnt))
(var i 0)
(while (< i cnt)
(array/push out (pv-nth pv i))
(++ i))
out)
(defn pv-from-indexed [xs]
# Build a pvec from any Janet-indexed collection (tuple/array) by constructing
# the trie BOTTOM-UP in one pass, instead of n incremental pv-conj calls (each
# of which allocated a pv wrapper and copied the tail). The structure produced
# is identical to the incremental one — tail-offset(n) = ((n-1)>>5)<<5 is
# exactly the trie/tail split, so leaf-for/nth/conj/assoc all read it the same.
# This is the bulk path behind vec/mapv/into-a-vector (jolt-5vsp collections).
(def n (length xs))
(if (<= n width)
# everything fits in the tail (matches EMPTY's shift)
(make-pv n bits empty-node (tuple/slice xs))
(let [tail-len (+ 1 (mod (- n 1) width))
trie-count (- n tail-len) # = tail-offset(n), a multiple of width
tail (tuple/slice xs trie-count n)]
# value leaves: full 32-element tuples over [0, trie-count)
(def leaves @[])
(var i 0)
(while (< i trie-count)
(array/push leaves (tuple/slice xs i (+ i width)))
(set i (+ i width)))
# group nodes 32-wide, bottom-up, until <=32 remain; that becomes the root
(var level leaves)
(var shift bits)
(while (> (length level) width)
(def parent @[])
(var j 0)
(while (< j (length level))
(array/push parent (tuple/slice level j (min (length level) (+ j width))))
(set j (+ j width)))
(set level parent)
(set shift (+ shift bits)))
(make-pv n shift (tuple/slice level) tail))))

View file

@ -1,833 +0,0 @@
# Jolt Clojure Reader
# Recursive descent parser for Clojure source text.
# Output convention:
# Symbols foo, foo/bar → {:jolt/type :symbol :ns "foo" :name "bar"}
# Keywords :foo, :foo/bar → Janet keyword :foo, :foo/bar
# Lists (a b c) → Janet array @[a b c]
# Vectors [a b c] → Janet tuple [a b c]
# Maps {:a 1} → Janet struct {:a 1}
# Sets #{1 2} → tagged struct {:jolt/type :jolt/set :value [1 2]}
(use ./types)
(import ./phm :as phm)
# Forward declaration for mutual recursion
(var read-form nil)
(var read-meta nil) # forward decl: read-dispatch (#^) calls it before its defn below
# Source-position tracking for the success checker (jolt-fqy). When enabled, the
# reader records each LIST form's absolute start offset (lists are the forms that
# become :invoke nodes — what the checker reports on). Off by default: a flag
# check per list is the only cost when the checker isn't running. Keyed by form
# IDENTITY (lists are fresh arrays, never interned), so a position survives
# macroexpansion exactly when the user's own sub-form is spliced through, and is
# absent for macro-synthesized structure — which is what we want (fall back to
# the call site). Not cleared between parses: nested parses (a require mid-load)
# would otherwise drop an outer file's positions; the table is bounded by forms
# compiled this process and only populated when the checker is on.
(def form-pos-table @{})
(var track-positions false)
(var pos-base 0) # absolute offset of the slice read-form currently sees
(defn track-positions!
"Enable/disable list-form position recording (jolt-fqy)."
[on] (set track-positions on))
(defn set-pos-base!
"Tell the reader the absolute offset of the slice it is about to read, so
recorded list positions are absolute (parse-all-positioned reads a shrinking
remainder)."
[b] (set pos-base b))
(defn form-pos
"Absolute start offset recorded for a list form, or nil."
[form] (get form-pos-table form))
(defn checker-enabled?
"True when JOLT_TYPE_CHECK selects a non-off level — the loaders use this to
decide whether to record form positions for the checker (jolt-fqy)."
[]
(def tc (os/getenv "JOLT_TYPE_CHECK"))
(if (and tc (not= tc "off") (not= tc "0")) true false))
(def whitespace-chars " \t\n\r,")
(defn whitespace? [c]
(or (= c 32) # space
(= c 10) # \n
(= c 9) # \t
(= c 13) # \r
(= c 44))) # comma
# Reader errors carry the raw byte OFFSET; the parse entry points (parse-string,
# parse-all-positioned) convert offset -> line:col against the full source and
# re-raise Clojure's 'Syntax error reading source at (file:line:col): msg'
# shape. Raising structured here keeps the 15 error sites one-liners.
# (jolt-2o7.5)
(defn- reader-error [msg pos]
(error {:jolt/type :jolt/reader-error :msg msg :pos pos}))
(defn line-col
"1-based [line col] of byte offset in source."
[source offset]
(var line 1)
(var bol 0)
(def stop (min offset (length source)))
(loop [i :range [0 stop]]
(when (= (in source i) (chr "\n")) (++ line) (set bol (+ i 1))))
[line (+ 1 (- stop bol))])
(defn format-reader-error
"Clojure-shaped syntax error message for a reader-error struct raised
against source; file may be nil (omitted)."
[e source file]
(def [l c] (line-col source (e :pos)))
(if file
(string "Syntax error reading source at (" file ":" l ":" c "): " (e :msg))
(string "Syntax error reading source at (" l ":" c "): " (e :msg))))
(defn reader-error? [e]
(and (struct? e) (= :jolt/reader-error (e :jolt/type))))
(defn skip-whitespace [s pos]
(if (and (< pos (length s))
(whitespace? (s pos)))
(skip-whitespace s (+ pos 1))
pos))
(defn digit? [c]
(and (>= c 48) (<= c 57)))
(defn hex-digit? [c]
(or (and (>= c 48) (<= c 57))
(and (>= c 65) (<= c 70))
(and (>= c 97) (<= c 102))))
(defn symbol-start? [c]
(or (and (>= c 65) (<= c 90)) # A-Z
(and (>= c 97) (<= c 122)) # a-z
(= c 42) # *
(= c 43) # +
(= c 33) # !
(= c 95) # _
(= c 45) # -
(= c 63) # ?
(= c 46) # .
(= c 60) # <
(= c 62) # >
(= c 61) # =
(= c 38) # &
(= c 124) # |
(= c 36) # $
(= c 37) # %
(= c 47))) # /
(defn symbol-char? [c]
(or (symbol-start? c)
(digit? c)
(= c 35) # #
(= c 39) # '
(= c 58))) # :
(defn read-symbol-name [s pos end]
(if (and (< end (length s))
(symbol-char? (s end)))
(read-symbol-name s pos (+ end 1))
end))
(defn make-symbol
"Create a Jolt symbol struct."
[name]
(let [slash (string/find "/" name)]
(if (and slash (> slash 0))
{:jolt/type :symbol
:ns (string/slice name 0 slash)
:name (string/slice name (+ slash 1))}
{:jolt/type :symbol
:ns nil
:name name})))
(defn sym
"Convenience to create a symbol during testing."
[name]
(make-symbol name))
(defn read-symbol [s pos]
(let [end (read-symbol-name s pos pos)]
(if (= end pos)
(reader-error (string "Unrecognized character: " (string/from-bytes (s pos))) pos)
(let [name (string/slice s pos end)]
(if (= name "nil") [nil end]
(if (= name "true") [true end]
(if (= name "false") [false end]
[(make-symbol name) end])))))))
(defn read-keyword-name [s pos end]
(if (and (< end (length s))
(symbol-char? (s end)))
(read-keyword-name s pos (+ end 1))
end))
(defn read-keyword [s pos]
# pos is at the first colon
(if (and (< (+ pos 1) (length s)) (= (s (+ pos 1)) 58))
# ::foo/bar — auto-resolved keyword
(let [start (+ pos 2)
end (read-keyword-name s start start)
name (string/slice s start end)]
[(keyword name) end])
# :foo or :foo/bar
(let [start (+ pos 1)
end (read-keyword-name s start start)
name (string/slice s start end)]
[(keyword name) end])))
(defn escape-char [c]
(if (= c 110) "\n"
(if (= c 116) "\t"
(if (= c 114) "\r"
(if (= c 92) "\\"
(if (= c 34) "\""
(string/from-bytes c)))))))
(defn read-string-chars [s pos end chars]
(if (>= pos end)
(reader-error "Unterminated string" pos)
(let [c (s pos)]
(if (= c 92) # backslash
(let [next-pos (+ pos 1)]
(if (>= next-pos end)
(reader-error "Unterminated escape" pos)
(read-string-chars s (+ pos 2) end
(array/push chars (escape-char (s next-pos))))))
(if (= c 34) # closing quote
[pos chars]
(read-string-chars s (+ pos 1) end
(array/push chars (string/from-bytes c))))))))
(defn read-string [s pos]
# pos is at opening double-quote
(let [end (length s)
[new-pos chars] (read-string-chars s (+ pos 1) end @[])]
[(string/join chars "") (+ new-pos 1)]))
(defn read-hex-digits [s pos end]
(if (and (< end (length s)) (hex-digit? (s end)))
(read-hex-digits s pos (+ end 1))
end))
(defn read-digits [s pos end]
(if (and (< end (length s)) (digit? (s end)))
(read-digits s pos (+ end 1))
end))
(defn read-fractional [s pos end]
(if (and (< end (length s)) (digit? (s end)))
(read-fractional s pos (+ end 1))
end))
# Value of an alphanumeric digit for radix parsing (0-9, a-z/A-Z = 10-35).
(defn- radix-digit-val [c]
(cond
(and (>= c 48) (<= c 57)) (- c 48) # 0-9
(and (>= c 97) (<= c 122)) (+ 10 (- c 97)) # a-z
(and (>= c 65) (<= c 90)) (+ 10 (- c 65)) # A-Z
nil))
(defn- read-alnum [s pos end]
(if (and (< end (length s)) (not (nil? (radix-digit-val (s end)))))
(read-alnum s pos (+ end 1))
end))
(defn- read-exponent
"If s[end] is e/E (optionally with sign) followed by digits, return the index
past the exponent; else end."
[s end]
(let [len (length s)]
(if (and (< end len) (or (= (s end) 101) (= (s end) 69))) # e / E
(let [p (if (and (< (+ end 1) len) (or (= (s (+ end 1)) 43) (= (s (+ end 1)) 45))) (+ end 2) (+ end 1))
de (read-digits s p p)]
(if (> de p) de end))
end)))
# Jolt has no true bignum/ratio types (see README): an integer/float literal
# suffixed N (bigint) or M (bigdec) reads as the plain number, a ratio a/b reads
# as the double quotient, and radixed integers (2r101, 16rFF) are parsed by base.
(defn read-number [s pos]
(var start pos) # start is mutable for sign handling
(var neg false)
(def len (length s))
# optional sign
(if (and (< pos len) (= (s pos) 45))
(do (set start (+ pos 1)) (set neg true)))
(let [pos start
hex? (and (< (+ pos 1) len)
(= (s pos) 48) (or (= (s (+ pos 1)) 120) (= (s (+ pos 1)) 88)))] # 0x / 0X
(if hex?
(let [hs (+ pos 2) he (read-hex-digits s hs hs)]
(if (= he hs) (reader-error "Expected hex digits" pos))
(let [he2 (if (and (< he len) (= (s he) 78)) (+ he 1) he) # trailing N
val (scan-number (string "0x" (string/slice s hs he)))]
[(if neg (- val) val) he2]))
(let [iend (read-digits s pos pos)]
(if (= iend pos) (reader-error "Expected number" pos))
(cond
# radix integer: <base>r<digits>, e.g. 2r1010, 16rFF, 36rZ
(and (< iend len) (or (= (s iend) 114) (= (s iend) 82)))
(let [base (scan-number (string/slice s pos iend))
ds (+ iend 1)
de (read-alnum s ds ds)]
(if (= de ds) (reader-error "Expected radix digits" ds))
(var acc 0)
(var i ds)
(while (< i de) (set acc (+ (* acc base) (radix-digit-val (s i)))) (++ i))
[(if neg (- acc) acc) de])
# ratio: <int>/<int> (only when a digit follows the slash)
(and (< (+ iend 1) len) (= (s iend) 47) (digit? (s (+ iend 1))))
(let [ds (+ iend 1) de (read-digits s ds ds)
numr (scan-number (string/slice s pos iend))
den (scan-number (string/slice s ds de))]
[(if neg (- (/ numr den)) (/ numr den)) de])
# fractional and/or exponent, optional trailing N/M
(let [frac-end (if (and (< iend len) (= (s iend) 46))
(let [fs (+ iend 1) fe (read-fractional s fs fs)]
(if (= fe fs) (error "Expected digit after .")) fe)
iend)
exp-end (read-exponent s frac-end)
val (scan-number (string/slice s start exp-end))
# consume a trailing N (bigint) or M (bigdec) suffix
fin (if (and (< exp-end len) (or (= (s exp-end) 78) (= (s exp-end) 77)))
(+ exp-end 1) exp-end)]
[(if neg (- val) val) fin]))))))
# Read forms until `close` (a byte), returning [items end-pos]. Shared by the
# list/vector/set readers: skip #_ discards and splice #?@ items uniformly, so the
# skip/splice handling can't drift between them (it did once). The map reader
# keeps its own loop — its key/value pairing needs a different value-slot scan.
(defn read-delimited [s start-pos close err]
(defn read-items [s pos items]
(let [pos (skip-whitespace s pos)]
(if (>= pos (length s))
(reader-error err pos))
(if (= (s pos) close)
[items (+ pos 1)]
(let [[form new-pos] (read-form s pos)]
# skip #_ discarded forms
(if (and (struct? form) (= :jolt/skip (form :jolt/type)))
(read-items s new-pos items)
# splice #?@ items into the collection
(if (and (struct? form) (= :jolt/splice (form :jolt/type)))
(read-items s new-pos (array/concat items (form :items)))
(read-items s new-pos (array/push items form))))))))
(read-items s start-pos @[]))
(defn read-list [s pos]
# pos is at opening paren
(read-delimited s (+ pos 1) 41 "Unterminated list")) # )
(defn read-vector [s pos]
# pos is at opening bracket
(let [[items end] (read-delimited s (+ pos 1) 93 "Unterminated vector")] # ]
[(tuple/slice (tuple ;items)) end]))
# A map-literal form. Janet structs drop nil keys/values, so when a key or value
# is nil (e.g. {:a nil}) build a phm — it preserves nil, matching Clojure. The
# common nil-free case stays a struct: fast, and what the downstream map-form
# handling (evaluator/analyzer) already expects. Collection keys are left to
# eval-time construction (build-map-literal/eval-form), which phm-ifies them.
# Public so the jolt.host contract (host_iface form-make-map) can build a map FORM
# with the same source-order kv tracking the portable Clojure reader relies on.
(defn reader-map [kvs]
(var has-nil false) (var i 0)
(while (< i (length kvs)) (when (nil? (in kvs i)) (set has-nil true) (break)) (++ i))
# Source order rides along out-of-band (jolt-p3c): struct iteration is hash
# order, but Clojure evaluates literal entries left to right. A struct
# PROTOTYPE carries it without changing the form's map behavior (keys/kvs/
# length ignore protos; jolt-equal? compares maps structurally); the phm rep
# (nil key/value present) gets a plain extra field.
(if has-nil
(let [m (phm/make-phm kvs)]
(put m :jolt/kv-order (tuple/slice kvs))
m)
(struct/with-proto (struct :jolt/kv-order (tuple/slice kvs)) ;kvs)))
(defn form-kv-order
"Source-ordered [k v k v ...] tuple of a map FORM (nil for maps built at
runtime, which carry no reader order)."
[form]
(cond
(struct? form) (get (struct/getproto form) :jolt/kv-order)
(table? form) (get form :jolt/kv-order)))
(defn read-map [s pos]
# pos is at opening brace
(defn read-kvs [s pos kvs]
(let [pos (skip-whitespace s pos)]
(if (>= pos (length s))
(reader-error "Unterminated map" pos))
(if (= (s pos) 125) # }
[(reader-map kvs) (+ pos 1)]
(let [[key new-pos] (read-form s pos)]
(if (and (struct? key) (= :jolt/skip (key :jolt/type)))
(read-kvs s new-pos kvs)
(if (and (struct? key) (= :jolt/splice (key :jolt/type)))
(read-kvs s new-pos (array/concat kvs (key :items)))
(let [pos (skip-whitespace s new-pos)
# The VALUE slot must skip comments/#_ while KEEPING the
# pending key: dropping both (the old behavior) desynced
# the kv pairing — {:a ; comment\n 1} read 1 as the next
# KEY and the closing } landed in value position
# ("Unmatched closing brace", jolt-ou8 / Selmer deps.edn).
[val new-pos2]
(do
(var vp pos)
(var v nil)
(var looking true)
(while looking
(def [f np] (read-form s vp))
(set vp np)
(if (and (struct? f) (= :jolt/skip (f :jolt/type)))
(set vp (skip-whitespace s np))
(do (set v f) (set looking false))))
[v vp])]
(if (and (struct? val) (= :jolt/splice (val :jolt/type)))
# Only push key if splice contributes items
(if (> (length (val :items)) 0)
(do (array/push kvs key) (read-kvs s new-pos2 (array/concat kvs (val :items))))
(read-kvs s new-pos2 kvs))
(read-kvs s new-pos2 (-> kvs (array/push key) (array/push val)))))))))))
(read-kvs s (+ pos 1) @[]))
(defn read-set [s pos]
# pos is at #, next char is {
(let [[items end] (read-delimited s (+ pos 2) 125 "Unterminated set")] # }
[{:jolt/type :jolt/set :value (tuple/slice (tuple ;items))} end]))
(defn read-char-name-end [s pos]
(if (and (< pos (length s)) (symbol-char? (s pos)))
(read-char-name-end s (+ pos 1))
pos))
(defn read-char [s pos]
# pos is at backslash; produce a char value directly (self-evaluating)
(when (>= (+ pos 1) (length s))
(error "unexpected end of input after \\"))
(let [end (read-char-name-end s (+ pos 1))]
(if (= end (+ pos 1))
# The char right after \ isn't a symbol char (e.g. \{ \( \, \% \" ): it's a
# one-character literal of that character itself.
[(make-char (s (+ pos 1))) (+ pos 2)]
(let [char-name (string/slice s (+ pos 1) end)]
[(char-from-name char-name) end]))))
(defn read-anon-fn [s pos]
# pos is at #, next char is (
(let [[form new-pos] (read-form s (+ pos 1))]
# Positional index of a %-symbol name: % and %1 are both 1, %N is N, %& is the
# rest param (:rest); anything else is not a positional (nil). The fixed arity
# is the MAX index used (Clojure semantics: #(do %2 %&) -> [p1 p2 & rest], so
# unused lower positions still get a placeholder param and %& starts after %2).
(defn- pct-index [nm]
(cond
(= nm "%") 1
(= nm "%&") :rest
(and (> (length nm) 1) (= "%" (string/slice nm 0 1)))
(let [n (scan-number (string/slice nm 1))]
(if (and n (= n (math/floor n)) (>= n 1)) n nil))
nil))
# Pass 1: max positional index + whether %& is used.
(var max-n 0)
(var has-rest false)
(defn- scan-pct [f]
(cond
(and (struct? f) (= :symbol (f :jolt/type)))
(let [i (pct-index (f :name))]
(cond (= i :rest) (set has-rest true)
(and i (> i max-n)) (set max-n i)))
(or (array? f) (tuple? f)) (each x f (scan-pct x))
# set literal form — scan its elements (#(... #{%} ...))
(and (struct? f) (= :jolt/set (f :jolt/type))) (each x (f :value) (scan-pct x))
# map literal form — scan its keys AND values (#(... {:k %} ...))
(not (nil? (form-kv-order f))) (each x (form-kv-order f) (scan-pct x))
nil))
(scan-pct form)
# One canonical gensym per slot 1..max-n (placeholders for unused), plus rest.
# Param names carry a trailing `#` (auto-gensym suffix, matching Clojure's
# `p1__N#`) so a #() written inside a syntax-quote survives: sq-symbol treats
# `#`-suffixed names as auto-gensyms (mapped consistently, left unqualified)
# rather than qualifying them to the current ns — a qualified symbol is not a
# valid fn param. Harmless outside a backtick (just a regular symbol name).
(def slot-syms @{})
(var i 1)
(while (<= i max-n) (put slot-syms i (sym (string (gensym) "#"))) (++ i))
(def rest-sym (if has-rest (sym (string (gensym) "#")) nil))
# Pass 2: replace each %-symbol with its slot's gensym.
(defn- replace-pct [f]
(cond
(and (struct? f) (= :symbol (f :jolt/type)))
(let [idx (pct-index (f :name))]
(cond (= idx :rest) rest-sym
idx (get slot-syms idx)
f))
(array? f) (array ;(map replace-pct f))
(tuple? f) (tuple ;(map replace-pct f))
(and (struct? f) (= :jolt/set (f :jolt/type)))
{:jolt/type :jolt/set :value (tuple ;(map replace-pct (f :value)))}
(not (nil? (form-kv-order f)))
(reader-map (array ;(map replace-pct (form-kv-order f))))
f))
(def replaced (replace-pct form))
(def arg-names @[])
(set i 1)
(while (<= i max-n) (array/push arg-names (get slot-syms i)) (++ i))
(when has-rest
(array/push arg-names (sym "&"))
(array/push arg-names rest-sym))
[@[(sym "fn*") (tuple ;arg-names) replaced] new-pos]))
# The reader-conditional feature set (spec 02-reader S18): jolt is its own
# dialect, so the portable convention applies — the dialect key plus :default.
# Matching is by CLAUSE order (the first clause whose key is in the feature
# set wins), exactly like Clojure — NOT key-priority. JOLT_FEATURES overrides
# (comma-separated, e.g. "jolt,clj,default") for compat experiments and A/B
# measurement; :default is always honored.
# Mutable so a loading context can opt a clj-targeted foreign library into
# :clj compatibility (e.g. SCI) — see reader-features-set!. jolt itself and
# the conformance surface read under the portable set.
(var reader-features nil)
(defn reader-features-set!
"Replace the active reader-conditional feature set (a list of keyword-name
strings or keywords). :default is always honored. Returns the previous set
so callers can restore."
[names]
(def prev reader-features)
(def t @{})
(each n names (put t (if (keyword? n) n (keyword n)) true))
(put t :default true)
(set reader-features t)
prev)
(reader-features-set!
(let [env (os/getenv "JOLT_FEATURES")]
(if env (filter |(> (length $) 0) (string/split "," env))
["jolt" "default"])))
(defn read-reader-conditional [s pos]
# pos is at #, next char is ? or ?@
(def splice? (and (< (+ pos 2) (length s)) (= (s (+ pos 2)) 64))) # @ = 64
(def form-start (if splice? (+ pos 3) (+ pos 2)))
(let [[form new-pos] (read-form s form-start)]
(if (array? form)
(do
# First clause (in clause order) whose feature key is in the set.
# `matched` is tracked separately: a matched clause may be nil.
(var result nil)
(var matched false)
(var i 0)
(while (and (not matched) (< i (length form)))
(when (get reader-features (in form i))
(set result (in form (+ i 1)))
(set matched true))
(+= i 2))
(if splice?
# #?@ splicing: resolve :clj branch, wrap for splice
(let [items (if (nil? result)
@[]
(if (or (array? result) (tuple? result))
result
@[result]))]
[{:jolt/type :jolt/splice :items items} new-pos])
# #? non-splicing: skip nil results (from :cljs branches on CLJ)
(if (nil? result)
[{:jolt/type :jolt/skip} new-pos]
[result new-pos])))
[{:jolt/type :jolt/reader-conditional :clauses form} new-pos])))
(defn read-var-quote [s pos]
# pos is at #, next char is '
(let [[form new-pos] (read-form s (+ pos 2))]
[(array (sym "var") form) new-pos]))
(defn read-regex [s pos]
# pos is at #, next char is "
# Read until unescaped closing "
(var i (+ pos 2))
(var done nil)
(while (and (< i (length s)) (not done))
(if (= (s i) 92) # backslash — skip next char
(+= i 2)
(if (= (s i) 34) # closing quote
(set done [(struct ;[:jolt/type :jolt/tagged :tag :regex :form (string/slice s (+ pos 2) i)])
(+ i 1)])
(++ i))))
(if done done (reader-error "Unterminated regex literal" pos)))
(defn read-dispatch [s pos]
# pos is at #
(if (>= (+ pos 1) (length s))
(error "Unexpected end after #"))
(let [c (s (+ pos 1))]
(cond
(= c 123) (read-set s pos) # #{
(= c 40) (read-anon-fn s pos) # #(
(= c 63) (read-reader-conditional s pos) # #?
(= c 95) (let [[_ new-pos] (read-form s (+ pos 2))] # #_
[{:jolt/type :jolt/skip} new-pos])
(= c 39) (read-var-quote s pos) # #'
(= c 94) (read-meta s (+ pos 1)) # #^ — deprecated metadata reader macro (= ^)
(= c 34) (read-regex s pos) # #"regex
(= c 35) # ## symbolic value: ##Inf ##-Inf ##NaN
(let [end (read-symbol-name s (+ pos 2) (+ pos 2))
name (string/slice s (+ pos 2) end)]
(cond
(= name "Inf") [math/inf end]
(= name "-Inf") [(- math/inf) end]
(= name "NaN") [math/nan end]
(reader-error (string "Invalid symbolic value: ##" name) pos)))
# unknown dispatch — tagged literal
(let [end (read-symbol-name s pos pos)
tag (string/slice s pos end)
[form new-pos] (read-form s end)]
[{:jolt/type :jolt/tagged :tag (keyword tag) :form form} new-pos]))))
(defn- self-evaluating-literal?
"True for forms syntax-quote passes through unchanged: strings, numbers,
booleans, nil, keywords, and character literals. NOT symbols (they qualify)
or collections (they template)."
[form]
(or (nil? form) (= true form) (= false form) (number? form)
(string? form) (buffer? form) (keyword? form)
(and (struct? form) (= :jolt/char (form :jolt/type)))))
(defn read-quote [s new-pos token-sym]
(let [[form final-pos] (read-form s new-pos)]
# Spec 02-reader S25: syntax-quote of a self-evaluating literal is the
# literal, collapsed at READ time (matching Clojure's reader) — so nested
# backticks over literals are inert: ```"meow" reads as "meow".
(if (and (= "syntax-quote" (token-sym :name)) (self-evaluating-literal? form))
[form final-pos]
[(array token-sym form) final-pos])))
(defn- meta-form->map
"Normalize a metadata reader form (Clojure semantics): a symbol or string is a
:tag, a keyword is {kw true}. Returns a metadata table, or nil if it isn't one
of those simple shapes (e.g. a map literal — handled via with-meta instead)."
[meta-form]
(cond
(keyword? meta-form) {meta-form true}
# A symbol tag keeps its namespace qualifier (^t/Ray -> "t/Ray", not "Ray") so
# an aliased/qualified record hint resolves through the ns's aliases the same
# way a bare referred one does; dropping it silently mis-hinted across
# namespaces. Bare symbols (^Ray, ^String) are unchanged.
(and (struct? meta-form) (= :symbol (meta-form :jolt/type)))
{:tag (if (meta-form :ns)
(string (meta-form :ns) "/" (meta-form :name))
(meta-form :name))}
(string? meta-form) {:tag meta-form}
nil))
(set read-meta (fn read-meta [s pos]
# pos is at ^
(let [[meta-form new-pos] (read-form s (+ pos 1))
[form new-pos2] (read-form s new-pos)
m (meta-form->map meta-form)]
(if (and m (struct? form) (= :symbol (form :jolt/type)))
# Attach the metadata to the symbol itself and keep it a bare symbol, so
# type hints (^String x) and ^:dynamic etc. are transparent in every
# position (params, lets, bodies) — the evaluator reads :name and ignores
# :meta. This is what makes type hints "parse and otherwise do nothing".
[(struct ;(kvs form) :meta (merge (or (form :meta) {}) m)) new-pos2]
# Non-symbol targets (collections etc.) keep a runtime with-meta form. Use the
# NORMALIZED metadata map (:kw -> {:kw true}, tag -> {:tag …}); a map-literal
# meta-form (m is nil) is already a map, so pass it through.
[(array (sym "with-meta") form (if m m meta-form)) new-pos2]))))
(defn read-until-newline [s pos]
(if (or (>= pos (length s)) (= (s pos) 10))
pos
(read-until-newline s (+ pos 1))))
(set read-form (fn [s pos]
(let [pos (skip-whitespace s pos)]
(if (>= pos (length s))
[nil pos]
(let [c (s pos)]
(cond
# comment
(= c 59)
(let [line-end (read-until-newline s pos)]
[{:jolt/type :jolt/skip} line-end])
# dispatch
(= c 35)
(read-dispatch s pos)
# string
(= c 34)
(read-string s pos)
# list
(= c 40)
(let [r (read-list s pos)]
(when track-positions (put form-pos-table (in r 0) (+ pos-base pos)))
r)
# unmatched closing delimiters
(= c 41)
(reader-error "Unmatched delimiter: )" pos)
(= c 93)
(reader-error "Unmatched delimiter: ]" pos)
(= c 125)
(reader-error "Unmatched delimiter: }" pos)
# vector
(= c 91)
(read-vector s pos)
# map
(= c 123)
(read-map s pos)
# keyword
(= c 58)
(read-keyword s pos)
# quote
(= c 39)
(read-quote s (+ pos 1) (sym "quote"))
# syntax-quote / backtick
(= c 96)
(read-quote s (+ pos 1) (sym "syntax-quote"))
# unquote ~
(= c 126)
(if (and (< (+ pos 1) (length s)) (= (s (+ pos 1)) 64))
(read-quote s (+ pos 2) (sym "unquote-splicing"))
(read-quote s (+ pos 1) (sym "unquote")))
# deref — @x is (clojure.core/deref x), QUALIFIED like Clojure, so it
# still derefs even where a ns shadows `deref` (malli excludes and
# redefines clojure.core/deref for its own schema-deref).
(= c 64)
(read-quote s (+ pos 1) (sym "clojure.core/deref"))
# metadata
(= c 94)
(read-meta s pos)
# character
(= c 92)
(read-char s pos)
# number or symbol
(if (or (digit? c)
(and (= c 45) (< (+ pos 1) (length s)) (digit? (s (+ pos 1))))
(and (= c 43) (< (+ pos 1) (length s)) (digit? (s (+ pos 1)))))
(read-number s pos)
(read-symbol s pos))))))))
(defn parse-string
"Parse a Clojure source string and return the first form."
[s]
(try
(let [[form pos] (read-form s 0)]
(if (and (struct? form) (= :jolt/skip (form :jolt/type)))
(parse-string (string/slice s pos))
form))
([err fib]
(if (reader-error? err)
(error (format-reader-error err s nil))
(propagate err fib)))))
(defn parse-next
"Parse the first form from a string. Returns [form remaining-string]."
[s]
(defn- parse-next-loop [pos]
(let [[form new-pos] (read-form s pos)]
(if (and (struct? form) (= :jolt/skip (form :jolt/type)))
(parse-next-loop new-pos)
[form (string/slice s new-pos)])))
(parse-next-loop 0))
(defn parse-all-positioned
"Parse every top-level form of source, returning an array of [form line]
where line is the 1-based source line the form starts on. parse-next eats
leading trivia itself, so the form's start line is the running newline
count plus the newlines in the trivia (whitespace, commas, ; comments)
ahead of it. (jolt-2o7.4)"
[source &opt file]
(def out @[])
(var s source)
(var line 1)
(while (> (length (string/trim s)) 0)
# newlines in the leading trivia belong BEFORE the form's line
(var i 0)
(def n (length s))
(var scanning true)
(while (and scanning (< i n))
(def c (in s i))
(cond
(= c (chr "\n")) (do (++ line) (++ i))
(or (= c (chr " ")) (= c (chr "\t")) (= c (chr "\r")) (= c (chr ","))) (++ i)
(= c (chr ";")) (while (and (< i n) (not= (in s i) (chr "\n"))) (++ i))
(set scanning false)))
# list-form positions recorded during this parse-next are relative to s;
# tell the reader the slice base so they land absolute (jolt-fqy)
(when track-positions (set-pos-base! (- (length source) (length s))))
(def [form rest*]
(try (parse-next s)
([err fib]
(if (reader-error? err)
# err's :pos is relative to the remaining slice — rebase onto the
# full source so line:col are absolute (jolt-2o7.5)
(error (format-reader-error
{:jolt/type :jolt/reader-error :msg (err :msg)
:pos (+ (- (length source) (length s)) (err :pos))}
source file))
(propagate err fib)))))
(def consumed (- (length s) (length rest*)))
(def form-line line)
# count newlines inside the consumed chunk past the trivia
(loop [j :range [i consumed]]
(when (= (in s j) (chr "\n")) (++ line)))
(set s rest*)
(when (not (nil? form)) (array/push out [form form-line])))
out)
(defn parse-all-spans
"Parse every top-level form of source, returning an array of
[form abs-start abs-end] where abs-start is the byte offset of the form's
first non-trivia char and abs-end is just past it (leading whitespace/comment
trivia is excluded). Lets a caller reconstruct source by slicing verbatim —
used by `jolt uberscript`'s dead-code elimination to drop a defn's exact byte
span without re-printing (so formatting and reader macros survive)."
[source &opt file]
(def out @[])
(var s source)
(while (> (length (string/trim s)) 0)
(def base (- (length source) (length s)))
(var i 0)
(def n (length s))
(var scanning true)
(while (and scanning (< i n))
(def c (in s i))
(cond
(or (= c (chr "\n")) (= c (chr " ")) (= c (chr "\t")) (= c (chr "\r")) (= c (chr ","))) (++ i)
(= c (chr ";")) (while (and (< i n) (not= (in s i) (chr "\n"))) (++ i))
(set scanning false)))
(def [form rest*]
(try (parse-next s)
([err fib]
(if (reader-error? err)
(error (format-reader-error
{:jolt/type :jolt/reader-error :msg (err :msg)
:pos (+ base (err :pos))}
source file))
(propagate err fib)))))
(def consumed (- (length s) (length rest*)))
(set s rest*)
(when (not (nil? form)) (array/push out [form (+ base i) (+ base consumed)])))
out)

View file

@ -1,499 +0,0 @@
# Regex support for Jolt, compiled to Janet's PEG engine.
#
# Path A: a real regex parser -> AST -> PEG grammar with continuation passing
# and position-based group captures. This gives genuine backtracking (greedy and
# lazy) that threads through capturing groups, plus capturing groups returned in
# Clojure's [whole g1 g2 ...] order, lookahead, anchors, classes, flags.
#
# Supported: literals, `.`, char classes `[...]`/`[^...]` (ranges, POSIX,
# `\d \w \s` etc.), quantifiers `* + ? {n} {n,} {n,m}` (greedy + lazy `?`),
# groups `(...)` (numbered), non-capturing `(?:...)`, lookahead `(?=...)`/`(?!...)`,
# alternation `|`, anchors `^ $ \b \B`, escapes, inline flag `(?i)`, and
# single-digit backreferences `\1`..`\9` (a backreferenced group matches
# possessively — no backtracking back into it; rare in practice).
# Not supported: lookbehind, named groups (rare; documented).
(defn- chr [s] (get s 0))
(defn regex? [x] (and (table? x) (= :jolt/regex (x :jolt/type))))
# ============================================================
# Parser: regex source -> AST
# ============================================================
# AST nodes (structs): {:op ...}
# :char {:b byte :ci bool} :any {:dotall bool}
# :class {:peg <frag> :neg bool} :pred {:peg <frag>}
# :seq {:items [...]} :alt {:items [...]}
# :star/:plus/:quest {:item ast :greedy bool}
# :rep {:item ast :min n :max m-or-nil :greedy bool}
# :group {:n num :item ast} :ncgroup {:item ast}
# :look {:neg bool :item ast}
# :anchor {:kind :start/:end/:wordb/:nwordb}
(defn- lower-b [b] (if (and (>= b 65) (<= b 90)) (+ b 32) b))
(defn- upper-b [b] (if (and (>= b 97) (<= b 122)) (- b 32) b))
(defn- pred-frag [c]
(case c
(chr "d") '(range "09")
(chr "D") '(if-not (range "09") 1)
(chr "w") '(choice (range "az" "AZ" "09") (set "_"))
(chr "W") '(if-not (choice (range "az" "AZ" "09") (set "_")) 1)
(chr "s") '(set " \t\n\r\f\v")
(chr "S") '(if-not (set " \t\n\r\f\v") 1)
nil))
# Unicode property classes \p{...} (jolt-xlp), mapped onto the byte PEGs:
# ASCII exactly; any high byte (>= 0x80, i.e. inside a UTF-8 sequence) counts
# as a LETTER byte — so ^\p{L}+$ accepts UTF-8 words, while \p{N}/\p{Z}
# stay ASCII-only. Lu/Ll are ASCII (case is byte-based throughout this
# engine). Unknown property names error at compile.
(defn- prop-frag [name]
(case name
"L" '(choice (range "az" "AZ") (range "\x80\xFF"))
"Lu" '(range "AZ")
"Ll" '(range "az")
"N" '(range "09")
"Nd" '(range "09")
"Z" '(set " ")
"Zs" '(set " ")
"P" '(set "!\"#%&'()*,-./:;?@[\\]_{}")
"Ps" '(set "([{")
"Pe" '(set ")]}")
"Alpha" '(choice (range "az" "AZ") (range "\x80\xFF"))
"Digit" '(range "09")
nil))
# At s[i] = backslash: if this is \p{Name} / \P{Name}, return [peg-frag end-i]
# (end-i past the closing brace), else nil.
(defn- parse-prop [s i]
(def pc (get s (+ i 1)))
(when (and (or (= pc (chr "p")) (= pc (chr "P")))
(= (get s (+ i 2)) (chr "{")))
(def close (string/find "}" s (+ i 3)))
(unless close (error "regex: unterminated \\p{...}"))
(def nm (string/slice s (+ i 3) close))
(def frag (prop-frag nm))
(unless frag (error (string "regex: unsupported property class \\p{" nm "}")))
[(if (= pc (chr "P")) ~(if-not ,frag 1) frag) (+ close 1)]))
(defn- esc-byte [c]
(case c
(chr "n") 10 (chr "t") 9 (chr "r") 13 (chr "f") 12 (chr "v") 11 (chr "0") 0
c))
# Parse a [...] character class body, returns a PEG fragment + negation flag.
(defn- parse-class [s start ci]
(var i start)
(def neg (and (< i (length s)) (= (s i) (chr "^"))))
(when neg (++ i))
(def alts @[])
(while (and (< i (length s)) (not= (s i) (chr "]")))
(cond
# POSIX class [:alpha:] etc.
(and (= (s i) (chr "[")) (< (+ i 1) (length s)) (= (s (+ i 1)) (chr ":")))
(let [close (string/find ":]" s i)
name (string/slice s (+ i 2) close)]
(array/push alts (case name
"alpha" '(range "az" "AZ")
"digit" '(range "09")
"alnum" '(range "az" "AZ" "09")
"space" '(set " \t\n\r\f\v")
"upper" '(range "AZ")
"lower" '(range "az")
'(set "")))
(set i (+ close 2)))
# escape inside class — \p{...} first (multi-char), then 2-char escapes
(= (s i) (chr "\\"))
(if-let [pr (parse-prop s i)]
(do (array/push alts (pr 0)) (set i (pr 1)))
(let [c (s (+ i 1)) p (pred-frag c)]
(if p (array/push alts p)
(array/push alts ~(set ,(string/from-bytes (esc-byte c)))))
(set i (+ i 2))))
# range a-z
(and (< (+ i 2) (length s)) (= (s (+ i 1)) (chr "-")) (not= (s (+ i 2)) (chr "]")))
(do
(if ci
(array/push alts ~(range ,(string/from-bytes (lower-b (s i)) (lower-b (s (+ i 2)))
(upper-b (s i)) (upper-b (s (+ i 2))))))
(array/push alts ~(range ,(string/from-bytes (s i) (s (+ i 2))))))
(set i (+ i 3)))
# single char
(do
(if ci
(array/push alts ~(set ,(string/from-bytes (lower-b (s i)) (upper-b (s i)))))
(array/push alts ~(set ,(string/from-bytes (s i)))))
(++ i))))
(def frag (if (= 1 (length alts)) (alts 0) ~(choice ,;alts)))
[(if neg ~(if-not ,frag 1) frag) (+ i 1)]) # i is at ], skip it
(var parse-alt nil)
(defn- parse-atom [st]
# returns ast; advances (st :pos)
(def s (st :s))
(def c (s (st :pos)))
(def ci (st :ci))
(cond
(= c (chr "("))
(let [nx (if (< (+ (st :pos) 1) (length s)) (s (+ (st :pos) 1)) 0)]
(if (= nx (chr "?"))
(let [k (s (+ (st :pos) 2))]
(cond
(= k (chr ":")) (do (+= (st :pos) 3)
(let [inner (parse-alt st)] (+= (st :pos) 1) {:op :ncgroup :item inner}))
(= k (chr "=")) (do (+= (st :pos) 3)
(let [inner (parse-alt st)] (+= (st :pos) 1) {:op :look :neg false :item inner}))
(= k (chr "!")) (do (+= (st :pos) 3)
(let [inner (parse-alt st)] (+= (st :pos) 1) {:op :look :neg true :item inner}))
# inline flags (?i) / (?m) / (?im:...) etc.
(do
(var j (+ (st :pos) 2))
(var seti false)
(var setm false)
(while (and (< j (length s)) (not= (s j) (chr ")")) (not= (s j) (chr ":")))
(when (= (s j) (chr "i")) (set seti true))
(when (= (s j) (chr "m")) (set setm true))
(++ j))
(if (= (s j) (chr ":"))
(do (set (st :pos) (+ j 1))
(def savedci (st :ci))
(def savedml (st :ml))
(when seti (set (st :ci) true))
(when setm (set (st :ml) true))
(def inner (parse-alt st))
(set (st :ci) savedci)
(set (st :ml) savedml)
(+= (st :pos) 1)
{:op :ncgroup :item inner})
(do (set (st :pos) (+ j 1)) # (?i) / (?m) — flag for rest of pattern
(when seti (set (st :ci) true))
(when setm (set (st :ml) true))
{:op :seq :items @[]})))))
# capturing group
(let [n (++ (st :ngroup))]
(+= (st :pos) 1)
(let [inner (parse-alt st)]
(+= (st :pos) 1) # skip )
{:op :group :n n :item inner}))))
(= c (chr "["))
(let [[frag np] (parse-class s (+ (st :pos) 1) ci)]
(set (st :pos) np)
{:op :class :peg frag})
(= c (chr "."))
(do (++ (st :pos)) {:op :any :dotall (st :dotall)})
(= c (chr "\\"))
(if-let [pr (parse-prop s (st :pos))]
(do (set (st :pos) (pr 1)) {:op :pred :peg (pr 0)})
(let [nc (s (+ (st :pos) 1)) p (pred-frag nc)]
(+= (st :pos) 2)
(cond
p {:op :pred :peg p}
(= nc (chr "b")) {:op :anchor :kind :wordb}
(= nc (chr "B")) {:op :anchor :kind :nwordb}
# \1..\9 backreference (single digit) — record the referenced group so
# the emitter knows to tag-capture its text
(and (>= nc (chr "1")) (<= nc (chr "9")))
(let [n (- nc (chr "0"))] (put (st :refs) n true) {:op :backref :n n})
{:op :char :b (esc-byte nc) :ci ci})))
(= c (chr "^")) (do (++ (st :pos)) {:op :anchor :kind :start})
(= c (chr "$")) (do (++ (st :pos)) {:op :anchor :kind :end})
(do (++ (st :pos)) {:op :char :b c :ci ci})))
(defn- parse-quant [st]
(def atom (parse-atom st))
(def s (st :s))
(if (>= (st :pos) (length s))
atom
(let [q (s (st :pos))]
# called after advancing past the quantifier char: a trailing `?` -> lazy
(defn lazy? []
(if (and (< (st :pos) (length s)) (= (s (st :pos)) (chr "?")))
(do (++ (st :pos)) false) # lazy -> greedy=false
true))
(cond
(= q (chr "*")) (do (++ (st :pos)) {:op :star :item atom :greedy (lazy?)})
(= q (chr "+")) (do (++ (st :pos)) {:op :plus :item atom :greedy (lazy?)})
(= q (chr "?")) (do (++ (st :pos)) {:op :quest :item atom :greedy (lazy?)})
(= q (chr "{"))
(let [close (string/find "}" s (st :pos))
spec (string/slice s (+ (st :pos) 1) close)
comma (string/find "," spec)]
(set (st :pos) (+ close 1))
(def greedy (lazy?))
(if comma
(let [lo (scan-number (string/slice spec 0 comma))
hs (string/slice spec (+ comma 1))
hi (if (= 0 (length hs)) nil (scan-number hs))]
{:op :rep :item atom :min lo :max hi :greedy greedy})
{:op :rep :item atom :min (scan-number spec) :max (scan-number spec) :greedy greedy}))
atom))))
(defn- parse-seq [st]
(def s (st :s))
(def items @[])
(while (and (< (st :pos) (length s))
(not= (s (st :pos)) (chr "|"))
(not= (s (st :pos)) (chr ")")))
(array/push items (parse-quant st)))
(if (= 1 (length items)) (items 0) {:op :seq :items items}))
(set parse-alt (fn [st]
(def branches @[(parse-seq st)])
(def s (st :s))
(while (and (< (st :pos) (length s)) (= (s (st :pos)) (chr "|")))
(++ (st :pos))
(array/push branches (parse-seq st)))
(if (= 1 (length branches)) (branches 0) {:op :alt :items branches})))
(defn- parse [source]
(def st @{:s source :pos 0 :ngroup 0 :ci false :ml false :dotall false :refs @{}})
(def ast (parse-alt st))
[ast (st :ngroup) (st :ml) (st :refs)])
# ============================================================
# Emit: AST -> PEG grammar (continuation passing)
# ============================================================
(def- word-frag '(choice (range "az" "AZ" "09") (set "_")))
(defn- char-peg [b ci]
(if (and ci (not= (lower-b b) (upper-b b)))
~(set ,(string/from-bytes (lower-b b) (upper-b b)))
~(set ,(string/from-bytes b))))
(defn- make-emitter [grammar ml refs]
(var ctr 0)
(defn fresh [] (++ ctr) (keyword (string "r" ctr)))
(var emit nil)
(set emit (fn [ast k]
(case (ast :op)
:char ~(sequence ,(char-peg (ast :b) (ast :ci)) ,k)
:any ~(sequence ,(if (ast :dotall) 1 ~(if-not "\n" 1)) ,k)
:class ~(sequence ,(ast :peg) ,k)
:pred ~(sequence ,(ast :peg) ,k)
:seq (do (var acc k) (var i (dec (length (ast :items))))
(while (>= i 0) (set acc (emit (in (ast :items) i) acc)) (-- i)) acc)
:alt (let [kr (fresh)]
(put grammar kr k)
~(choice ,;(map (fn [a] (emit a (keyword kr))) (ast :items))))
:star (let [r (fresh)]
(put grammar r (if (ast :greedy)
~(choice ,(emit (ast :item) (keyword r)) ,k)
~(choice ,k ,(emit (ast :item) (keyword r)))))
(keyword r))
:plus (let [r (fresh)]
(put grammar r (if (ast :greedy)
~(choice ,(emit (ast :item) (keyword r)) ,k)
~(choice ,k ,(emit (ast :item) (keyword r)))))
(emit (ast :item) (keyword r)))
:quest (if (ast :greedy)
~(choice ,(emit (ast :item) k) ,k)
~(choice ,k ,(emit (ast :item) k)))
:rep (let [lo (ast :min) hi (ast :max) item (ast :item) greedy (ast :greedy)]
# desugar: lo required, then (hi-lo) optional, or star if hi is nil
(var tail (if (nil? hi)
(let [r (fresh)]
(put grammar r (if greedy
~(choice ,(emit item (keyword r)) ,k)
~(choice ,k ,(emit item (keyword r)))))
(keyword r))
# Each optional level is its OWN grammar rule that
# references the previous level by name. Inlining instead
# (choice (emit item acc) acc) duplicates `acc` per level,
# so {0,n} built a structure the PEG compiler expanded to
# 2^n nodes and hung (jolt-3xur). Naming keeps it O(n).
(do (def kr (fresh)) (put grammar kr k)
(var acc (keyword kr)) (var c (- hi lo))
(while (> c 0)
(let [r (fresh)]
(put grammar r (if greedy ~(choice ,(emit item acc) ,acc)
~(choice ,acc ,(emit item acc))))
(set acc (keyword r)))
(-- c))
acc)))
(var acc tail) (var c lo)
(while (> c 0) (set acc (emit item acc)) (-- c))
acc)
:group (let [n (ast :n)]
(if (and refs (refs n))
# backreferenced group: also capture its text under tag :gN so a
# later \N can (backmatch). The item runs with just the end-marker
# as its continuation, so it matches POSSESSIVELY — backtracking
# INTO a backreferenced group isn't supported (rare; the usual
# (x)...\1 patterns don't need it).
(let [tag (keyword (string "g" n))]
~(sequence (/ (position) ,(fn [p] [n :s p]))
(<- ,(emit (ast :item) ~(/ (position) ,(fn [p] [n :e p]))) ,tag)
,k))
~(sequence (/ (position) ,(fn [p] [n :s p]))
,(emit (ast :item)
~(sequence (/ (position) ,(fn [p] [n :e p])) ,k)))))
:backref ~(sequence (backmatch ,(keyword (string "g" (ast :n)))) ,k)
:ncgroup (emit (ast :item) k)
:look (if (ast :neg)
~(sequence (not ,(emit (ast :item) 0)) ,k)
~(sequence (not (not ,(emit (ast :item) 0))) ,k))
:anchor (case (ast :kind)
:start (if ml
~(sequence (choice (not (look -1 1)) (look -1 "\n")) ,k)
~(sequence (not (look -1 1)) ,k))
:end (if ml
~(sequence (choice (not 1) (not (not "\n"))) ,k)
~(sequence (not 1) ,k))
:wordb ~(sequence (choice (sequence (look -1 ,word-frag) (not ,word-frag))
(sequence (not (look -1 ,word-frag)) (not (not ,word-frag))))
,k)
:nwordb ~(sequence (choice (sequence (look -1 ,word-frag) (not (not ,word-frag)))
(sequence (not (look -1 ,word-frag)) (not ,word-frag)))
,k)
~(sequence 0 ,k))
(error (string "regex emit: unhandled op " (ast :op))))))
emit)
(defn compile-regex [source]
(def [ast ngroups ml refs] (parse source))
(def grammar @{})
(def emit (make-emitter grammar ml refs))
# group 0 = whole match: mark start, body, mark end
(def body (emit ast ~(sequence (/ (position) ,(fn [p] [0 :e p])) 0)))
(put grammar :main ~(sequence (/ (position) ,(fn [p] [0 :s p])) ,body))
(def gstruct (table/to-struct grammar))
# anchored variant for re-matches: whole input must be consumed
(def anchored (table/to-struct (merge grammar {:main ~(sequence ,(grammar :main) -1)})))
@{:jolt/type :jolt/regex
:source source
:ngroups ngroups
:peg (peg/compile gstruct)
:anchored (peg/compile anchored)})
(defn re-pattern [source]
(if (regex? source) source (compile-regex source)))
# ============================================================
# Matching
# ============================================================
(defn- marks->groups [marks s ngroups]
# marks: array of [n :s pos] / [n :e pos]; build [g0 g1 ... gN], slicing input
(def starts (array/new-filled (+ ngroups 1)))
(def ends (array/new-filled (+ ngroups 1)))
(each m marks
# backreferenced groups also leave a tagged TEXT capture (a string) on the
# stack — skip those; only the [n :s/:e pos] position tuples carry group spans
(when (indexed? m)
(let [n (m 0)]
(if (= (m 1) :s) (put starts n (m 2)) (put ends n (m 2))))))
(def groups (array/new-filled (+ ngroups 1)))
(var n 0)
(while (<= n ngroups)
(if (and (not (nil? (in starts n))) (not (nil? (in ends n))))
(put groups n (string/slice s (in starts n) (in ends n)))
(put groups n nil))
(++ n))
groups)
(defn- match-at [re s start]
# returns groups array or nil
(def marks (peg/match (re :peg) s start))
(if marks (marks->groups marks s (re :ngroups)) nil))
(defn- groups->result [groups ngroups]
# 0 groups -> whole-match string; else tuple [whole g1 ...]
(if (= ngroups 0) (in groups 0) (tuple/slice groups)))
(defn re-find [re s]
(def re (re-pattern re))
(var result nil) (var pos 0)
(while (and (nil? result) (<= pos (length s)))
(def g (match-at re s pos))
(if g (set result (groups->result g (re :ngroups))) (++ pos)))
result)
(defn re-matches [re s]
(def re (re-pattern re))
(def marks (peg/match (re :anchored) s 0))
(if marks (groups->result (marks->groups marks s (re :ngroups)) (re :ngroups)) nil))
(defn re-seq [re s]
(def re (re-pattern re))
(def out @[]) (var pos 0)
(while (<= pos (length s))
(def g (match-at re s pos))
(if g
(let [whole (in g 0)]
(array/push out (groups->result g (re :ngroups)))
(set pos (+ pos (max 1 (length whole)))))
(++ pos)))
# Clojure's re-seq is nil (not an empty seq) when there are no matches, so
# `(if-let [m (re-seq ...)] ...)` works — an empty seq would be truthy.
(if (= 0 (length out)) nil out))
(defn re-split [re s &opt limit]
# limit (Java Pattern.split n>0): at most `limit` parts — stop after limit-1
# splits and keep the rest of the string as the final unsplit part. nil/<=0
# splits at every match.
(def re (re-pattern re))
(def lim (if (and limit (> limit 0)) limit nil))
(def out @[]) (var pos 0) (var last 0)
(while (and (<= pos (length s)) (or (nil? lim) (< (length out) (dec lim))))
(def g (match-at re s pos))
(if (and g (> (length (in g 0)) 0))
(do (array/push out (string/slice s last pos))
(set pos (+ pos (length (in g 0))))
(set last pos))
(++ pos)))
(array/push out (string/slice s last))
out)
(defn- expand-replacement [repl groups]
# $0 / $1 ... substitution in replacement string
(def buf @"") (var i 0)
(while (< i (length repl))
(let [c (repl i)]
(if (and (= c (chr "$")) (< (+ i 1) (length repl)) (>= (repl (+ i 1)) 48) (<= (repl (+ i 1)) 57))
(let [n (- (repl (+ i 1)) 48)]
(when (and (< n (length groups)) (in groups n)) (buffer/push-string buf (in groups n)))
(set i (+ i 2)))
(do (buffer/push-string buf (string/from-bytes c)) (++ i)))))
(string buf))
(defn- replacement-for
"One match's replacement text. A string replacement gets $N expansion; a FN
replacement (Clojure: fn of the match — string, or [whole g1 ...] when the
pattern has groups) is called and its result used literally."
[replacement g ngroups]
(cond
(string? replacement) (expand-replacement replacement g)
(or (function? replacement) (cfunction? replacement))
(string (replacement (groups->result g ngroups)))
(string replacement)))
(defn re-replace-all [re s replacement]
(def re (re-pattern re))
(def buf @"") (var pos 0) (var last 0)
(while (<= pos (length s))
(def g (match-at re s pos))
(if (and g (> (length (in g 0)) 0))
(do (buffer/push-string buf (string/slice s last pos))
(buffer/push-string buf (replacement-for replacement g (re :ngroups)))
(set pos (+ pos (length (in g 0))))
(set last pos))
(++ pos)))
(buffer/push-string buf (string/slice s last))
(string buf))
(defn re-replace-first [re s replacement]
(def re (re-pattern re))
(var pos 0) (var done nil)
(while (and (nil? done) (<= pos (length s)))
(def g (match-at re s pos))
(if (and g (> (length (in g 0)) 0))
(set done [pos g])
(++ pos)))
(if done
(let [[p g] done]
(string (string/slice s 0 p)
(replacement-for replacement g (re :ngroups))
(string/slice s (+ p (length (in g 0))))))
s))

View file

@ -1,35 +0,0 @@
# Bakes the Clojure stdlib (.clj/.cljc under src/jolt/clojure and src/jolt/jolt)
# into the image at build time, so the runtime can load clojure.string, jolt.http,
# jolt.nrepl, etc. from any directory — not just when run from the repo.
#
# `sources` is built at module-load time. During `jpm build` that's the build
# (cwd = repo), so the map is captured into the image and frozen; in the shipped
# binary the files are never read from disk. Running from source rebuilds it.
(defn- relpath->ns [rel]
# string/replace-all takes the string LAST, so thread with ->>
(->> rel (string/replace-all "/" ".") (string/replace-all "_" "-")))
(defn- strip-ext [name]
(cond
(string/has-suffix? ".cljc" name) (string/slice name 0 (- (length name) 5))
(string/has-suffix? ".clj" name) (string/slice name 0 (- (length name) 4))
name))
(defn- collect [root prefix acc]
(when (os/stat root)
(each e (os/dir root)
(def p (string root "/" e))
(cond
(= :directory (os/stat p :mode)) (collect p (string prefix e "/") acc)
(or (string/has-suffix? ".clj" e) (string/has-suffix? ".cljc" e))
(put acc (relpath->ns (string prefix (strip-ext e))) (slurp p)))))
acc)
(def sources
(let [acc @{}]
(collect "src/jolt/clojure" "clojure/" acc)
(collect "src/jolt/jolt" "jolt/" acc)
# Portable Clojure core (analyzer/IR/…): jolt-core/jolt/foo.clj -> jolt.foo
(collect "jolt-core" "" acc)
acc))

View file

@ -1,11 +0,0 @@
# Jolt value layer (types)
#
# AGGREGATOR (jolt-bvek phase 5a): the value/var/ns/ctx/protocol concerns now
# live in sibling modules, loaded here in dependency order and re-exported
# (:export true) so every consumer keeps its single `(use ./types)`.
(import ./types_symbols :prefix "" :export true)
(import ./types_var :prefix "" :export true)
(import ./types_ns :prefix "" :export true)
(import ./types_ctx :prefix "" :export true)
(import ./types_protocols :prefix "" :export true)

View file

@ -1,241 +0,0 @@
# Jolt value layer — Context (+ inst/uuid values)
# Extracted from types.janet (jolt-bvek phase 5a split).
(use ./types_symbols)
(use ./types_var)
(use ./types_ns)
# ============================================================
# Context
# ============================================================
(defn ctx-find-ns
"Find or create a namespace in the context by name symbol."
[ctx ns-sym]
(let [env (ctx :env)
namespaces (env :namespaces)]
(or (get namespaces ns-sym)
(let [ns (make-ns ns-sym)]
(put namespaces ns-sym ns)
ns))))
# Instant value: an immutable tagged struct keyed by epoch milliseconds, so
# equality and map-key hashing are by INSTANT (offset-normalized): two #inst
# literals with different offsets denoting the same moment are =.
(defn make-inst [ms]
{:jolt/type :jolt/inst :ms ms})
(defn parse-inst
"Parse an RFC3339 timestamp with Clojure's partial defaults
(yyyy[-MM[-dd[Thh[:mm[:ss[.fff]]]]]][Z|+hh:mm|-hh:mm]) to an inst value.
Errors on a malformed timestamp."
[ts]
(def pat (peg/compile
~(sequence
(capture (repeat 4 :d)) # year
(opt (sequence "-" (capture (repeat 2 :d)))) # month
(opt (sequence "-" (capture (repeat 2 :d)))) # day
(opt (sequence "T" (capture (repeat 2 :d)) # hour
(opt (sequence ":" (capture (repeat 2 :d)) # min
(opt (sequence ":" (capture (repeat 2 :d)) # sec
(opt (sequence "." (capture (some :d)))))))))) # frac
(opt (choice (capture "Z")
(sequence (capture (set "+-")) (capture (repeat 2 :d))
":" (capture (repeat 2 :d)))))
-1)))
(def m (peg/match pat ts))
(when (nil? m) (error (string "Unrecognized #inst timestamp: " ts)))
# captures arrive positionally; classify by shape: digits runs + offset parts.
(var year nil) (var month 1) (var day 1)
(var hh 0) (var mm 0) (var ss 0) (var frac "0")
(var off-sign nil) (var off-h 0) (var off-m 0)
(var i 0)
(def fields @[:year :month :day :hh :mm :ss])
(var fi 0)
(while (< i (length m))
(def part (in m i))
(cond
(= part "Z") nil
(or (= part "+") (= part "-"))
(do (set off-sign part)
(set off-h (scan-number (in m (+ i 1))))
(set off-m (scan-number (in m (+ i 2))))
(+= i 2))
# fractional seconds arrive right after :ss was filled
(and (>= fi 6))
(set frac part)
(do
(def v (scan-number part))
(case (in fields fi)
:year (set year v) :month (set month v) :day (set day v)
:hh (set hh v) :mm (set mm v) :ss (set ss v))
(++ fi)))
(++ i))
(when (nil? year) (error (string "Unrecognized #inst timestamp: " ts)))
(def base-s (os/mktime {:year year :month (- month 1) :month-day (- day 1)
:hours hh :minutes mm :seconds ss}))
# fractional part -> milliseconds (truncate beyond 3 digits)
(def frac3 (string/slice (string frac "000") 0 3))
(def ms-frac (scan-number frac3))
(def off-s (* (if (= off-sign "-") -1 1) (+ (* off-h 3600) (* off-m 60))))
(make-inst (- (+ (* base-s 1000) ms-frac) (* off-s 1000))))
(defn inst->rfc3339
"Canonical print form: yyyy-MM-ddThh:mm:ss.fff-00:00 (UTC, like Clojure)."
[inst]
(def ms (inst :ms))
(def s (math/floor (/ ms 1000)))
(def frac (- ms (* s 1000)))
(def d (os/date s))
(string/format "%04d-%02d-%02dT%02d:%02d:%02d.%03d-00:00"
(d :year) (+ 1 (d :month)) (+ 1 (d :month-day))
(d :hours) (d :minutes) (d :seconds) frac))
# UUID value: an immutable tagged struct. Lowercased at construction so
# equality and map-key hashing are case-insensitive by value (struct equality),
# matching Clojure (java.util.UUID equality / cljs UUID).
(defn make-uuid [s]
{:jolt/type :jolt/uuid :str (string/ascii-lower s)})
(defn make-ctx
"Create a new evaluation context.
(make-ctx) — empty context with 'user namespace
(make-ctx opts) — context with initial namespaces from opts
opts may contain:
:namespaces — struct of {ns-symbol → {sym → value, ...}, ...}"
[&opt opts]
(default opts nil)
(let [compile? (if opts (get opts :compile?) false)
# Direct-linking (call-site/unit property, like Clojure). :aot-core?
# (default true; JOLT_AOT_CORE=0 disables) compiles the core tiers +
# compiler with direct-linking on. :direct-linking? is the per-unit flag
# the back end reads while emitting; it defaults to the user-code setting
# (off unless opted in) and load-core-overlay! flips it on around core.
aot-core? (let [o (if opts (get opts :aot-core?) nil)]
(if (nil? o) (not (= "0" (os/getenv "JOLT_AOT_CORE"))) o))
# Macro expanders compile in EVERY mode (macros are ordinary compiled
# fns, as in Clojure) — including interpret mode, where evaluation stays
# interpreted but expansion runs native. :compile-macros? false (or
# JOLT_INTERPRET_MACROS=1) opts back into the fully-interpreted oracle.
compile-macros? (let [o (if opts (get opts :compile-macros?) nil)]
(if (nil? o)
(not (= "1" (os/getenv "JOLT_INTERPRET_MACROS")))
o))
env @{:namespaces @{}
:class->opts @{}
:current-ns "user"
:compile? compile?
:aot-core? aot-core?
:compile-macros? compile-macros?
# User-code direct-linking default (off unless opted in), the
# apples-to-apples analog of jank's -Odirect-call / Clojure's
# :direct-linking. JOLT_DIRECT_LINK=1 turns it on for user units;
# this is also the gate the inline pass reads (a call is only
# inline-safe when the callee won't be redefined). load-core-overlay!
# still flips core to :aot-core? around the tiers and restores this.
:direct-linking? (let [o (if opts (get opts :direct-linking?) nil)]
(if (nil? o) (= "1" (os/getenv "JOLT_DIRECT_LINK")) o))
# Inline + scalar-replacement passes (jolt-87f). OFF for all of init
# (core load + self-hosted compiler recompile), so core/bootstrap
# compile exactly as before; api/init flips it on to the user
# direct-linking setting AFTER init, so only opted-in user code
# inlines. The inline pass also reads this (via host/inline-enabled?).
:inline? false
# Ordered roots searched (after the stdlib) to resolve a namespace
# to a .clj/.cljc file. jolt-core holds the portable Clojure layer
# (analyzer/IR/core); deps.edn resolution appends dep src dirs.
:source-paths @["jolt-core" "src/jolt"]
:type-registry @{}
:data-readers (let [dr @{}]
(put dr (keyword "#inst") (fn [s] (parse-inst s)))
(put dr (keyword "#uuid") (fn [s] (make-uuid s)))
dr)}
# create the user namespace via a partial context
_ (ctx-find-ns {:env env} "user")]
# initialize from opts
(when opts
(when-let [ns-opts (get opts :namespaces)]
(loop [[ns-sym mappings] :pairs ns-opts]
(let [ns (ctx-find-ns {:env env} ns-sym)]
(loop [[sym val] :pairs mappings]
(ns-intern ns sym val))))))
{:jolt/type :jolt/context
:env env}))
(defn ctx?
"Check if x is a Jolt Context."
[x]
(and (struct? x) (= :jolt/context (x :jolt/type))))
(defn ctx-env
"Return the env atom from the context."
[ctx]
(ctx :env))
(defn ctx-current-ns
"Get the current namespace symbol."
[ctx]
(get (ctx :env) :current-ns))
(defn ctx-set-current-ns
"Set the current namespace symbol. Also keeps the *ns* dynamic var's root in
sync (the var table is cached on the env by install-stateful-fns! — one table
put on this hot path, no ns lookup chain)."
[ctx ns-sym]
(put (ctx :env) :current-ns ns-sym)
(when-let [v (get (ctx :env) :ns-var)]
(put v :root (ctx-find-ns ctx ns-sym))))
(defn all-ns
"Return a list of all namespaces in the context."
[ctx]
(let [namespaces (get (ctx :env) :namespaces)
result @[]]
(loop [[_ ns] :pairs namespaces]
(array/push result ns))
result))
(defn remove-ns
"Remove a namespace from the context by name string."
[ctx ns-name]
(put (get (ctx :env) :namespaces) ns-name nil) nil)
(defn create-ns
"Create a new namespace."
[ctx ns-name]
(ctx-find-ns ctx ns-name))
(defn the-ns
"Return the current namespace object."
[ctx]
(ctx-find-ns ctx (ctx-current-ns ctx)))
(defn ns-interns
"Return the map of all interned vars in the current namespace."
[ctx]
(let [ns (ctx-find-ns ctx (ctx-current-ns ctx))]
(ns :mappings)))
(defn ns-aliases
"Return the alias map of the current namespace."
[ctx]
(let [ns (ctx-find-ns ctx (ctx-current-ns ctx))]
(ns :aliases)))
(defn find-var
"Resolve a symbol to a var in the current context.
Looks in current namespace first, then clojure.core."
[ctx sym-s]
(let [name (sym-s :name)
ns-sym (sym-s :ns)]
(if ns-sym
(let [ns (ctx-find-ns ctx ns-sym)]
(ns-find ns name))
(let [current-ns (ctx-find-ns ctx (ctx-current-ns ctx))
v (ns-find current-ns name)]
(if v v
(let [core-ns (ctx-find-ns ctx "clojure.core")]
(ns-find core-ns name)))))))
# ============================================================

View file

@ -1,100 +0,0 @@
# Jolt value layer — Namespace
# Extracted from types.janet (jolt-bvek phase 5a split).
(use ./types_symbols)
(use ./types_var)
# ============================================================
# Namespace
# ============================================================
(defn make-ns
"Create a new namespace.
(make-ns name) — empty namespace
name is a symbol struct {:jolt/type :symbol ...}"
[name]
(struct
:jolt/type :jolt/namespace
:name name
:mappings @{}
:imports @{}
:aliases @{}))
(defn ns?
"Check if x is a Jolt Namespace."
[x]
(and (or (struct? x) (table? x)) (= :jolt/namespace (x :jolt/type))))
(defn ns-name
"Return the name symbol of a namespace."
[ns]
(ns :name))
(defn ns-map
"Return the mappings table (symbol → var) for a namespace."
[ns]
(ns :mappings))
(defn ns-intern
"Find or create a var named by sym in namespace ns, setting root binding to val if given.
(ns-intern ns sym) — find or create unbound var
(ns-intern ns sym val) — find or create with root binding"
[ns sym &opt val]
(default val nil)
(let [mappings (ns :mappings)
existing (get mappings sym)]
(if existing
(do
(when (not (nil? val))
(bind-root existing val))
existing)
# Store the namespace *name*, not the ns table: a back-pointer to the ns
# would make the var cyclic (ns -> mappings -> var -> ns), and the compiler
# embeds var cells as constants, which can't be cyclic.
(let [v (make-var sym val {:ns (get ns :name) :name sym})]
(put mappings sym v)
v))))
(defn ns-find
"Find a var by symbol in the namespace. Returns nil if not found."
[ns sym]
(get (ns :mappings) sym))
(defn ns-unmap
"Remove a mapping by symbol from the namespace."
[ns sym]
(put (ns :mappings) sym nil))
(defn ns-resolve
"Resolve a symbol in a namespace. Looks in own mappings first,
then aliases. Returns the var or nil."
[ns sym]
(or (ns-find ns sym)
(let [qualified? (sym? sym)]
(when qualified?
# qualified symbol: look up via alias (string-keyed store)
(let [alias-ns (get (ns :aliases) (sym :ns))]
(when alias-ns
(ns-find alias-ns (sym :name))))))))
(defn ns-import
"Add an import to the namespace. class-name is local symbol, fq-class-name is the full qualified name."
[ns class-name fq-class-name]
(put (ns :imports) class-name fq-class-name))
(defn ns-import-lookup
"Look up an import in the namespace. Returns the full qualified name or nil."
[ns class-name]
(get (ns :imports) class-name))
(defn ns-add-alias
"Add an alias: alias-name (string) -> target ns NAME (string). The ONE alias
store (jolt-ark): resolution and ns-aliases both read it; :imports is class
imports only."
[ns alias-name target-ns-name]
(put (ns :aliases) alias-name target-ns-name))
(defn ns-alias-lookup
"The target ns NAME for alias-name, or nil."
[ns alias-name]
(get (ns :aliases) alias-name))

View file

@ -1,156 +0,0 @@
# Jolt value layer — protocol/type registry + shape-records
# Extracted from types.janet (jolt-bvek phase 5a split).
(use ./types_symbols)
(use ./types_var)
(use ./types_ns)
(use ./types_ctx)
# Protocol type registry
# ============================================================
(defn register-protocol-method
"Register a protocol method implementation for a type."
[ctx type-tag protocol-name method-name fn]
(let [env (ctx :env)
registry (get env :type-registry)
type-impls (or (get registry type-tag)
(do (put registry type-tag @{}) (get registry type-tag)))
proto-impls (or (get type-impls protocol-name)
(do (put type-impls protocol-name @{}) (get type-impls protocol-name)))]
(put proto-impls method-name fn)
# Bump the registry generation so any dispatch cache keyed on it invalidates.
(put env :type-registry-gen (+ 1 (or (get env :type-registry-gen) 0)))))
(defn find-protocol-method
"Find a protocol method implementation for a type."
[ctx type-tag protocol-name method-name]
(let [registry (get (ctx :env) :type-registry)
type-impls (get registry type-tag)]
(when type-impls
(let [proto-impls (get type-impls protocol-name)]
(when proto-impls
(get proto-impls method-name))))))
(defn find-method-any-protocol
"Find a method implementation for a type, searching every protocol it
implements (dot calls name the method but not the protocol)."
[ctx type-tag method-name]
(let [type-impls (get (get (ctx :env) :type-registry) type-tag)]
(when type-impls
(var r nil)
(eachp [_ proto-impls] type-impls
(when (nil? r) (set r (get proto-impls method-name))))
r)))
(defn type-satisfies?
"Check if a type satisfies a protocol."
[ctx type-tag protocol-name]
(let [registry (get (ctx :env) :type-registry)
type-impls (get registry type-tag)]
(if (and type-impls (get type-impls protocol-name)) true false)))
# --- shape records (hidden classes, jolt-t34) -------------------------------
# A "shape record" is a cheap fixed-layout representation for a map literal
# whose keys are a known compile-time set (e.g. a vec3 {:r :g :b}). It is a
# Janet tuple [SHAPE v0 v1 ...] where SHAPE is an interned descriptor struct
# {:jolt/shape KEYS :idx {k->pos}}. Construction is a tuple (≈2x cheaper than a
# struct), const-keyword lookup compiles to an index, and general map ops below
# recognize it via shape-rec? and treat it as a map — so it is transparent
# wherever it flows. Created only by the backend when JOLT_SHAPE is on and the
# inference proves the shape; the runtime support here is always present so a
# shape value is handled correctly regardless.
(def- shape-cache @{}) # canonical-keys-tuple -> interned shape descriptor
# Canonical key order, OWNED BY THE RUNTIME (jolt-t34): every site that builds or
# reads a shape (shape-for, emit-map, emit-kw-lookup, build-map-literal) derives
# the layout from this one function, so they always agree regardless of what
# order the inference passed the keys in. Sorted by the keys' jdn print form —
# deterministic and total across keywords/strings/numbers/bools.
(defn shape-sort [ks]
(sort (array ;ks) (fn [a b] (< (string/format "%j" a) (string/format "%j" b)))))
(defn shape-for
"Interned shape descriptor for a key set. Keys are canonicalized internally,
so callers need not pre-sort and any permutation yields the same descriptor."
[keyv]
(def sk (tuple ;(shape-sort keyv)))
(or (get shape-cache sk)
(let [idx @{}]
(var i 0) (each k sk (put idx k i) (++ i))
(def desc (struct :jolt/shape sk :idx (table/to-struct idx)))
(put shape-cache sk desc)
desc)))
(defn shape-rec? [x]
(and (tuple? x) (> (length x) 0)
(struct? (in x 0)) (not (nil? (in (in x 0) :jolt/shape)))))
(defn shape-keys [rec] ((in rec 0) :jolt/shape))
(defn shape-get [rec k default]
(def desc (in rec 0))
(def pos (get (desc :idx) k))
(cond
(not (nil? pos)) (in rec (+ pos 1))
# records respond to the virtual :jolt/deftype key with their type tag, so
# every existing (get obj :jolt/deftype) dispatch site keeps working
(and (= k :jolt/deftype) (desc :type)) (desc :type)
default))
(defn shape-assoc [rec k v]
# assoc on a known key keeps the layout. A new key: a record keeps its type
# and grows a slot (Clojure records stay records when extended); a plain
# shape-rec falls back to a struct.
(def desc (in rec 0))
(def pos (get (desc :idx) k))
(cond
(not (nil? pos)) (let [a (array ;rec)] (put a (+ pos 1) v) (tuple ;a))
(desc :type)
(let [new-keys (array ;(desc :jolt/shape) k) idx @{}]
(var i 0) (each kk new-keys (put idx kk i) (++ i))
(def ndesc (struct :jolt/shape (tuple ;new-keys) :idx (table/to-struct idx) :type (desc :type)))
(def out @[ndesc])
(each kk (desc :jolt/shape) (array/push out (shape-get rec kk nil)))
(array/push out v)
(tuple ;out))
(let [t @{}] (each kk (desc :jolt/shape) (put t kk (shape-get rec kk nil))) (put t k v) (table/to-struct t))))
(defn shape-count [rec] (- (length rec) 1))
(defn shape-contains? [rec k] (not (nil? (get ((in rec 0) :idx) k))))
# a struct snapshot of a shape-rec — the reusable bridge for ops that already
# handle structs (dissoc, vals, seq, equality, print, ...) without per-op code
(defn shape->struct [rec]
(def desc (in rec 0)) (def t @{})
(each kk (desc :jolt/shape) (put t kk (in rec (+ 1 (get (desc :idx) kk)))))
(table/to-struct t))
# --- records as shapes (jolt-t34 R3) ----------------------------------------
# A user record (deftype/defrecord) is a shape-rec whose descriptor ALSO carries
# :type (the type tag). Field layout is the DECLARED field order (not sorted),
# so the positional ->Name constructor maps args to slots directly. The
# descriptor is interned per type tag, so all instances of a type share it.
# record-tag unifies the type accessor over both the new shape-rec records and
# the table form still used for reified protocol objects.
(def- record-desc-cache @{})
(defn record-desc [type-tag field-keys]
"Build a record descriptor (interned in declared field order) for the given
key set. Not cached — used for records extended past their declared fields."
(let [idx @{}]
(var i 0) (each k field-keys (put idx k i) (++ i))
(struct :jolt/shape (tuple ;field-keys) :idx (table/to-struct idx) :type type-tag)))
# Interned per (type-tag, field-keys): keying on the tag alone would hand back a
# STALE descriptor after a record is redefined with different fields (a REPL
# redefine, or two same-named records in different test cases) — the new instance
# would carry the old layout. Old instances keep their own descriptor and stay
# valid; new ones get the new layout. (jolt-t34)
(defn record-shape-for [type-tag field-keys]
(def ck (tuple type-tag (tuple ;field-keys)))
(or (get record-desc-cache ck)
(let [desc (record-desc type-tag field-keys)]
(put record-desc-cache ck desc)
desc)))
(defn make-record [type-tag field-keys args]
(def out @[(record-shape-for type-tag field-keys)])
(var i 0) (each k field-keys (array/push out (in args i)) (++ i))
(tuple ;out))
(defn record-tag
"The deftype/record type tag of x, or nil. Covers shape-rec records (descriptor
:type) and the table form (reified objects, :jolt/deftype)."
[x]
(cond
(and (tuple? x) (> (length x) 0) (struct? (in x 0))) (get (in x 0) :type)
(and (table? x) (get x :jolt/deftype)) (get x :jolt/deftype)))

View file

@ -1,41 +0,0 @@
# Jolt value layer — characters + symbol helpers
# Extracted from types.janet (jolt-bvek phase 5a split).
# Jolt Types
# Core types for the Clojure-on-Janet interpreter.
#
# Types:
# JoltVar — mutable container with metadata (like Clojure Var)
# JoltNamespace — namespace with symbol→var mappings and imports
# JoltContext — evaluation context (env atom, namespaces)
#
# Symbols are represented as {:jolt/type :symbol :ns <string-or-nil> :name <string>}
# as produced by the reader.
# Characters are {:jolt/type :jolt/char :ch <codepoint>}, distinct from strings.
(defn make-char [code] {:jolt/type :jolt/char :ch code})
(def- char-named @{"newline" 10 "space" 32 "tab" 9 "return" 13
"formfeed" 12 "backspace" 8 "newpage" 12 "nul" 0})
(defn char-from-name
"Resolve a reader char-literal name (\\a, \\newline, \\uNNNN, \\oNNN) to a char value."
[name]
(cond
(= 1 (length name)) (make-char (in name 0))
(get char-named name) (make-char (get char-named name))
(and (> (length name) 1) (= (in name 0) (get "u" 0)))
(make-char (scan-number (string "16r" (string/slice name 1))))
(and (> (length name) 1) (= (in name 0) (get "o" 0)))
(make-char (scan-number (string "8r" (string/slice name 1))))
(error (string "Unsupported character: \\" name))))
# ============================================================
# Symbol helpers
# ============================================================
(defn sym?
"Check if x is a Jolt symbol struct."
[x]
(and (struct? x) (= :symbol (x :jolt/type))))

View file

@ -1,179 +0,0 @@
# Jolt value layer — Var
# Extracted from types.janet (jolt-bvek phase 5a split).
(use ./types_symbols)
# ============================================================
# Var
# ============================================================
# Dynamic-var binding stack. Stored fiber-locally (via Janet's dyn), so that
# concurrent go blocks — each a Janet fiber — don't interleave each other's
# dynamic bindings, and a go block conveys the bindings in effect when it was
# spawned (see snapshot-bindings/install-bindings). Each fiber lazily gets its
# own array on first use.
(defn cur-binding-stack []
(or (dyn :jolt/binding-stack)
(let [s @[]] (setdyn :jolt/binding-stack s) s)))
(defn push-thread-bindings
"Push a frame of dynamic var bindings. Takes a struct of var→value."
[bindings]
(array/push (cur-binding-stack) bindings))
(defn pop-thread-bindings
"Pop the most recent frame of dynamic var bindings."
[]
(array/pop (cur-binding-stack)))
(defn snapshot-bindings
"Shallow copy of the current binding stack (frames are immutable value maps).
Captured by a go block at spawn time for binding conveyance."
[]
(array/slice (cur-binding-stack)))
(defn install-bindings
"Install a snapshot as this fiber's binding stack (a fresh copy, so the
fiber's own push/pop/var-set don't mutate the snapshot's frames array)."
[snap]
(setdyn :jolt/binding-stack (array/slice snap)))
(defn make-var
"Create a new Jolt Var.
(make-var name) — unbound var
(make-var name init-val) — var with root binding
(make-var name init-val meta) — var with root and metadata
name is a symbol struct {:jolt/type :symbol ...}"
[name &opt init-val meta]
(default init-val nil)
(default meta nil)
(let [m (if meta meta {:name name})
result @{:jolt/type :jolt/var
:name name
:root init-val
:meta m
# Generation: bumped on every root change (redefinition). Call
# sites / dispatch caches keyed on this can detect a redef and
# invalidate; direct-linked (sealed) sites can detect staleness.
:gen 0
:dynamic (if meta (get meta :dynamic) false)
:macro (if meta (get meta :macro) false)
:ns (if meta (get meta :ns) nil)}]
result))
(defn var?
"Check if x is a Jolt Var."
[x]
(and (table? x) (= :jolt/var (x :jolt/type))))
(defn var-dynamic?
"Check if var is marked :dynamic."
[v]
(v :dynamic))
(defn var-macro?
"Check if var is marked :macro."
[v]
(v :macro))
(defn var-name
"Return the symbol name of the var."
[v]
(v :name))
(defn var-meta
"Return the metadata of the var."
[v]
(v :meta))
(defn var-ns
"Return the namespace of the var."
[v]
(v :ns))
(defn var-get
"Deref the var. If the var is dynamic and has a thread-local binding, return that.
Otherwise return the root binding."
[v]
# Fast path: no dynamic bindings are active (the common case — the stack is
# only non-empty inside a `binding` block), so the value is just the root. This
# is the hot path for every global deref; skip building the walk otherwise.
(def bs (cur-binding-stack))
(if (= 0 (length bs))
(v :root)
# walk binding stack top-down for this var
(do
(var result nil)
(var i (dec (length bs)))
(while (>= i 0)
(let [frame (in bs i)
val (get frame v)]
(if (not (nil? val))
(do
(set result (if (var? val) (var-get val) val))
(set i -1))
(-- i))))
(if (not (nil? result)) result (v :root)))))
(defn var-set
"Set a var's value. If the var has a thread-local binding on the stack, update
the innermost frame that binds it (matching Clojure, where var-set targets the
current binding); otherwise set the root."
[v val]
(def bs (cur-binding-stack))
(var i (dec (length bs)))
(var done false)
(while (and (not done) (>= i 0))
(let [frame (in bs i)]
(if (not (nil? (get frame v)))
(do (put bs i (merge frame {v val})) (set done true))
(-- i))))
(unless done (do (put v :root val) (put v :gen (+ 1 (or (v :gen) 0)))))
val)
(defn alter-var-root
"Atomically alter the root binding of v by applying f to current value plus args."
[v f & args]
(let [new-val (f (v :root) ;args)]
(put v :root new-val)
(put v :gen (+ 1 (or (v :gen) 0)))
new-val))
(defn alter-meta!
"Atomically update a var's metadata via (apply f args)."
[v f & args]
(let [new-meta (apply f (var-meta v) args)]
(put v :meta new-meta)
new-meta))
(defn reset-meta!
"Reset a var's metadata to the given value."
[v meta]
(put v :meta meta)
meta)
(defn with-meta
"Return a new var with updated metadata. The original var is unchanged."
[v meta]
# build new meta as a table first (to allow adding keys), then convert
(let [new-meta-table (merge @{} (v :meta) meta)
# convert to struct by extracting all keys
new-meta (table/to-struct new-meta-table)]
@{:jolt/type :jolt/var
:name (v :name)
:root (v :root)
:meta new-meta
:gen (or (v :gen) 0)
:dynamic (v :dynamic)
:macro (v :macro)
:ns (v :ns)}))
(defn bind-root
"Set the root binding and bump the var's generation (the redefinition
chokepoint: def, ns-intern-with-val, and the root-set paths all route here)."
[v val]
(put v :root val)
(put v :gen (+ 1 (or (v :gen) 0)))
val)

View file

@ -1,50 +0,0 @@
# Performance baseline for the clojure.core migration (jolt-1j0).
#
# Times representative core operations end-to-end (compile path) so a phase that
# moves fns from native Janet to the self-hosted Clojure overlay can be checked
# for regressions. Same programs before/after a phase -> relative delta is the
# migration's perf impact. Run: JOLT_BENCH=1 janet test/bench/core-bench.janet
# (skipped under `jpm test` — it asserts nothing; see main).
#
# Each program carries its own internal iteration so the measured work dominates
# parse/compile overhead. Reports the min of N runs (least noisy).
(import ../../src/jolt/api :as api)
(def runs 5)
(def benches
[[:fib "(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) (fib 28)"]
[:seq-pipe "(loop [i 0 a 0] (if (< i 300) (recur (inc i) (+ a (reduce + 0 (map inc (filter even? (range 200)))))) a))"]
[:reduce "(loop [i 0 a 0] (if (< i 2000) (recur (inc i) (+ a (reduce + 0 (range 500)))) a))"]
[:into-vec "(loop [i 0 a 0] (if (< i 1000) (recur (inc i) (+ a (count (into [] (map inc (range 100)))))) a))"]
[:map-build "(loop [i 0 a 0] (if (< i 2000) (recur (inc i) (+ a (count (reduce (fn [m k] (assoc m k k)) {} (range 50))))) a))"]
[:map-read "(let [m (zipmap (range 100) (range 100))] (loop [i 0 a 0] (if (< i 5000) (recur (inc i) (+ a (get m (mod i 100) 0))) a)))"]
[:str-join "(loop [i 0 a 0] (if (< i 1000) (recur (inc i) (+ a (count (apply str (map str (range 100)))))) a))"]
[:hof "(loop [i 0 a 0] (if (< i 2000) (recur (inc i) (+ a (reduce + 0 (map (comp inc inc) (range 200))))) a))"]])
(defn time-bench [ctx src]
(var best math/inf)
(for _ 0 runs
(def t0 (os/clock))
(api/load-string ctx src)
(def dt (* 1000 (- (os/clock) t0)))
(when (< dt best) (set best dt)))
best)
(defn main [&]
# `jpm test` recurses test/ and would run this every gate, but it's a manual
# perf tool that asserts nothing (just reports timings) — so skip it unless
# opted in with JOLT_BENCH=1. Keeps ~35s of unasserted benchmark work out of
# the correctness gate (same pattern as suite-worker's no-arg no-op).
(unless (os/getenv "JOLT_BENCH")
(print "core-bench: SKIP (set JOLT_BENCH=1 to run)")
(os/exit 0))
(def ctx (api/init {:compile? true}))
(print "bench (compile mode), min of " runs " runs, ms:")
(var total 0)
(each [name src] benches
(def ms (time-bench ctx src))
(+= total ms)
(printf " %-10s %8.2f ms" name ms))
(printf " %-10s %8.2f ms" "TOTAL" total))

View file

@ -1,49 +0,0 @@
# jolt-mn9o — atom watches + validators on Chez. The Chez atom record carries
# watches (alist) + validator slots; swap!/reset! validate-then-set-then-notify;
# add-watch/remove-watch/set-validator!/get-validator are native (post-prelude.ss
# re-asserts them over the overlay's ref-put!-based versions).
#
# janet test/chez/_atomwatch.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
# [expr expected] — :throws asserts a non-zero exit (validator rejection); the
# exception message text is not compared.
(def cases
[["(let [a (atom 0) seen (atom 0)] (add-watch a :k (fn [k r o n] (reset! seen 1))) (reset! a 5) @seen)" "1"]
["(let [a (atom 0) seen (atom 0)] (add-watch a :k (fn [k r o n] (swap! seen inc))) (remove-watch a :k) (reset! a 5) @seen)" "0"]
["(let [a (atom 0) log (atom [])] (add-watch a :k (fn [k r o n] (swap! log conj [o n]))) (reset! a 1) (reset! a 2) @log)" "[[0 1] [1 2]]"]
["(let [a (atom 0)] (set-validator! a number?) (reset! a 5) @a)" "5"]
["(let [a (atom 5)] (set-validator! a pos?) (swap! a inc) @a)" "6"]
["(let [a (atom 0)] (set-validator! a number?) (fn? (get-validator a)))" "true"]
["(let [a (atom 0)] (nil? (get-validator a)))" "true"]
["(let [a (atom 0)] (set-validator! a pos?) (reset! a -1))" :throws]
["(let [a (atom 5)] (set-validator! a pos?) (swap! a (fn [_] -1)) @a)" :throws]
["(let [a (atom 0) c (atom 0)] (add-watch a :k (fn [k r o n] (swap! c inc))) (swap! a inc) (swap! a inc) @c)" "2"]])
(defn run-capture [bin expr]
(def proc (os/spawn [bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (string/trim (if err (string err) ""))])
(var pass 0)
(def fails @[])
(each [expr expected] cases
(def [code got err] (run-capture jolt-bin expr))
(cond
(= expected :throws)
(if (not= code 0) (++ pass)
(array/push fails [expr (string "expected throw; exit " code)]))
(not= code 0) (array/push fails [expr (string "exit " code "; err: " err)])
(= got expected) (++ pass)
(array/push fails [expr (string "want `" expected "`, got `" got "`")])))
(printf "\n_atomwatch parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [e m] fails (printf " FAIL %s\n %s" e m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,59 +0,0 @@
# jolt-13zk — bare class-name tokens + (class x) on Chez. A class name (String,
# Keyword, File...) evaluates to its JVM canonical-name STRING — the same value
# (class instance) returns — so (= String (class "x")) holds and a (defmethod m
# String ...) keys against a (class ...) dispatch. host-class.ss ports
# Scalar (class x) returns the JVM-style class-name symbol.
# Collection (class ...) is host-taxonomy-dependent and is NOT compared here.
#
# janet test/chez/_class.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(def cases
[# --- bare class tokens evaluate to canonical strings ---
["String" "java.lang.String"]
["Number" "java.lang.Number"]
["Keyword" "clojure.lang.Keyword"]
["File" "java.io.File"]
["Exception" "java.lang.Exception"]
["MapEntry" "clojure.lang.MapEntry"]
# --- (class x) on scalars matches core-class ---
["(class 1)" "java.lang.Number"]
["(class 1.5)" "java.lang.Number"]
["(class \"s\")" "java.lang.String"]
["(class :k)" "clojure.lang.Keyword"]
["(class true)" "java.lang.Boolean"]
["(class false)" "java.lang.Boolean"]
["(class nil)" ""]
# --- token <-> class equality ---
["(= String (class \"abc\"))" "true"]
["(= Number (class 7))" "true"]
["(= String (class 7))" "false"]
# --- defmulti dispatch on class ---
["(do (defmulti cm (fn [x] (class x))) (defmethod cm String [x] :str) (cm \"a\"))" ":str"]
["(do (defmulti cn (fn [x] (class x))) (defmethod cn nil [x] :nil) (defmethod cn String [x] :str) (cn nil))" ":nil"]
["(do (defmulti cn (fn [x] (class x))) (defmethod cn nil [x] :nil) (defmethod cn String [x] :str) (cn \"z\"))" ":str"]])
(defn run-capture [bin expr]
(def proc (os/spawn [bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (string/trim (if err (string err) ""))])
(var pass 0)
(def fails @[])
(each [expr expected] cases
(def [code got err] (run-capture jolt-bin expr))
(cond
(not= code 0) (array/push fails [expr (string "exit " code "; err: " err)])
(= got expected) (++ pass)
(array/push fails [expr (string "want `" expected "`, got `" got "`")])))
(printf "\n_class parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [e m] fails (printf " FAIL %s\n %s" e m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,68 +0,0 @@
# jolt-kuic — the `.` special form + `.-field` field-access desugar on Chez. The
# analyzer lowers (. target member arg*) and (.-field target) to a :host-call;
# the Chez emit routes a non-shimmed :host-call through record-method-dispatch,
# which dot-forms.ss extends with field access + map/vector member dispatch.
# Each case carries its expected printed value.
#
# janet test/chez/_dotform.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(def cases
[# strings: method calls via the String surface
["(. \"HI\" toLowerCase)" "hi"]
["(. \"abc\" length)" "3"]
["(. \"abc\" toUpperCase)" "ABC"]
# vectors / maps: collection interop (count/nth/get/containsKey)
["(. [1 2 3] count)" "3"]
["(. [10 20 30] nth 1)" "20"]
["(. {:a 1 :b 2} count)" "2"]
["(. {:a 1 :b 2} get :b)" "2"]
["(. {:a 1} containsKey :a)" "true"]
["(. {:count 99} count)" "1"]
# map member: stored fn called with self (+ args), else field value
["(. {:value 41 :describe (fn [self] (str \"v=\" (:value self)))} describe)" "v=41"]
["(. {:greet (fn [self n] (str \"Hello \" n))} greet \"Alice\")" "Hello Alice"]
["(. {:value 41} value)" "41"]
# field access via .-field head
["(.-value {:value 41})" "41"]
["(.-x {:x 7 :y 9})" "7"]
# field access via (. obj -field)
["(. {:value 41} -value)" "41"]
# records: .-field reads a field, .method dispatches the protocol method
["(do (defrecord Rf [x]) (.-x (->Rf 7)))" "7"]
["(do (defrecord Rg [a b]) (.-b (->Rg 1 2)))" "2"]
["(do (defprotocol Greet (hi [_])) (defrecord Rh [nm] Greet (hi [_] (str \"hi \" nm))) (. (->Rh \"x\") hi))" "hi x"]
# universal object-methods on a non-record map win over a field lookup
["(try (throw (ex-info \"bad\" {})) (catch Throwable e (.getMessage e)))" "bad"]
["(try (throw (ex-info \"bad\" {:k 1})) (catch Throwable e (.getMessage e)))" "bad"]
["(try (throw \"boom\") (catch Throwable e (.getMessage e)))" "boom"]
["(try (throw (Exception. \"boom\")) (catch Throwable e (.getMessage e)))" "boom"]
["(try (throw (IllegalArgumentException. \"bad\")) (catch Exception e (.getMessage e)))" "bad"]
["(.equals \"a\" \"a\")" "true"]
["(.equals \"a\" \"b\")" "false"]
["(. {:value 41 :describe (fn [self] (str \"v=\" (:value self)))} describe)" "v=41"]])
(defn run-capture [bin expr]
(def proc (os/spawn [bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (string/trim (if err (string err) ""))])
(var pass 0)
(def fails @[])
(each [expr expected] cases
(def [code got err] (run-capture jolt-bin expr))
(cond
(not= code 0) (array/push fails [expr (string "exit " code "; err: " err)])
(= got expected) (++ pass)
(array/push fails [expr (string "want `" expected "`, got `" got "`")])))
(printf "\n_dotform parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [e m] fails (printf " FAIL %s\n %s" e m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,75 +0,0 @@
# jolt-2o7x — dynamic var binding (binding / with-bindings* / var-set /
# thread-bound? / with-local-vars / with-redefs / bound-fn* / get-thread-bindings).
# Expectations are the JVM-canonical values. TDD harness: bin/joltc
# -e per case, last non-empty line == expected.
#
# janet test/chez/_dynbind.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(def cases
[# --- binding: install / restore / seen across a fn call ---
["binding rebinds" "(do (def ^:dynamic *bx* 10) (binding [*bx* 99] *bx*))" "99"]
["binding restores" "(do (def ^:dynamic *by* 10) (binding [*by* 99] *by*) *by*)" "10"]
["binding seen by fn" "(do (def ^:dynamic *bz* 0) (defn rdz [] *bz*) (binding [*bz* 7] (rdz)))" "7"]
["binding both: vec" "(do (def ^:dynamic *bv* 1) [(binding [*bv* 2] *bv*) *bv*])" "[2 1]"]
["nested binding" "(do (def ^:dynamic *bn* 1) (binding [*bn* 2] (binding [*bn* 3] *bn*)))" "3"]
["nested binding outer" "(do (def ^:dynamic *bo* 1) (binding [*bo* 2] (binding [*bo* 3] nil) *bo*))" "2"]
# (a macro reading a dynamic var — corpus 606/607 — needs top-level defmacro
# in the -e path, which the Chez driver can't compile yet; out of scope here.)
# --- var-set inside a binding targets the frame; restored on exit ---
["var-set in binding" "(do (def ^:dynamic *z* 1) (binding [*z* 0] (var-set (var *z*) 5) *z*))" "5"]
["var-set frame restores" "(do (def ^:dynamic *zr* 1) (binding [*zr* 0] (var-set (var *zr*) 5)) *zr*)" "1"]
# --- thread-bound? ---
["thread-bound? unbound" "(do (def ^:dynamic *tb* 1) (thread-bound? (var *tb*)))" "false"]
["thread-bound? in scope" "(do (def ^:dynamic *tc* 1) (binding [*tc* 2] (thread-bound? (var *tc*))))" "true"]
# --- with-bindings* / bound-fn* / get-thread-bindings ---
# Binding frames look up by var cell identity, so a var-keyed binding map
# resolves: the Clojure-correct value is 7.
["with-bindings*" "(do (def ^:dynamic *wb* 1) (with-bindings* {(var *wb*) 7} (fn [] *wb*)))" "7"]
["bound-fn* conveys" "(do (def ^:dynamic *cf* 1) (let [g (binding [*cf* 3] (bound-fn* (fn [] *cf*)))] (g)))" "3"]
["get-thread-bindings" "(do (def ^:dynamic *gb* 1) (binding [*gb* 9] (get (get-thread-bindings) (var *gb*))))" "9"]
# --- with-local-vars ---
["local-var get" "(with-local-vars [x 1] (var-get x))" "1"]
["local-var set" "(with-local-vars [x 1] (var-set x 2) (var-get x))" "2"]
["local-var two" "(with-local-vars [a 1 b 2] [(var-get a) (var-get b)])" "[1 2]"]
["local-var as value" "(with-local-vars [x 0] (let [bump (fn [v] (var-set v (+ 5 (var-get v))))] (bump x) (var-get x)))" "5"]
["local-var init outer" "(let [y 3] (with-local-vars [x y] (var-get x)))" "3"]
["local-var body result" "(with-local-vars [x 1] :done)" ":done"]
# --- with-redefs ---
["with-redefs rebinds" "(do (defn wrf [] 1) (with-redefs [wrf (fn [] 42)] (wrf)))" "42"]
["with-redefs restores" "(do (defn wrg [] 1) (with-redefs [wrg (fn [] 42)]) (wrg))" "1"]
["with-redefs on throw" "(do (defn wrh [] 1) (try (with-redefs [wrh (fn [] 42)] (throw (ex-info \"x\" {}))) (catch :default e nil)) (wrh))" "1"]
["with-redefs-fn" "(do (defn wri [] 1) (with-redefs-fn {(var wri) (fn [] 42)} (fn [] (wri))))" "42"]
# --- alter-var-root ---
["alter-var-root" "(do (def av 1) (alter-var-root (var av) inc) av)" "2"]])
(defn run-capture [expr]
(def proc (os/spawn [jolt-bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (if err (string err) "")])
(var pass 0)
(def fails @[])
(each [label expr expected] cases
(def [code got err] (run-capture expr))
(cond
(not= code 0) (array/push fails [label (string "exit " code "; err: " (string/trim err))])
(= got expected) (++ pass)
(array/push fails [label (string "want `" expected "`, got `" got "`")])))
(printf "\n_dynbind parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [l m] fails (printf " FAIL [%s] %s" l m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,74 +0,0 @@
# jolt-at0a (inc X) — #inst / #uuid literals + java.time formatting on Chez.
# #inst lowers (analyzer :inst node) to a jinst value (RFC3339 ms, partial
# defaults + offsets); #uuid to a juuid; the java.time shim (DateTimeFormatter/
# Instant/ZoneId/LocalDateTime/FormatStyle/Locale/Date.
# Reader data-readers and statics emit the runtime value
# (host/chez/inst-time.ss).
#
#
# janet test/chez/_insttime.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(def cases
[# --- #inst reading + identity ---
["(inst? #inst \"2020-01-01T00:00:00Z\")" "true"]
["(inst-ms #inst \"1970-01-01T00:00:01Z\")" "1000"]
["(inst-ms #inst \"1970-01-01T00:00:00.123Z\")" "123"]
["(inst-ms #inst \"2020-01-01T00:00:00Z\")" "1577836800000"]
["(inst-ms* #inst \"1970-01-01T00:00:00Z\")" "0"]
["(some? #inst \"2020-01-01T00:00:00Z\")" "true"]
["(str (type #inst \"2020-01-01T00:00:00Z\"))" ":jolt/inst"]
# --- partial timestamps + offsets (value equality by instant) ---
["(= #inst \"2020\" #inst \"2020-01-01T00:00:00.000Z\")" "true"]
["(= #inst \"2020-03\" #inst \"2020-03-01T00:00:00Z\")" "true"]
["(= #inst \"2020-03-15\" #inst \"2020-03-15T00:00:00Z\")" "true"]
["(= #inst \"2020-01-01T01:00:00+01:00\" #inst \"2020-01-01T00:00:00Z\")" "true"]
["(= #inst \"2019-12-31T23:00:00-01:00\" #inst \"2020-01-01T00:00:00Z\")" "true"]
["(= #inst \"2020-01-01T00:00:00Z\" #inst \"2020-01-01T00:00:01Z\")" "false"]
# --- map key + pr-str round trip ---
["(get {#inst \"2020-01-01T00:00:00Z\" :v} #inst \"2020-01-01T00:00:00.000Z\")" ":v"]
["(pr-str #inst \"2020-01-01T00:00:00Z\")" "#inst \"2020-01-01T00:00:00.000-00:00\""]
["(str #inst \"2020-01-01T00:00:00Z\")" "2020-01-01T00:00:00.000-00:00"]
# --- #uuid ---
["(uuid? #uuid \"550e8400-e29b-41d4-a716-446655440000\")" "true"]
["(= #uuid \"550E8400-E29B-41D4-A716-446655440000\" #uuid \"550e8400-e29b-41d4-a716-446655440000\")" "true"]
["(pr-str #uuid \"550e8400-e29b-41d4-a716-446655440000\")" "#uuid \"550e8400-e29b-41d4-a716-446655440000\""]
# --- java.util.Date / java.time.Instant interop ---
["(.getTime #inst \"1970-01-01T00:00:00Z\")" "0"]
["(instance? java.util.Date #inst \"2020-01-01T00:00:00Z\")" "true"]
["(instance? java.sql.Timestamp #inst \"2020-01-01T00:00:00Z\")" "false"]
["(.toEpochMilli (Instant/ofEpochMilli 1234))" "1234"]
["(instance? java.time.Instant (Instant/ofEpochMilli 0))" "true"]
["(> (.toEpochMilli (Instant/now)) 1500000000000)" "true"]
["(instance? LocalDateTime (-> #inst \"2020-03-05T13:45:30Z\" (.toInstant) (.atZone (ZoneId/systemDefault)) (.toLocalDateTime)))" "true"]
# --- DateTimeFormatter pattern engine ---
["(.format (DateTimeFormatter/ofPattern \"yyyy-MM-dd\") #inst \"2020-03-05T13:45:30Z\")" "2020-03-05"]
["(boolean (re-matches #\"[A-Z][a-z]{2} \\d{1,2}, 2020 \\d{1,2}:\\d{2} [AP]M\" (.format (DateTimeFormatter/ofPattern \"MMM d, yyyy h:mm a\") #inst \"2020-03-05T13:45:30Z\")))" "true"]
["(boolean (re-matches #\"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\" (.format DateTimeFormatter/ISO_LOCAL_DATE_TIME #inst \"2020-03-05T13:45:30Z\")))" "true"]
["(string? (.format (DateTimeFormatter/ofLocalizedDate FormatStyle/MEDIUM) #inst \"2020-03-05T13:45:30Z\"))" "true"]
["(string? (.format (.withLocale (DateTimeFormatter/ofPattern \"yyyy\") (java.util.Locale. \"en\")) #inst \"2020-01-01T00:00:00Z\"))" "true"]])
(defn run-capture [bin expr]
(def proc (os/spawn [bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (string/trim (if err (string err) ""))])
(var pass 0)
(def fails @[])
(each [expr expected] cases
(def [code got err] (run-capture jolt-bin expr))
(cond
(not= code 0) (array/push fails [expr (string "exit " code "; err: " err)])
(= got expected) (++ pass)
(array/push fails [expr (string "want `" expected "`, got `" got "`")])))
(printf "\n_insttime parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [e m] fails (printf " FAIL %s\n %s" e m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,61 +0,0 @@
# jolt-yyud — clojure.java.io/file + java.io.File interop + slurp/spit/flush/
# file-seq on Chez. A File is a path-backed jfile record (instance? java.io.File,
# str -> path, the File method surface). This is a Chez-native implementation.
# Reader-coupled cases (line-seq, slurp over a reader, toURL) are deferred to jolt-at0a.
#
#
# janet test/chez/_io.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(defn io [body] (string "(do (require (quote [clojure.java.io :as io])) " body ")"))
(def cases
[# --- File construction + method surface ---
[(io "(str (io/file \"/a/b\"))") "/a/b"]
[(io "(str (io/file \"/a\" \"b\"))") "/a/b"]
[(io "(.getName (io/file \"/a/b/c.txt\"))") "c.txt"]
[(io "(.getPath (io/file \"/a\" \"b\"))") "/a/b"]
[(io "(.isDirectory (io/file \"docs\"))") "true"]
[(io "(.isFile (io/file \"project.janet\"))") "true"]
[(io "(.isFile (io/file \"docs\"))") "false"]
[(io "(.exists (io/file \"/no/such/path/xyz\"))") "false"]
[(io "(.exists (io/file \"project.janet\"))") "true"]
# --- instance? + type ---
[(io "(instance? java.io.File (io/file \"/a/b\"))") "true"]
[(io "(instance? java.io.File \"/a/b\")") "false"]
[(io "(str (type (io/file \"/a\")))") ":jolt/file"]
# --- file-seq ---
[(io "(every? (fn [f] (instance? java.io.File f)) (file-seq (io/file \"docs\")))") "true"]
[(io "(pos? (count (filter (fn [f] (.isFile f)) (file-seq (io/file \"docs\")))))") "true"]
["(do (require (quote [clojure.string :as s])) (boolean (some (fn [p] (s/ends-with? p \"project.janet\")) (file-seq \".\"))))" "true"]
# --- slurp / spit / flush ---
["(string? (slurp \"project.janet\"))" "true"]
["(do (spit \"/tmp/jolt-io-test.txt\" \"hello\") (slurp \"/tmp/jolt-io-test.txt\"))" "hello"]
["(do (spit \"/tmp/jolt-io-test.txt\" \"a\") (spit \"/tmp/jolt-io-test.txt\" \"b\" :append true) (slurp \"/tmp/jolt-io-test.txt\"))" "ab"]
["(flush)" ""]
[(io "(string? (slurp (io/file \"project.janet\")))") "true"]])
(defn run-capture [bin expr]
(def proc (os/spawn [bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (string/trim (if err (string err) ""))])
(var pass 0)
(def fails @[])
(each [expr expected] cases
(def [code got err] (run-capture jolt-bin expr))
(cond
(not= code 0) (array/push fails [expr (string "exit " code "; err: " err)])
(= got expected) (++ pass)
(array/push fails [expr (string "want `" expected "`, got `" got "`")])))
(printf "\n_io parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [e m] fails (printf " FAIL %s\n %s" e m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,61 +0,0 @@
# jolt-at0a (inc W) — reader-coupled io deferred from inc V (jolt-yyud):
# clojure.java.io/reader (a StringReader over slurp/string/char[]/File/path),
# char-array, File.toURL/.toURI (-> a java.net.URL jhost), slurp draining a
# StringReader, and with-open's __close seam over both jhost readers and plain
# :close maps. All Chez-native (host/chez/io.ss); no analyzer change. Reader/edn
# runtime read (clojure.edn/read over a PushbackReader) stays jolt-r8ku.
#
#
# janet test/chez/_ioreader.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(defn io [body] (string "(do (require (quote [clojure.java.io :as io])) " body ")"))
(def cases
[# --- char-array ---
["(str (char-array \"abc\"))" "(\\a \\b \\c)"]
["(.read (StringReader. (apply str (char-array \"Qz\"))))" "81"]
# --- io/reader: char[] / string-reader passthrough / path ---
[(io "(.read (io/reader (char-array \"abc\")))") "97"]
[(io "(.read (io/reader (StringReader. \"k\")))") "107"]
[(io "(slurp (io/reader (char-array \"xyz\")))") "xyz"]
[(io "(string? (slurp (io/reader \"project.janet\")))") "true"]
# --- File.toURL / .toURI ---
[(io "(.toString (.toURL (io/file \"/tmp/x\")))") "file:/tmp/x"]
[(io "(.toURI (io/file \"/tmp/x\"))") "file:/tmp/x"]
[(io "(.getPath (.toURL (io/file \"/tmp/x\")))") "/tmp/x"]
[(io "(.getAbsolutePath (io/file \"/a/b\"))") "/a/b"]
# --- slurp drains a StringReader (+ ignores :encoding opts) ---
["(slurp (StringReader. \"a=1\"))" "a=1"]
["(slurp (StringReader. \"b\") :encoding \"UTF-8\")" "b"]
# --- with-open: jhost reader + plain :close map ---
["(with-open [r (StringReader. \"a\")] (.read r))" "97"]
["(let [log (atom [])] (with-open [c {:close (fn [] (swap! log conj :closed))}] :r) (deref log))" "[:closed]"]
["(let [log (atom [])] (try (with-open [c {:close (fn [] (swap! log conj :closed))}] (throw (ex-info \"boom\" {}))) (catch Exception e nil)) (deref log))" "[:closed]"]
["(let [log (atom [])] (with-open [a {:close (fn [] (swap! log conj :outer))} b {:close (fn [] (swap! log conj :inner))}] :r) (deref log))" "[:inner :outer]"]
["(with-open [c {:close (fn [] nil) :v 5}] (:v c))" "5"]])
(defn run-capture [bin expr]
(def proc (os/spawn [bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (string/trim (if err (string err) ""))])
(var pass 0)
(def fails @[])
(each [expr expected] cases
(def [code got err] (run-capture jolt-bin expr))
(cond
(not= code 0) (array/push fails [expr (string "exit " code "; err: " err)])
(= got expected) (++ pass)
(array/push fails [expr (string "want `" expected "`, got `" got "`")])))
(printf "\n_ioreader parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [e m] fails (printf " FAIL %s\n %s" e m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,89 +0,0 @@
# jolt-avt6 — host class statics + constructors on Chez. The analyzer lowers
# Class/member to :host-static and (Class. ...) to :host-new; the Chez emit lowers
# them to host-static-ref/host-static-call/host-new (host-static.ss registry).
# Each case carries its expected printed value. Env-dependent values (os.name) are
# asserted via a predicate so the case stays portable across machines.
#
# janet test/chez/_javastatic.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(def cases
[["(Math/sqrt 16)" "4"]
["(Math/abs -3)" "3"]
["(Math/max 2 7)" "7"]
["(pos? Long/MAX_VALUE)" "true"]
["(String/valueOf 42)" "42"]
["(String/valueOf \"hi\")" "hi"]
["(String/valueOf :k)" ":k"]
["(String/valueOf nil)" "null"]
["(Long/parseLong \"42\")" "42"]
["(Long/valueOf \"42\")" "42"]
["(Integer/parseInt \"ff\" 16)" "255"]
["(.byteValue (Integer/valueOf \"ff\" 16))" "-1"]
["(Boolean/parseBoolean \"true\")" "true"]
["(Boolean/parseBoolean \"yes\")" "false"]
["(Character/isUpperCase \\A)" "true"]
["(Character/isLowerCase \\a)" "true"]
["(Character/isUpperCase \\a)" "false"]
["(Thread/interrupted)" "false"]
["(string? (System/getProperty \"os.name\"))" "true"]
["(string? (get (System/getenv) \"HOME\"))" "true"]
["(string? (System/getenv \"HOME\"))" "true"]
["(fn? System/exit)" "true"]
["(string? (get (System/getProperties) \"os.name\"))" "true"]
["(pos? (count (seq (System/getenv))))" "true"]
["(let [es (map (fn [[k v]] [k v]) (System/getenv))] (and (pos? (count es)) (every? vector? es)))" "true"]
# constructors + their methods
["(.toString (StringBuilder. \"x\"))" "x"]
["(.toString (-> (StringBuilder.) (.append \"a\") (.append \\b) (.append 1)))" "ab1"]
["(.toString (.append (StringBuilder. 16) \"x\"))" "x"]
["(let [sb (StringBuilder.)] (.append sb \"abcd\") (.setLength sb 2) (.toString sb))" "ab"]
["(let [w (StringWriter.)] (.write w \"a\") (.append w \\b) (.toString w))" "ab"]
["(let [r (StringReader. \"ab\")] [(.read r) (.read r) (.read r)])" "[97 98 -1]"]
["(let [r (StringReader. \"ab\")] (.mark r 1) [(.read r) (do (.reset r) (.read r))])" "[97 97]"]
["(let [r (java.io.PushbackReader. (java.io.StringReader. \"ab\"))] [(.read r) (.read r)])" "[97 98]"]
["(let [r (PushbackReader. (StringReader. \"ab\")) a (.read r)] (.unread r a) [a (.read r) (.read r)])" "[97 97 98]"]
["(let [r (PushbackReader. (StringReader. \"a\"))] (.unread r \\x) [(.read r) (.read r)])" "[120 97]"]
["(BigInteger. \"123\")" "123"]
["(let [m (HashMap. {:a 1 :b 2})] (.get m :b))" "2"]
["(let [m (HashMap. {})] (.put m :x 1) (.put m :y 2) (.size m))" "2"]
["(let [t (StringTokenizer. \"a=1&b=2\" \"&\")] [(.nextToken t) (.nextToken t)])" "[a=1 b=2]"]
["(.toString (StringBuilder. \"x\"))" "x"]
# ring-codec surface
["(URLEncoder/encode \"a b=c\")" "a+b%3Dc"]
["(URLDecoder/decode (URLEncoder/encode \"x &=%?\"))" "x &=%?"]
["(String. (.encode (Base64/getEncoder) (.getBytes \"hello\")))" "aGVsbG8="]
["(String. (.decode (Base64/getDecoder) (String. (.encode (Base64/getEncoder) (.getBytes \"hello\")))))" "hello"]
["(Integer/parseInt \"ff\" 16)" "255"]
# Pattern statics
["(regex? (Pattern/compile \"a.c\"))" "true"]
["(.split (Pattern/compile \",\") \"a,b,c\")" "(a b c)"]
["(do (require '[clojure.string :as s]) (s/replace \"a1b2\" (Pattern/compile \"[0-9]\") \"\"))" "ab"]
["(boolean (re-find (Pattern/compile \"^x\" Pattern/MULTILINE) \"y\\nx\"))" "true"]
["(boolean (re-find (re-pattern (Pattern/quote \"a.c\")) \"za.cy\"))" "true"]
["(boolean (re-find (re-pattern (Pattern/quote \"a.c\")) \"zabcy\"))" "false"]])
(defn run-capture [bin expr]
(def proc (os/spawn [bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (string/trim (if err (string err) ""))])
(var pass 0)
(def fails @[])
(each [expr expected] cases
(def [code got err] (run-capture jolt-bin expr))
(cond
(not= code 0) (array/push fails [expr (string "exit " code "; err: " err)])
(= got expected) (++ pass)
(array/push fails [expr (string "want `" expected "`, got `" got "`")])))
(printf "\n_javastatic parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [e m] fails (printf " FAIL %s\n %s" e m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,54 +0,0 @@
# jolt-yxqm — namespace value model (find-ns/ns-name/all-ns/resolve/ns-publics/
# in-ns/*ns* …). TDD harness: drives bin/joltc -e per case (fresh subprocess
# = per-case isolation), checks the LAST printed line == expected. Expected
# values are the JVM-canonical reference, baked per case.
#
# janet test/chez/_ns.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
# [label expr expected-last-line]
(def cases
[["find-ns existing" "(some? (find-ns 'clojure.core))" "true"]
["find-ns missing" "(nil? (find-ns 'does.not.exist))" "true"]
["resolve native-op +" "(var? (resolve '+))" "true"]
["resolve undefined" "(nil? (resolve 'totally-undefined-xyz))" "true"]
["ns-publics has def" "(do (def npv 1) (some? (get (ns-publics 'user) 'npv)))" "true"]
["ns-map has def" "(do (def nmv 1) (some? (get (ns-map 'user) 'nmv)))" "true"]
["ns-aliases is a map" "(map? (ns-aliases 'clojure.core))" "true"]
["ns-interns is a map" "(map? (ns-interns 'user))" "true"]
["ns-interns count pos" "(do (def q 1) (pos? (count (ns-interns 'user))))" "true"]
["all-ns count pos" "(pos? (count (all-ns)))" "true"]
["ns-name *ns* = user" "(= (ns-name *ns*) (ns-name (find-ns 'user)))" "true"]
["in-ns returns ns str" "(str (in-ns 'jolt.test-ns-b))" "jolt.test-ns-b"]
["in-ns updates *ns*" "(do (in-ns 'jolt.test-ns-a) (str *ns*))" "jolt.test-ns-a"]
["ns-unmap clears var" "(do (def nuv 1) (ns-unmap 'user 'nuv) (nil? (resolve 'nuv)))" "true"]
["in-ns no error" "(do (in-ns 'my.ns) (symbol? 'x))" "true"]
["str ns-name *ns*" "(str (ns-name *ns*))" "user"]
["find-var qualified" "(var? (find-var 'clojure.core/map))" "true"]
["the-ns ns-name" "(= 'user (ns-name (the-ns 'user)))" "true"]
["create-ns ns-name" "(= 'foo.bar (ns-name (create-ns 'foo.bar)))" "true"]])
(defn run-capture [expr]
(def proc (os/spawn [jolt-bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (if err (string err) "")])
(var pass 0)
(def fails @[])
(each [label expr expected] cases
(def [code got err] (run-capture expr))
(cond
(not= code 0) (array/push fails [label (string "exit " code "; err: " (string/trim err))])
(= got expected) (++ pass)
(array/push fails [label (string "want `" expected "`, got `" got "`")])))
(printf "\n_ns parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [l m] fails (printf " FAIL [%s] %s" l m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,95 +0,0 @@
# jolt-r8ku (inc Y) — Chez-side Clojure data reader: read-string / read /
# read+string / with-in-str / clojure.edn. The reader (host/chez/reader.ss)
# produces jolt forms directly; the *in* family and
# clojure.edn are Clojure over the read-string / __parse-next seams.
#
# Outputs are kept order-stable (equality checks, scalars) so set/map iteration
# order — which is host-dependent — doesn't masquerade as a
# divergence.
#
# janet test/chez/_reader.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(def cases
[# --- scalars + collections (value equality, order-independent) ---
["(= 42 (read-string \"42\"))" "true"]
["(= 42 (read-string \"0x2A\"))" "true"]
# numeric tower (jolt-n6al): "1/2" reads as an exact Ratio (= JVM), so a
# category-aware = against the double 0.5 is false; assert numeric value (==),
# which holds in both the all-flonum and tower models.
["(== 0.5 (read-string \"1/2\"))" "true"]
["(= -3.5 (read-string \"-3.5\"))" "true"]
["(== 1000.0 (read-string \"1e3\"))" "true"]
["(= 1 (read-string \"1N\"))" "true"]
["(integer? (read-string \"7\"))" "true"]
["(= :foo (read-string \":foo\"))" "true"]
["(= :a/b (read-string \":a/b\"))" "true"]
["(= :foo (read-string \"::foo\"))" "true"]
["(= (quote sym) (read-string \"sym\"))" "true"]
["(= (quote ns/sym) (read-string \"ns/sym\"))" "true"]
["(nil? (read-string \"nil\"))" "true"]
["(true? (read-string \"true\"))" "true"]
["(false? (read-string \"false\"))" "true"]
["(= \\a (read-string \"\\\\a\"))" "true"]
["(= \\newline (read-string \"\\\\newline\"))" "true"]
["(= \\space (read-string \"\\\\space\"))" "true"]
["(= \"a\\nb\" (read-string \"\\\"a\\\\nb\\\"\"))" "true"]
["(= [1 2 3] (read-string \"[1 2 3]\"))" "true"]
["(= (quote (+ 1 2)) (read-string \"(+ 1 2)\"))" "true"]
["(= {:a 1 :b 2} (read-string \"{:a 1 :b 2}\"))" "true"]
# core read-string returns the raw set FORM (edn->value builds the real set)
["(:value (read-string \"#{1 2 3}\"))" "[1 2 3]"]
["(= :jolt/set (:jolt/type (read-string \"#{1 2 3}\")))" "true"]
["(= [1 [2 3] {:k :v}] (read-string \"[1 [2 3] {:k :v}]\"))" "true"]
# --- reader macros ---
["(= (quote (quote x)) (read-string \"'x\"))" "true"]
["(= (quote (clojure.core/deref a)) (read-string \"@a\"))" "true"]
["(= (quote (syntax-quote (a (unquote b)))) (read-string \"`(a ~b)\"))" "true"]
["(= (quote (unquote-splicing xs)) (read-string \"~@xs\"))" "true"]
# --- whitespace / comments / discard ---
["(= 42 (read-string \"; comment\\n42\"))" "true"]
["(= [1 2] (read-string \"[1 #_ 9 2]\"))" "true"]
["(nil? (read-string \"\"))" "true"]
["(nil? (read-string \" , ,\"))" "true"]
# --- metadata ^ on a symbol ---
["(:tag (meta (read-string \"^String x\")))" "String"]
["(:foo (meta (read-string \"^:foo x\")))" "true"]
# --- *in* reader family: read / read+string / with-in-str ---
["(= 42 (with-in-str \"42\" (read)))" "true"]
["(= (quote (+ 1 2)) (with-in-str \"(+ 1 2)\" (read)))" "true"]
["(with-in-str \"1 2\" [(read) (read)])" "[1 2]"]
["(= :done (with-in-str \"\" (read *in* false :done)))" "true"]
["(let [[v s] (with-in-str \"42 rest\" (read+string))] (and (= v 42) (string? s)))" "true"]
["(= [1 2] (with-in-str \"1 2\" [(first (read+string)) (first (read+string))]))" "true"]
# --- clojure.edn (set/tagged forms built into real values) ---
["(do (require (quote [clojure.edn :as e0])) (= #{1 2} (e0/read-string \"#{1 2}\")))" "true"]
["(do (require (quote [clojure.edn :as e0])) (uuid? (e0/read-string \"#uuid \\\"550e8400-e29b-41d4-a716-446655440000\\\"\")))" "true"]
["(do (require (quote [clojure.edn :as e0])) (inst? (e0/read-string \"#inst \\\"2020-01-01T00:00:00Z\\\"\")))" "true"]
["(do (require (quote [clojure.edn :as e0])) (= :end (e0/read-string {:eof :end} \"\")))" "true"]
["(do (require (quote [clojure.edn :as e0])) (= [:custom 5] (e0/read-string {:readers {(quote custom) (fn [v] [:custom v])}} \"#custom 5\")))" "true"]
["(do (require (quote clojure.edn)) (= {:a 1 :b 2} (clojure.edn/read-string \"{:a 1\\n :b 2}\")))" "true"]])
(defn run-capture [bin expr]
(def proc (os/spawn [bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (string/trim (if err (string err) ""))])
(var pass 0)
(def fails @[])
(each [expr expected] cases
(def [code got err] (run-capture jolt-bin expr))
(cond
(not= code 0) (array/push fails [expr (string "exit " code "; err: " err)])
(= got expected) (++ pass)
(array/push fails [expr (string "want `" expected "`, got `" got "`")])))
(printf "\n_reader parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [e m] fails (printf " FAIL %s\n %s" e m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,65 +0,0 @@
# sequential? / seq? on lazy seqs (jolt-2o7x follow-up). The inc M fix made the
# native seq? var recognize a lazy-seq (re-def-var!, not just set!). sequential?
# is overlay (`(defn sequential? [x] (or (vector? x) (seq? x)))`), so it inherits
# the fix transitively; this pins that both predicates agree with the JVM oracle
# over every lazy-seq-producing form (and the native =/hash path via set!).
# Expectations are the JVM-canonical values.
#
# janet test/chez/_seqpred.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(def cases
[# --- seq? over lazy seqs ---
# (NB: range is lazy, so (type (range 3)) is :seq not :vector
# seq; a range-container divergence, not the predicate. sequential? agrees on it.)
["seq? map" "(seq? (map inc [1 2 3]))" "true"]
["seq? filter" "(seq? (filter odd? [1 2 3]))" "true"]
["seq? lazy-seq" "(seq? (lazy-seq (cons 1 nil)))" "true"]
["seq? take iterate" "(seq? (take 3 (iterate inc 0)))" "true"]
["seq? cons onto lazy" "(seq? (cons 0 (range 3)))" "true"]
["seq? vector false" "(seq? [1 2 3])" "false"]
["seq? map-coll false" "(seq? {:a 1})" "false"]
["seq? nil false" "(seq? nil)" "false"]
# --- sequential? over lazy seqs (overlay, delegates to seq?) ---
["sequential? range" "(sequential? (range 3))" "true"]
["sequential? map" "(sequential? (map inc [1 2 3]))" "true"]
["sequential? filter" "(sequential? (filter odd? [1 2 3]))" "true"]
["sequential? lazy-seq" "(sequential? (lazy-seq (cons 1 nil)))" "true"]
["sequential? infinite" "(sequential? (take 2 (repeat 9)))" "true"]
["sequential? vector" "(sequential? [1 2 3])" "true"]
["sequential? list" "(sequential? '(1 2 3))" "true"]
["sequential? map false" "(sequential? {:a 1})" "false"]
["sequential? set false" "(sequential? #{1 2})" "false"]
["sequential? nil false" "(sequential? nil)" "false"]
# --- native =/hash path (jolt-sequential? via set!) over a raw lazy seq ---
["= vec lazyseq" "(= [0 1 2] (range 3))" "true"]
["= lazyseq vec" "(= (range 3) [0 1 2])" "true"]
["= lazyseq list" "(= (map inc [0 1]) '(1 2))" "true"]
["set contains lazyseq" "(contains? #{[0 1 2]} (vec (range 3)))" "true"]])
(defn run-capture [expr]
(def proc (os/spawn [jolt-bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (if err (string err) "")])
(var pass 0)
(def fails @[])
(each [label expr expected] cases
(def [code got err] (run-capture expr))
(cond
(not= code 0) (array/push fails [label (string "exit " code "; err: " (string/trim err))])
(= got expected) (++ pass)
(array/push fails [label (string "want `" expected "`, got `" got "`")])))
(printf "\n_seqpred parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [l m] fails (printf " FAIL [%s] %s" l m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,77 +0,0 @@
# jolt-j5vg / jolt-22vo / clojure.pprint — Phase-2 stdlib parity closeout.
#
# clojure.set — pure Clojure (src/jolt/clojure/set.clj) added to the Chez
# prelude tier (driver.janet stdlib-ns-files).
# clojure.math — native flonum-math shims (host/chez/math.ss) def-var!'d into
# the clojure.math ns; the analyzer already knows the ns exists
# (api.janet install-clojure-math!), so refs lower to var-deref.
# clojure.pprint — minimal shim on the prelude; pprint's 2-arity no longer uses
# (binding [*out* writer] ...) (uncompilable on the no-fallback
# Chez back end; *out* isn't a bindable var — output always goes
# through the host seam).
#
# Outputs are order-stable (value-equality / scalars) so set/map iteration order
# — which is host-dependent — never masquerades as a divergence.
#
#
# janet test/chez/_stdlib.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(def cases
[# --- clojure.math (jolt-22vo / jolt-h79) ---
["(< 1.4142 (clojure.math/sqrt 2) 1.4143)" "true"]
["(long (clojure.math/pow 2 10))" "1024"]
["(long (clojure.math/tan 0))" "0"]
["(clojure.math/round 2.6)" "3"]
["(clojure.math/floor 2.9)" "2"]
["(clojure.math/signum -7.2)" "-1"]
["(< 3.14 (clojure.math/to-radians 180) 3.15)" "true"]
["(< 3.14 clojure.math/PI 3.15)" "true"]
["(< 2.71 clojure.math/E 2.72)" "true"]
["(long (clojure.math/cbrt 27))" "3"]
["(< 4.6 (clojure.math/log 100) 4.7)" "true"]
# Chez has no native log10 (computed as log(x)/log(10)), so it can differ from
# C log10 in the last ulp (3 vs 2.9999…); range-check, don't pin.
["(< 2.99 (clojure.math/log10 1000) 3.01)" "true"]
["(do (require (quote [clojure.math :as m])) (long (m/hypot 3 4)))" "5"]
["(mapv (comp long clojure.math/sqrt) [1 4])" "[1 2]"]
["(long (clojure.math/atan2 0 1))" "0"]
# --- clojure.set (jolt-j5vg) ---
["(do (require (quote [clojure.set :as s])) (= #{1 2 3 4} (s/union #{1 2} #{3 4})))" "true"]
["(do (require (quote [clojure.set :as s])) (= #{2} (s/intersection #{1 2} #{2 3})))" "true"]
["(do (require (quote [clojure.set :as s])) (= #{1} (s/difference #{1 2} #{2 3})))" "true"]
["(do (require (quote [clojure.set :as s])) (s/subset? #{1} #{1 2}))" "true"]
["(do (require (quote [clojure.set :as s])) (s/superset? #{1 2} #{1}))" "true"]
["(do (require (quote [clojure.set :as s])) (= {1 :a 2 :b} (s/map-invert {:a 1 :b 2})))" "true"]
["(do (require (quote [clojure.set :as s])) (= #{:a} (s/select keyword? #{:a})))" "true"]
["(do (require (quote [clojure.set :as s])) (= #{{:a 1 :b 2}} (s/join #{{:a 1}} #{{:b 2}})))" "true"]
["(do (require (quote [clojure.set :as s])) (= {:b 1} (s/rename-keys {:a 1} {:a :b})))" "true"]
["(do (require (quote [clojure.set :as s])) (= 2 (count (s/index #{{:k 1} {:k 2}} [:k]))))" "true"]
# --- clojure.pprint (minimal shim) ---
["(do (require (quote [clojure.pprint :as pp])) (= \"[1 2 3]\\n\" (with-out-str (pp/pprint [1 2 3]))))" "true"]
["(do (require (quote [clojure.pprint :as pp])) (pp/with-pprint-dispatch pp/code-dispatch 42))" "42"]])
(defn run-capture [bin expr]
(def proc (os/spawn [bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (string/trim (if err (string err) ""))])
(var pass 0)
(def fails @[])
(each [expr expected] cases
(def [code got err] (run-capture jolt-bin expr))
(cond
(not= code 0) (array/push fails [expr (string "exit " code "; err: " err)])
(= got expected) (++ pass)
(array/push fails [expr (string "want `" expected "`, got `" got "`")])))
(printf "\n_stdlib parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [e m] fails (printf " FAIL %s\n %s" e m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,65 +0,0 @@
# jolt-nfca — host java.lang.String method interop on Chez: (.toUpperCase s),
# (.indexOf s x), (.substring s a b), the regex methods (.matches/.replaceAll/
# .replaceFirst), etc. The string-methods surface. Each case carries its
# expected value.
# An expected of :throws asserts a non-zero exit (unsupported method).
#
# janet test/chez/_str.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(def cases
[["toLowerCase" "(.toLowerCase \"HI\")" "hi"]
["toUpperCase" "(.toUpperCase \"hi\")" "HI"]
["trim" "(.trim \" x \")" "x"]
["length" "(.length \"abc\")" "3"]
["isEmpty" "[(.isEmpty \"\") (.isEmpty \"a\")]" "[true false]"]
["indexOf hit" "(.indexOf \"abc\" \"b\")" "1"]
["indexOf miss" "(.indexOf \"abc\" \"z\")" "-1"]
["indexOf from" "(.indexOf \"abab\" \"a\" 1)" "2"]
["indexOf int code" "(.indexOf \"a=b\" 61)" "1"]
["lastIndexOf" "(.lastIndexOf \"abab\" \"b\")" "3"]
["substring 1" "(.substring \"abc\" 1)" "bc"]
["substring 1 2" "(.substring \"abc\" 1 2)" "b"]
["startsWith" "(.startsWith \"abc\" \"ab\")" "true"]
["endsWith" "(.endsWith \"abc\" \"bc\")" "true"]
["contains" "(.contains \"abc\" \"b\")" "true"]
["replace literal" "(.replace \"abc\" \"b\" \"x\")" "axc"]
["replace all occ" "(.replace \"aaa\" \"a\" \"b\")" "bbb"]
["charAt" "(.charAt \"abc\" 1)" "\\b"]
["equalsIgnoreCase" "(.equalsIgnoreCase \"AbC\" \"aBc\")" "true"]
["toString" "(.toString \"hi\")" "hi"]
["concat" "(.concat \"ab\" \"cd\")" "abcd"]
["matches whole" "(.matches \"abc\" \"a.c\")" "true"]
["matches partial" "(.matches \"abcd\" \"a.c\")" "false"]
["replaceAll" "(.replaceAll \"a_b_c\" \"_\" \"-\")" "a-b-c"]
["replaceFirst" "(.replaceFirst \"a_b_c\" \"_\" \"-\")" "a-b_c"]
["split regex" "(vec (.split \"a,b,c\" \",\"))" "[a b c]"]
["unsupported" "(.frobnicate \"abc\")" :throws]])
(defn run-capture [expr]
(def proc (os/spawn [jolt-bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (if err (string err) "")])
(var pass 0)
(def fails @[])
(each [label expr expected] cases
(def [code got err] (run-capture expr))
(cond
(= expected :throws)
(if (not= code 0) (++ pass)
(array/push fails [label (string "want throw, got `" got "` (exit 0)")]))
(not= code 0) (array/push fails [label (string "exit " code "; err: " (string/trim err))])
(= got expected) (++ pass)
(array/push fails [label (string "want `" expected "`, got `" got "`")])))
(printf "\n_str parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [l m] fails (printf " FAIL [%s] %s" l m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,66 +0,0 @@
# jolt-nfca (clojure.string half) — the clojure.string namespace on Chez via the
# alias `s` established by a runtime (require '[clojure.string :as s]). The Chez
# AOT driver pre-evals require forms against the ctx so the alias resolves at
# analyze time, and clojure.string is emitted as a prelude tier over the str-*
# primitives. Each case carries its expected value.
#
# janet test/chez/_strns.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(defn- with-req [body] (string "(do (require (quote [clojure.string :as s])) " body ")"))
# [label body-after-require expected]
(def cases
[["upper-case" "(s/upper-case \"abc\")" "ABC"]
["lower-case" "(s/lower-case \"ABC\")" "abc"]
["capitalize" "(s/capitalize \"hello\")" "Hello"]
["trim" "(s/trim \" x \")" "x"]
["triml" "(= \"x \" (s/triml \" x \"))" "true"]
["trimr" "(= \" x\" (s/trimr \" x \"))" "true"]
["blank? empty" "(s/blank? \"\")" "true"]
["blank? ws" "(s/blank? \" \")" "true"]
["blank? no" "(s/blank? \"x\")" "false"]
["blank? nil" "(s/blank? nil)" "true"]
["includes? y" "(s/includes? \"abcd\" \"bc\")" "true"]
["includes? n" "(s/includes? \"abcd\" \"zz\")" "false"]
["starts-with? y" "(s/starts-with? \"abc\" \"ab\")" "true"]
["starts-with? n" "(s/starts-with? \"abc\" \"bc\")" "false"]
["ends-with? y" "(s/ends-with? \"abc\" \"bc\")" "true"]
["join no sep" "(s/join [\"a\" \"b\" \"c\"])" "abc"]
["join sep" "(s/join \",\" [\"a\" \"b\" \"c\"])" "a,b,c"]
["join nums" "(s/join \"-\" [1 2 3])" "1-2-3"]
["split literal" "(s/split \"a,b,c\" \",\")" "[a b c]"]
["split regex" "(s/split \"a1b2c\" #\"[0-9]\")" "[a b c]"]
["split-lines" "(s/split-lines \"a\\nb\\nc\")" "[a b c]"]
["replace lit" "(s/replace \"a_b_c\" \"_\" \"-\")" "a-b-c"]
["replace regex" "(s/replace \"a1b2\" #\"[0-9]\" \"\")" "ab"]
["replace-first" "(s/replace-first \"a_b_c\" \"_\" \"-\")" "a-b_c"]
["reverse" "(s/reverse \"abc\")" "cba"]
["index-of hit" "(s/index-of \"abc\" \"b\")" "1"]
["index-of miss" "(nil? (s/index-of \"abc\" \"z\"))" "true"]
["trim-newline" "(s/trim-newline \"abc\\n\\n\")" "abc"]])
(defn run-capture [expr]
(def proc (os/spawn [jolt-bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (if err (string err) "")])
(var pass 0)
(def fails @[])
(each [label body expected] cases
(def [code got err] (run-capture (with-req body)))
(cond
(not= code 0) (array/push fails [label (string "exit " code "; err: " (string/trim err))])
(= got expected) (++ pass)
(array/push fails [label (string "want `" expected "`, got `" got "`")])))
(printf "\n_strns parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [l m] fails (printf " FAIL [%s] %s" l m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,81 +0,0 @@
# jolt-fmm4 — (type x) on Chez: :type meta override, record class-name symbol,
# and a comprehensive value->taxonomy mapping (no value type crashes -> must be
# total, the recorded gotcha). Each case carries its expected value.
# (type (range 3)) is :seq (range is lazy); range-container divergences are
# covered elsewhere.
#
# janet test/chez/_type.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(def cases
[# --- scalars ---
["int" "(type 5)" ":number"]
["float" "(type 5.0)" ":number"]
["ratio-ish" "(type (/ 10 2))" ":number"]
["string" "(type \"s\")" ":string"]
["keyword" "(type :k)" ":keyword"]
["symbol" "(type 'x)" ":symbol"]
["true" "(type true)" ":boolean"]
["false" "(type false)" ":boolean"]
["nil" "(type nil)" ""]
["char" "(type \\a)" ":char"]
# --- collections ---
["vector" "(type [1 2])" ":vector"]
["empty vector" "(type [])" ":vector"]
["map" "(type {:a 1})" ":map"]
["set" "(type #{1})" ":set"]
["list" "(type '(1 2))" ":seq"]
["empty list" "(type '())" ":seq"]
["map entry" "(type (first {:a 1}))" ":vector"]
["lazy map" "(type (map inc [1 2]))" ":seq"]
["lazy filter" "(type (filter odd? [1 2 3]))" ":seq"]
["lazy-seq" "(type (lazy-seq (cons 1 nil)))" ":seq"]
["take iterate" "(type (take 2 (iterate inc 0)))" ":seq"]
["fn" "(type inc)" ":fn"]
["sorted-map" "(type (sorted-map :a 1))" ":map"]
["sorted-set" "(type (sorted-set 1))" ":jolt/sorted-set"]
# --- :type meta override (the headline jolt-fmm4 case) ---
["meta override" "(type (with-meta [1] {:type :custom}))" ":custom"]
["meta override map" "(type (with-meta {:a 1} {:type :rec}))" ":rec"]
["meta no :type" "(type (with-meta [1] {:other 9}))" ":vector"]
# --- record -> ns-qualified class-name symbol ---
["record symbol" "(do (defrecord TyR [a]) (type (->TyR 1)))" "user.TyR"]
["record roundtrip" "(do (defrecord TyR [a]) (= (symbol (str (type (->TyR 1)))) (type (->TyR 1))))" "true"]
["record is symbol" "(do (defrecord TyR [a]) (symbol? (type (->TyR 1))))" "true"]
# --- exotic host wrappers (seed :jolt/* tags; total, never crash) ---
["atom" "(type (atom 1))" ":jolt/atom"]
["volatile" "(type (volatile! 1))" ":jolt/volatile"]
["regex" "(type #\"re\")" ":jolt/regex"]
["var" "(do (def vx 1) (type (var vx)))" ":jolt/var"]
["transient" "(type (transient []))" ":jolt/transient"]
["uuid" "(type (random-uuid))" ":jolt/uuid"]
["ex-info" "(type (ex-info \"x\" {}))" ":jolt/ex-info"]])
(defn run-capture [expr]
(def proc (os/spawn [jolt-bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (if err (string err) "")])
(var pass 0)
(def fails @[])
(each [label expr expected] cases
(def [code got err] (run-capture expr))
(cond
(not= code 0) (array/push fails [label (string "exit " code "; err: " (string/trim err))])
(= got expected) (++ pass)
(array/push fails [label (string "want `" expected "`, got `" got "`")])))
(printf "\n_type parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [l m] fails (printf " FAIL [%s] %s" l m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,45 +0,0 @@
# jolt-zikh — var def-time metadata capture (^:private / ^Type tag / docstring).
# (meta (var v)) must carry the def-time reader metadata + :ns/:name, matching the
# JVM-canonical reference. TDD harness: bin/joltc -e per case, last line ==
# expected.
#
# janet test/chez/_var_meta.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
(def cases
# NOTE: ^{:map} metadata on a def name (e.g. (def ^{:doc "hi"} dv 1)) reads as
# (def (with-meta name m) v) and is uncompilable for the COMPILER generally
# (analyzer.clj rejects it) — out of subset, not a meta-capture gap. Shorthand
# ^:kw / ^Type
# and the docstring form keep the name a plain symbol, so they're in scope.
[["^:private on var" "(do (def ^:private pv 1) (:private (meta (var pv))))" "true"]
["^Type tag on var" "(do (def ^String tv \"a\") (:tag (meta (var tv))))" "String"]
["(def name doc val)" "(do (def dv2 \"hi\" 1) (:doc (meta (var dv2))))" "hi"]
["meta carries :name" "(do (def mv 1) (:name (meta (var mv))))" "mv"]
["meta carries :ns" "(do (def nv 1) (:ns (meta (var nv))))" "user"]
["plain def: no user meta" "(do (def pl 1) (nil? (:private (meta (var pl)))))" "true"]])
(defn run-capture [expr]
(def proc (os/spawn [jolt-bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (if err (string err) "")])
(var pass 0)
(def fails @[])
(each [label expr expected] cases
(def [code got err] (run-capture expr))
(cond
(not= code 0) (array/push fails [label (string "exit " code "; err: " (string/trim err))])
(= got expected) (++ pass)
(array/push fails [label (string "want `" expected "`, got `" got "`")])))
(printf "\n_var_meta parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [l m] fails (printf " FAIL [%s] %s" l m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,77 +0,0 @@
# jolt-75sv — list? (a list marker on cseq, since cseq backs both lists and
# realized/lazy seqs) + map-entry-as-vector + clojure.walk.
#
# janet test/chez/_walk.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/joltc"))
# -e reads only the FIRST form — wrap require + use in a single (do ...).
(defn w [body] (string "(do (require (quote [clojure.walk :as w])) " body ")"))
(def cases
[# --- list? : true for lists, cons/reverse/conj-on-list; false for seqs ---
["(list? (list 1 2))" "true"]
["(list? (list 1))" "true"]
["(list? '(1 2))" "true"]
["(list? '())" "true"]
["(list? (list))" "true"]
["(list? (cons 1 nil))" "true"]
["(list? (cons 1 [2]))" "true"]
["(list? (cons 1 '(2)))" "true"]
["(list? (conj (list 1) 0))" "true"]
["(list? (conj '() 1))" "true"]
["(list? (reverse [1 2]))" "true"]
["(list? (reverse '(1 2)))" "true"]
["(list? [1 2])" "false"]
["(list? {:a 1})" "false"]
["(list? (map inc [1 2]))" "false"]
["(list? (filter odd? [1 2 3]))" "false"]
["(list? (seq [1 2]))" "false"]
["(list? (rest (list 1 2)))" "false"]
["(list? (next (list 1 2)))" "false"]
["(list? (take 2 (list 1 2 3)))" "false"]
["(list? (concat '(1) '(2)))" "false"]
["(list? (rest [1 2]))" "false"]
["(list? 5)" "false"]
["(list? nil)" "false"]
# --- map-entry IS a vector (Clojure MapEntry; seed agrees) ---
["(vector? (first {:a 1}))" "true"]
["(vector? (first (seq {:a 1})))" "true"]
["(map-entry? (first {:a 1}))" "true"]
["(= (first {:a 1}) [:a 1])" "true"]
["(vector? [1 2])" "true"]
["(vector? (rest [1 2 3]))" "false"]
# --- clojure.walk ---
[(w "(w/postwalk (fn [x] (if (number? x) (inc x) x)) {:a 1})") "{:a 2}"]
[(w "(w/keywordize-keys {\"a\" 1})") "{:a 1}"]
[(w "(= {\"a\" 1} (w/stringify-keys {:a 1}))") "true"]
[(w "(w/postwalk-replace {'x 2} '(+ x x))") "(+ 2 2)"]
[(w "(w/postwalk (fn [n] (if (symbol? n) :a n)) '(x y))") "(:a :a)"]
[(w "(w/prewalk-replace {'* '* 'y 3} '(* y y))") "(* 3 3)"]
[(w "(w/postwalk-replace {:a 1 :b 2} '(:a [:b :a]))") "(1 [2 1])"]
[(w "(w/postwalk-replace {1 :one} [1 2 1])") "[:one 2 :one]"]
["(do (require (quote [clojure.template :as t])) (t/apply-template '[x y] '(+ x y) '(1 2)))" "(+ 1 2)"]])
(defn run-capture [bin expr]
(def proc (os/spawn [bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (string/trim (if err (string err) ""))])
(var pass 0)
(def fails @[])
(each [expr expected] cases
(def [code got err] (run-capture jolt-bin expr))
(cond
(not= code 0) (array/push fails [expr (string "exit " code "; err: " err)])
(= got expected) (++ pass)
(array/push fails [expr (string "want `" expected "`, got `" got "`")])))
(printf "\n_walk parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [e m] fails (printf " FAIL %s\n %s" e m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -1,102 +0,0 @@
# Phase 1 (jolt-cf1q.2) close-out bench — the compute benches run END TO END
# through the REAL pipeline (Clojure source -> Janet-hosted analyzer -> IR ->
# Scheme emitter -> Chez compile -> run), timed in-process (Chez startup
# excluded), and reported against the spike ceiling (spike/chez/RESULTS.md).
#
# This is the Phase 1 gate evidence: (1) compile-only is TOTAL for the compute
# subset — every form emits, no interpreter fallback (Chez has none); (2) the
# emitted code runs at ~the substrate ceiling, with the residual gap being
# exactly the typed fl*/fx* emission that Phase 4 owns.
#
# JOLT_CHEZ_BENCH=1 janet test/chez/bench-pipeline.janet
# Opt-in (like core-bench); skipped in the normal gate.
(import ../../src/jolt/backend :as backend)
(import ../../src/jolt/reader :as r)
(import ../../host/chez/driver :as d)
(import ../../host/chez/emit :as emit)
(unless (os/getenv "JOLT_CHEZ_BENCH")
(print "skip: set JOLT_CHEZ_BENCH=1 to run the Chez pipeline bench")
(os/exit 0))
(unless (d/chez-available?)
(print "skip: chez not on PATH")
(os/exit 0))
(def ctx (d/make-ctx))
# Emit a top-level program (one or more defns) through the real pipeline. Returns
# the concatenated Scheme for every form — each form must emit (compile-only is
# total) or this throws, which is itself the totality check.
(defn emit-program [src]
(def forms (map first (r/parse-all-positioned src)))
(string/join (map (fn [f] (emit/emit (backend/analyze-form ctx f))) forms) "\n"))
(def fib-src "(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))")
# mandelbrot kernel: loop/recur + let + or-expansion + cross-var call, all flonum.
(def mandel-src ``
(defn count-point [cr ci cap]
(loop [i 0 zr 0.0 zi 0.0]
(if (or (>= i cap) (> (+ (* zr zr) (* zi zi)) 4.0))
i
(recur (inc i)
(+ (- (* zr zr) (* zi zi)) cr)
(+ (* 2.0 (* zr zi)) ci)))))
(defn run [n]
(let [cap 200 nd (* 1.0 n)]
(loop [y 0 acc 0]
(if (>= y n) acc
(let [ci (- (* 2.0 (/ (* 1.0 y) nd)) 1.0)
row (loop [x 0 a 0]
(if (>= x n) a
(let [cr (- (* 3.5 (/ (* 1.0 x) nd)) 2.5)]
(recur (inc x) (+ a (count-point cr ci cap))))))]
(recur (inc y) (+ acc row)))))))
``)
(def fib-scm (emit-program fib-src))
(def mandel-scm (emit-program mandel-src))
(print "compile-only total: fib + mandelbrot emitted with no fallback")
# One Chez program: load the RT, the emitted defns, a hand-written FLONUM fib
# reference (jolt's realistic ceiling given the all-double model), then time each
# end-to-end value (warm up first; exclude Chez startup via a monotonic clock).
(def prog
(string
"(import (chezscheme))\n(load \"host/chez/rt.ss\")\n"
fib-scm "\n" mandel-scm "\n"
"(define fib (var-deref \"user\" \"fib\"))\n"
"(define mrun (var-deref \"user\" \"run\"))\n"
# hand flonum fib — the substrate ceiling for jolt's number model
"(define (ffib n) (if (fl< n 2.0) n (fl+ (ffib (fl- n 1.0)) (ffib (fl- n 2.0)))))\n"
"(define (now-ns) (let ((t (current-time 'time-monotonic))) (+ (* (time-second t) 1000000000) (time-nanosecond t))))\n"
"(define (timed thunk) (let* ((t0 (now-ns)) (r (thunk)) (ms (/ (- (now-ns) t0) 1000000.0))) (cons r ms)))\n"
"(fib 24)(ffib 24.0)(mrun 30)\n" # warm up
"(let ((a (timed (lambda () (fib 30))))\n"
" (b (timed (lambda () (ffib 30.0))))\n"
" (c (timed (lambda () (mrun 200)))))\n"
" (printf \"~a ~a ~a ~a ~a ~a\\n\"\n"
" (jolt-pr-str (car a)) (exact->inexact (cdr a))\n"
" (jolt-pr-str (car b)) (exact->inexact (cdr b))\n"
" (jolt-pr-str (car c)) (exact->inexact (cdr c))))"))
(def path (string "/tmp/chez-bench-pipeline-" (os/getpid) ".ss"))
(spit path prog)
(def proc (os/spawn ["chez" "--script" path] :p {:out :pipe :err :pipe}))
(def out (string/trim (string (ev/read (proc :out) 0x100000))))
(def err (string/trim (string (or (ev/read (proc :err) 0x100000) ""))))
(def code (os/proc-wait proc))
(unless (= code 0) (printf "BENCH FAILED (code %d): %s" code err) (os/exit 1))
(def p (string/split " " out))
(defn num [i] (scan-number (get p i "0")))
(printf "\nReal-pipeline compute benches (Chez startup excluded):\n")
(printf " fib 30 (jolt, flonum) = %s in %6.2f ms" (get p 0) (num 1))
(printf " fib 30 (hand flonum ceiling) = %6.2f ms <- jolt's number-model ceiling" (num 3))
(printf " fib 30 (spike fixnum ceiling)= 5.20 ms <- Phase 4 fl/fx typed-emit target")
(printf " mandelbrot 200 (jolt) = %s in %6.2f ms" (get p 4) (num 5))
(printf " mandelbrot 200 (spike generic)= 98.10 ms <- generic-flonum ceiling")
(printf " mandelbrot 200 (spike typed) = 13.40 ms <- Phase 4 fl/fx typed-emit target")
(def fib-overhead (if (> (num 3) 0) (/ (num 1) (num 3)) 0))
(printf "\n jolt fib is %.2fx the hand-flonum ceiling (residual = truthy/dispatch; typed-emit closes the fixnum gap)." fib-overhead)
(os/exit 0)

View file

@ -1,72 +0,0 @@
# Chez Phase 3 inc9a (jolt-9phg) — the pure-Chez self-build (no Janet in the loop).
#
# inc8 proved the on-Chez compiler reproduces itself. inc9a makes that the actual
# build: host/chez/bootstrap.ss loads the CHECKED-IN seed (host/chez/seed/{prelude,
# image}.ss — the bootstrap compiler, minted once via the fixpoint) and rebuilds the
# clojure.core prelude + compiler image FROM SOURCE on Chez. No Janet is invoked in
# the compile path — this test only spawns `chez`; the read->analyze->emit is 100%
# Chez. So a fresh checkout + Chez (no Janet) yields a working jolt.
#
# The seed is a JOINT byte-fixpoint, so rebuilding from an up-to-date seed
# reproduces it exactly. If the seed SOURCES change (core tiers, the compiler, the
# host contract, the reader, emit-image.ss) the rebuilt artifacts will differ and
# this test fails — re-mint the seed with driver/mint-chez-seed* (see the failure
# message) and commit the refreshed seed.
#
# janet test/chez/bootstrap-test.janet
(import ../../host/chez/driver :as d)
(var total 0) (var fails 0)
(defn ok [name pred &opt extra]
(++ total)
(if pred (printf "ok: %s" name)
(do (++ fails) (printf "FAIL: %s %s" name (or extra "")))))
(unless (d/chez-available?)
(print "chez not on PATH — skipping bootstrap-test")
(os/exit 0))
(def seed-prelude "host/chez/seed/prelude.ss")
(def seed-image "host/chez/seed/image.ss")
(ok "checked-in seed prelude exists" (os/stat seed-prelude))
(ok "checked-in seed image exists" (os/stat seed-image))
(when (and (os/stat seed-prelude) (os/stat seed-image))
(def tmp (or (os/getenv "TMPDIR") "/tmp"))
(def out-prelude (string tmp "/jolt-bootstrap-pre-" (os/getpid) ".ss"))
(def out-image (string tmp "/jolt-bootstrap-img-" (os/getpid) ".ss"))
(def t0 (os/clock))
# PURE CHEZ: spawn `chez --script bootstrap.ss` — no Janet in the compile path.
(def [code out err] (d/run-bootstrap seed-prelude seed-image out-prelude out-image))
(printf "pure-Chez bootstrap pass: %.1fs" (- (os/clock) t0))
(ok "bootstrap.ss runs cleanly on Chez (no Janet)" (= code 0)
(string "exit " code " " (string/slice err 0 (min 300 (length err)))))
(defn- bytes= [a b] (and (os/stat a) (os/stat b)
(= (string (slurp a)) (string (slurp b)))))
(def remint "re-mint with driver/mint-chez-seed* and commit host/chez/seed/")
(when (= code 0)
(ok "rebuilt prelude == checked-in seed (joint fixpoint)" (bytes= out-prelude seed-prelude)
(string "seed is stale — " remint))
(ok "rebuilt image == checked-in seed (joint fixpoint)" (bytes= out-image seed-image)
(string "seed is stale — " remint))
# The rebuilt artifacts must be a WORKING compiler.
(def cases
[["(let [x 1 y 2] (+ x y))" "3"]
["(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) (fib 10)" "55"]
["(->> (range 10) (filter even?) (map #(* % %)) (reduce +))" "120"]
["(let [{:keys [a b] :or {b 9}} {:a 1}] [a b])" "[1 9]"]
["(require '[clojure.string :as s]) (s/join \",\" [1 2 3])" "1,2,3"]
["(loop [i 0 acc 0] (if (< i 5) (recur (inc i) (+ acc i)) acc))" "10"]])
(var pass 0)
(each [src want] cases
(def [c o _] (d/eval-zero-janet out-prelude out-image (string "(do " src ")")))
(when (and (= c 0) (= o want)) (++ pass)))
(ok "Chez-built artifacts compile+run real cases" (= pass (length cases))
(string pass "/" (length cases) " cases passed")))
(each p [out-prelude out-image] (when (os/stat p) (os/rm p))))
(printf "\nbootstrap-test: %d/%d checks passed" (- total fails) total)
(os/exit (if (zero? fails) 0 1))

View file

@ -1,96 +0,0 @@
# Chez Phase 3 inc9b (jolt-9phg) — the pure-Chez runtime CLI (no Janet).
#
# bin/joltc execs `chez --script host/chez/cli.ss`, which loads the checked-in
# bootstrap seed (host/chez/seed/) + the zero-Janet spine and compiles+evals a
# Clojure -e expression entirely on Chez. This test drives bin/joltc and checks
# results: with only Chez installed, jolt runs end to end — no Janet at build or
# run time. (This harness is Janet only to spawn the process; the compile+eval is
# 100% Chez.)
#
# janet test/chez/cli-test.janet
(import ../../host/chez/driver :as d)
(var total 0) (var fails 0)
(defn ok [name pred &opt extra]
(++ total)
(if pred (printf "ok: %s" name)
(do (++ fails) (printf "FAIL: %s %s" name (or extra "")))))
(unless (d/chez-available?)
(print "chez not on PATH — skipping cli-test")
(os/exit 0))
(defn- joltc [expr]
(def proc (os/spawn ["bin/joltc" "-e" expr] :p {:out :pipe :err :pipe}))
(def out (string/trim (string (:read (proc :out) :all))))
(def err (string/trim (string (or (:read (proc :err) :all) ""))))
(def code (os/proc-wait proc))
[code out err])
(def cases
[["(+ 1 2)" "3"]
["(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) (fib 15)" "610"]
["(->> (range 10) (filter even?) (map #(* % %)) (reduce +))" "120"]
["(let [{:keys [a b] :or {b 99}} {:a 1}] [a b])" "[1 99]"]
["(require '[clojure.string :as s]) (s/upper-case \"hello\")" "HELLO"]
["(case 3 1 :a 2 :b :other)" ":other"]
["(reduce + (vals (reduce (fn [m k] (assoc m k (* k k))) {} [1 2 3])))" "14"]
["(map inc [1 2 3])" "(2 3 4)"]
["nil" ""]
# jolt-r8ku: runtime eval / load-string / defmacro on the Chez spine.
["(eval (quote (+ 1 2)))" "3"]
["(eval (list (quote +) 1 2 3))" "6"]
["(eval (quote (let [a 2 b 3] (* a b))))" "6"]
["(load-string \"(+ 1 2)\")" "3"]
["(load-string \"(def y 5) (* y y)\")" "25"]
["(load-string \"\")" ""]
["(map eval [(quote (+ 1 1)) (quote (* 3 3))])" "(2 9)"]
["(defmacro add1 [x] (list (quote +) x 1)) (add1 10)" "11"]
["(defmacro twice [x] `(do ~x ~x)) (twice (+ 2 3))" "5"]
["(defmacro m [x] `(+ ~x 1)) (m (m (m 0)))" "3"]
# jolt-byjr: concurrency — futures/pmap/promise on real OS threads (shared heap).
["(deref (future (+ 1 2)))" "3"]
["@(future (* 6 7))" "42"]
["(deref (future (mapv inc [1 2 3])))" "[2 3 4]"]
["(let [f (future (+ 1 1))] [(deref f) (deref f)])" "[2 2]"]
["(future? (future 1))" "true"]
["(future? 42)" "false"]
["(let [f (future 1)] (deref f) (future-done? f))" "true"]
["(let [f (future 1)] (deref f) (realized? f))" "true"]
["(let [f (future 42)] (deref f) (deref f 1000 :nope))" "42"]
["(vec (pmap inc [1 2 3]))" "[2 3 4]"]
["(vec (pmap + [1 2 3] [4 5 6]))" "[5 7 9]"]
["(vec (pcalls (fn [] 1) (fn [] 2)))" "[1 2]"]
["(vec (pvalues (+ 1 2) (+ 3 4)))" "[3 7]"]
# shared heap = JVM semantics (NOT Janet's isolated-heap snapshot): a captured
# atom is shared, so the future's swap! is visible to the parent.
["(let [a (atom 0)] (deref (future (swap! a inc))) @a)" "1"]
["(let [a (atom 0)] (dorun (pmap (fn [_] (swap! a inc)) [1 2 3 4])) @a)" "4"]
# promise blocks until delivered (JVM), unlike the Janet atom-shim.
["(let [p (promise)] (deliver p 7) @p)" "7"]
["(let [p (promise)] (future (deliver p :hi)) @p)" ":hi"]
# jolt-byjr: clojure.core.async on real-thread blocking channels.
["(require (quote [clojure.core.async :refer [chan go <! >! <!!]])) (def c (chan)) (go (>! c (+ 40 2))) (<!! c)" "42"]
["(require (quote [clojure.core.async :refer [chan go go-loop <! >! <!! close!]])) (def c (chan 5)) (go (>! c 1) (>! c 2) (>! c 3) (close! c)) (<!! (go-loop [o []] (let [v (<! c)] (if (nil? v) o (recur (conj o v))))))" "[1 2 3]"]
["(require (quote [clojure.core.async :refer [chan go <! >! <!!]])) (def x (chan)) (def y (chan)) (go (>! x 10)) (go (>! y 32)) (<!! (go (+ (<! x) (<! y))))" "42"]
["(require (quote [clojure.core.async :refer [chan go <! >! <!! alts!]])) (def x (chan)) (def y (chan)) (go (>! y :v)) (<!! (go (let [[v ch] (alts! [x y])] (and (= v :v) (= ch y)))))" "true"]
["(require (quote [clojure.core.async :refer [chan go go-loop <! >! <!! close!]])) (def c (chan 10 (map inc))) (go (>! c 1) (>! c 2) (>! c 3) (close! c)) (<!! (go-loop [o []] (let [v (<! c)] (if (nil? v) o (recur (conj o v))))))" "[2 3 4]"]
["(require (quote [clojure.core.async :refer [timeout <!!]])) (<!! (timeout 10)) :done" ":done"]
["(require (quote [clojure.core.async :refer [chan go <! >! <!!]])) (def ^:dynamic *x* 0) (<!! (binding [*x* 7] (go (<! (clojure.core.async/timeout 5)) *x*)))" "7"]
# jolt-byjr: async agents (serialized per-agent dispatch); await for determinism.
["(deref (agent 0))" "0"]
["(let [a (agent 0)] (send-off a + 5) (await a) (deref a))" "5"]
["(let [a (agent 1)] (send a + 6) (await a) (deref a))" "7"]
["(let [a (agent 0)] (dotimes [_ 100] (send a inc)) (await a) (deref a))" "100"]
["(agent-error (agent 0))" ""]
["(let [a (agent 0)] (send a (fn [_] (throw (ex-info \"boom\" {})))) (await a) (boolean (agent-error a)))" "true"]])
(each [src want] cases
(def [code out err] (joltc src))
(ok (string "joltc: " (if (> (length src) 48) (string (string/slice src 0 48) "...") src))
(and (= code 0) (= out want))
(string "[" code "] got " (string/format "%j" out) " want " (string/format "%j" want)
(if (= "" err) "" (string " err: " (string/slice err 0 (min 120 (length err))))))))
(printf "\ncli-test: %d/%d checks passed" (- total fails) total)
(os/exit (if (zero? fails) 0 1))

View file

@ -1,104 +0,0 @@
# Phase 1 (jolt-cf1q.2, inc 3d) — clojure.core prelude emission probe.
#
# The path to an `-e`-capable jolt-chez: emit the clojure.core tiers
# (jolt-core/clojure/core/NN-*.clj) through the SAME live Janet analyzer ->
# host/chez/emit pipeline, as a Scheme PRELUDE of `def-var!` forms. User code's
# `(var-deref "clojure.core" "<fn>")` then resolves the fn at runtime.
#
# Most core fns are NOT native-ops, so they must be emitted; the ones that
# reference host interop / native Janet ops / unimplemented primitives can't be
# emitted yet (each a clean "out of subset" emit error). This probe reports how
# far the emit gets per tier and aggregates the gap list — the punch-list the
# next increments chase down. Measurement tool, gated out of the default suite.
# JOLT_CHEZ_PRELUDE=1 janet test/chez/core-prelude-probe.janet
(import ../../src/jolt/api :as api)
(import ../../src/jolt/backend :as backend)
(import ../../src/jolt/reader :as r)
(import ../../src/jolt/types_ctx :as tctx)
(import ../../host/chez/emit :as emit)
(unless (os/getenv "JOLT_CHEZ_PRELUDE")
(print "skip: set JOLT_CHEZ_PRELUDE=1 to run the core-prelude emission probe")
(os/exit 0))
# load order — same as api/core-tiers (the kernel tier is bootstrap-compiled in
# the live system; here we just measure emit reach, so treat it like the rest).
(def tier-files
["00-syntax" "00-kernel" "10-seq" "20-coll" "25-sorted" "30-macros" "40-lazy" "50-io"])
(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)
# jolt reader forms are arrays of jolt VALUES; a symbol is a struct
# {:jolt/type :symbol :name "..."} (jolt symbols aren't Janet symbols).
(defn- sym-name [x]
(when (and (struct? x) (= :symbol (get x :jolt/type))) (get x :name)))
# A short label for a top-level form: the defn/def name, or the form head.
(defn- form-label [f]
(if (and (indexed? f) (> (length f) 1))
(let [head (or (sym-name (in f 0)) "?") nm (sym-name (in f 1))]
(if nm (string head " " nm) head))
(string/slice (string/format "%p" f) 0 40)))
# Pull the unsupported fn/op name out of an emit error message for aggregation.
(defn- gap-key [msg]
(def m (string msg))
(cond
(string/find "stdlib fn" m) (let [i (string/find "`" m)] (string "stdlib: " (string/slice m (inc i) (string/find "`" m (inc i)))))
(string/find "stdlib ref" m) (let [i (string/find "`" m)] (string "stdlib: " (string/slice m (inc i) (string/find "`" m (inc i)))))
(string/find "host call" m) "host-call"
(string/find "host ref" m) "host-ref"
(string/find "unhandled op" m) (string/slice m (max 0 (- (length m) 30)))
(string/find "unsupported literal" m) "unsupported-literal"
(string/slice m 0 (min 50 (length m)))))
# Macros are analyze-time only (the Janet analyzer expands them away before emit),
# so they don't belong in a RUNTIME prelude — skip them, don't count as gaps.
(defn- macro-form? [f]
(and (indexed? f) (> (length f) 0)
(let [h (sym-name (in f 0))] (and h (or (= h "defmacro") (= h "definline"))))))
(emit/set-prelude-mode! true)
(def ctx (api/init {:compile? true}))
(tctx/ctx-set-current-ns ctx "clojure.core")
(var total 0) (var compiled 0)
(def gaps @{}) # gap-key -> count
(def gap-examples @{}) # gap-key -> first form label that hit it
(each tf tier-files
(def src (slurp (string "jolt-core/clojure/core/" tf ".clj")))
(def forms (parse-all src))
(var t-total 0) (var t-ok 0)
(each f forms
(unless (macro-form? f)
(++ total) (++ t-total)
(def res (protect (emit/emit (backend/analyze-form ctx f))))
(if (res 0)
(do (++ compiled) (++ t-ok))
(let [k (gap-key (res 1))]
(put gaps k (+ 1 (or (get gaps k) 0)))
(unless (get gap-examples k) (put gap-examples k (form-label f)))))))
(printf " %-10s %3d/%-3d forms emit" tf t-ok t-total))
(printf "\nCore prelude emit reach: %d/%d top-level forms compile to Scheme" compiled total)
(printf "%d distinct gaps (fn/op the emit back end can't lower yet):" (length gaps))
(def sorted-gaps (sort-by (fn [k] (- (get gaps k))) (keys gaps)))
(each k sorted-gaps
(printf " %4d x %-34s e.g. %s" (get gaps k) k (get gap-examples k)))
(flush)
# 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 355)
(when (< compiled reach-floor)
(printf "REGRESSION: prelude emit reach %d < floor %d" compiled reach-floor)
(os/exit 1))

View file

@ -596,7 +596,7 @@
{:suite "host-interop / java.io.File" :label "getName" :expected "\"c.txt\"" :actual "(do (require '[clojure.java.io :as io]) (.getName (io/file \"/a/b/c.txt\")))"} {:suite "host-interop / java.io.File" :label "getName" :expected "\"c.txt\"" :actual "(do (require '[clojure.java.io :as io]) (.getName (io/file \"/a/b/c.txt\")))"}
{:suite "host-interop / java.io.File" :label "getPath joins" :expected "\"/a/b\"" :actual "(do (require '[clojure.java.io :as io]) (.getPath (io/file \"/a\" \"b\")))"} {:suite "host-interop / java.io.File" :label "getPath joins" :expected "\"/a/b\"" :actual "(do (require '[clojure.java.io :as io]) (.getPath (io/file \"/a\" \"b\")))"}
{:suite "host-interop / java.io.File" :label "isDirectory of repo dir" :expected "true" :actual "(do (require '[clojure.java.io :as io]) (.isDirectory (io/file \"docs\")))"} {:suite "host-interop / java.io.File" :label "isDirectory of repo dir" :expected "true" :actual "(do (require '[clojure.java.io :as io]) (.isDirectory (io/file \"docs\")))"}
{:suite "host-interop / java.io.File" :label "isFile of repo file" :expected "true" :actual "(do (require '[clojure.java.io :as io]) (.isFile (io/file \"project.janet\")))"} {:suite "host-interop / java.io.File" :label "isFile of repo file" :expected "true" :actual "(do (require '[clojure.java.io :as io]) (.isFile (io/file \"README.md\")))"}
{:suite "host-interop / java.io.File" :label "exists is false off-disk" :expected "false" :actual "(do (require '[clojure.java.io :as io]) (.exists (io/file \"/no/such/path/xyz\")))"} {:suite "host-interop / java.io.File" :label "exists is false off-disk" :expected "false" :actual "(do (require '[clojure.java.io :as io]) (.exists (io/file \"/no/such/path/xyz\")))"}
{:suite "host-interop / java.io.File" :label "file-seq yields File values" :expected "true" :actual "(do (require '[clojure.java.io :as io]) (every? (fn [f] (instance? java.io.File f)) (file-seq (io/file \"docs\"))))"} {:suite "host-interop / java.io.File" :label "file-seq yields File values" :expected "true" :actual "(do (require '[clojure.java.io :as io]) (every? (fn [f] (instance? java.io.File f)) (file-seq (io/file \"docs\"))))"}
{:suite "host-interop / java.io.File" :label "file-seq finds files" :expected "true" :actual "(do (require '[clojure.java.io :as io]) (pos? (count (filter (fn [f] (.isFile f)) (file-seq (io/file \"docs\"))))))"} {:suite "host-interop / java.io.File" :label "file-seq finds files" :expected "true" :actual "(do (require '[clojure.java.io :as io]) (pos? (count (filter (fn [f] (.isFile f)) (file-seq (io/file \"docs\"))))))"}
@ -2137,14 +2137,14 @@
{:suite "sorted / seq fn interop" :label "vec of sorted-set" :expected "[1 2 3]" :actual "(vec (sorted-set 3 1 2))"} {:suite "sorted / seq fn interop" :label "vec of sorted-set" :expected "[1 2 3]" :actual "(vec (sorted-set 3 1 2))"}
{:suite "sorted / seq fn interop" :label "into vec" :expected "[[1 :a] [2 :b]]" :actual "(into [] (sorted-map 2 :b 1 :a))"} {:suite "sorted / seq fn interop" :label "into vec" :expected "[[1 :a] [2 :b]]" :actual "(into [] (sorted-map 2 :b 1 :a))"}
{:suite "sorted / seq fn interop" :label "sorted-map-by 3way cmp" :expected "[3 2 1]" :actual "(vec (keys (sorted-map-by (fn [a b] (- b a)) 1 :a 2 :b 3 :c)))"} {:suite "sorted / seq fn interop" :label "sorted-map-by 3way cmp" :expected "[3 2 1]" :actual "(vec (keys (sorted-map-by (fn [a b] (- b a)) 1 :a 2 :b 3 :c)))"}
{:suite "io / slurp, spit, printf, flush (host-classified)" :label "slurp returns string" :expected "true" :actual "(string? (slurp \"project.janet\"))"} {:suite "io / slurp, spit, printf, flush (host-classified)" :label "slurp returns string" :expected "true" :actual "(string? (slurp \"README.md\"))"}
{:suite "io / slurp, spit, printf, flush (host-classified)" :label "slurp content" :expected "true" :actual "(do (require (quote [clojure.string :as s])) (s/includes? (slurp \"project.janet\") \"jolt\"))"} {:suite "io / slurp, spit, printf, flush (host-classified)" :label "slurp content" :expected "true" :actual "(do (require (quote [clojure.string :as s])) (s/includes? (slurp \"README.md\") \"jolt\"))"}
{:suite "io / slurp, spit, printf, flush (host-classified)" :label "spit + slurp round" :expected "\"hello\"" :actual "(do (spit \"/tmp/jolt-spit-test.txt\" \"hello\") (slurp \"/tmp/jolt-spit-test.txt\"))"} {:suite "io / slurp, spit, printf, flush (host-classified)" :label "spit + slurp round" :expected "\"hello\"" :actual "(do (spit \"/tmp/jolt-spit-test.txt\" \"hello\") (slurp \"/tmp/jolt-spit-test.txt\"))"}
{:suite "io / slurp, spit, printf, flush (host-classified)" :label "spit append" :expected "\"ab\"" :actual "(do (spit \"/tmp/jolt-spit-test.txt\" \"a\") (spit \"/tmp/jolt-spit-test.txt\" \"b\" :append true) (slurp \"/tmp/jolt-spit-test.txt\"))"} {:suite "io / slurp, spit, printf, flush (host-classified)" :label "spit append" :expected "\"ab\"" :actual "(do (spit \"/tmp/jolt-spit-test.txt\" \"a\") (spit \"/tmp/jolt-spit-test.txt\" \"b\" :append true) (slurp \"/tmp/jolt-spit-test.txt\"))"}
{:suite "io / slurp, spit, printf, flush (host-classified)" :label "printf formats" :expected "\"x=1 y=a\"" :actual "(with-out-str (printf \"x=%d y=%s\" 1 \"a\"))"} {:suite "io / slurp, spit, printf, flush (host-classified)" :label "printf formats" :expected "\"x=1 y=a\"" :actual "(with-out-str (printf \"x=%d y=%s\" 1 \"a\"))"}
{:suite "io / slurp, spit, printf, flush (host-classified)" :label "printf no newline" :expected "false" :actual "(do (require (quote [clojure.string :as s])) (s/includes? (with-out-str (printf \"%d\" 1)) \"\\n\"))"} {:suite "io / slurp, spit, printf, flush (host-classified)" :label "printf no newline" :expected "false" :actual "(do (require (quote [clojure.string :as s])) (s/includes? (with-out-str (printf \"%d\" 1)) \"\\n\"))"}
{:suite "io / slurp, spit, printf, flush (host-classified)" :label "flush returns nil" :expected "nil" :actual "(flush)"} {:suite "io / slurp, spit, printf, flush (host-classified)" :label "flush returns nil" :expected "nil" :actual "(flush)"}
{:suite "io / slurp, spit, printf, flush (host-classified)" :label "file-seq finds files" :expected "true" :actual "(do (require (quote [clojure.string :as s])) (boolean (some (fn [p] (s/ends-with? p \"project.janet\")) (file-seq \".\"))))"} {:suite "io / slurp, spit, printf, flush (host-classified)" :label "file-seq finds files" :expected "true" :actual "(do (require (quote [clojure.string :as s])) (boolean (some (fn [p] (s/ends-with? p \"README.md\")) (file-seq \".\"))))"}
{:suite "ns / ns-map, ns-unmap, ns-refers" :label "ns-map has var" :expected "true" :actual "(do (def nmv 1) (some? (get (ns-map (quote user)) (quote nmv))))"} {:suite "ns / ns-map, ns-unmap, ns-refers" :label "ns-map has var" :expected "true" :actual "(do (def nmv 1) (some? (get (ns-map (quote user)) (quote nmv))))"}
{:suite "ns / ns-map, ns-unmap, ns-refers" :label "ns-unmap removes" :expected "nil" :actual "(do (def nuv 1) (ns-unmap (quote user) (quote nuv)) (resolve (quote nuv)))"} {:suite "ns / ns-map, ns-unmap, ns-refers" :label "ns-unmap removes" :expected "nil" :actual "(do (def nuv 1) (ns-unmap (quote user) (quote nuv)) (resolve (quote nuv)))"}
{:suite "ns / ns-map, ns-unmap, ns-refers" :label "ns-refers sees refer" :expected "true" :actual "(do (require (quote clojure.string)) (refer (quote clojure.string)) (some? (get (ns-refers (quote user)) (quote join))))"} {:suite "ns / ns-map, ns-unmap, ns-refers" :label "ns-refers sees refer" :expected "true" :actual "(do (require (quote clojure.string)) (refer (quote clojure.string)) (some? (get (ns-refers (quote user)) (quote join))))"}

View file

@ -1,130 +0,0 @@
# Extract the case LIST (suite, label, actual) from the spec sources into corpus.edn.
#
# corpus.edn :expected is sourced from reference JVM Clojure by
# test/conformance/regen-corpus.clj — this extractor's :expected column is a
# placeholder. Use it to (re)derive the case list when adding spec rows, then run
# regen-corpus.clj to fill :expected from the JVM. Do not commit its raw output.
#
# Parses every test/spec/*.janet as DATA (no eval), pulls each
# (defspec "suite" [label expected actual] ...) triple. Run from repo root:
# janet test/chez/extract-corpus.janet
(use ../../src/jolt/reader) # not needed for parse, but keeps paths obvious
(defn parse-all [src]
(def p (parser/new))
(parser/consume p src)
(parser/eof p)
(def out @[])
(while (parser/has-more p) (array/push out (parser/produce p)))
out)
(defn edn-str [s]
# escape a Clojure-source string into an EDN/Janet string literal
(def b @`"`)
(each c s
(cond
(= c (chr `"`)) (buffer/push b `\"`)
(= c (chr "\\")) (buffer/push b "\\\\")
(= c (chr "\n")) (buffer/push b "\\n")
(= c (chr "\t")) (buffer/push b "\\t")
(buffer/push-byte b c)))
(buffer/push b `"`)
(string b))
(defn triples-from [form]
# form is (defspec "suite" [l e a] ...) as a parsed tuple
(when (and (indexed? form) (> (length form) 0) (= (first form) 'defspec))
(def suite (in form 1))
(def rows @[])
(each case (slice form 2)
(when (and (indexed? case) (>= (length case) 3))
(def label (in case 0))
(def expected (in case 1))
(def actual (in case 2))
# expected is either a Clojure-source string or the :throws keyword;
# actual is always a Clojure-source string. Skip non-literal rows.
(when (and (string? label) (string? actual)
(or (string? expected) (keyword? expected)))
(array/push rows {:suite suite :label label
:expected expected :actual actual})))
)
rows))
(def spec-dir "test/spec")
(def all @[])
(each f (sort (os/dir spec-dir))
(when (string/has-suffix? "-spec.janet" f)
(def path (string spec-dir "/" f))
(each form (parse-all (slurp path))
(when-let [rows (triples-from form)]
(array/concat all rows)))))
# --- fold in the inline conformance cases (jolt-ohtd) -------------------------
# test/integration/conformance-test.janet carries ~355 hand-written cases as a
# (def cases [["label" "expected" "actual"] ...]) vector — historically invisible to
# the corpus because extract only reads test/spec/. Pull them in too, deduped by
# :actual against the spec rows, so the host-neutral corpus is the union of both.
# Suites come from the file's `### ---- section ----` headers via a line scan (the
# parser drops comments, so section structure is recovered from raw text).
(def conf-path "test/integration/conformance-test.janet")
(defn- section-map [src]
# label-string -> section-name, by scanning raw lines: track the most recent
# `### ---- NAME ----` / `### ==== NAME ====` header above each `["label" ...]`.
(def out @{})
(var section "misc")
(each line (string/split "\n" src)
(def tl (string/trim line))
(cond
(string/has-prefix? "###" tl)
(let [body (string/trim (string/trim (string/trim tl "#") " ") "-= ")]
(when (> (length body) 0) (set section body)))
(string/has-prefix? "[\"" tl)
(let [close (string/find "\"" tl 2)]
(when close (put out (string/slice tl 2 close) section)))))
out)
(def conf-src (slurp conf-path))
(def sections (section-map conf-src))
(def seen-actual (tabseq [r :in all] (r :actual) true))
(var conf-added 0)
(each form (parse-all conf-src)
(when (and (indexed? form) (>= (length form) 3)
(= (first form) 'def) (= (in form 1) 'cases))
(each c (in form 2)
(when (and (indexed? c) (= 3 (length c))
(string? (in c 0)) (string? (in c 1)) (string? (in c 2)))
(def [label expected actual] [(in c 0) (in c 1) (in c 2)])
(unless (seen-actual actual)
(put seen-actual actual true)
(++ conf-added)
(array/push all {:suite (string "conformance / " (get sections label "misc"))
:label label :expected expected :actual actual}))))))
(printf "folded %d unique conformance cases (deduped by :actual)" conf-added)
# emit EDN-and-Janet-valid corpus. [suite label] is the canonical case id, so make
# it unique: a duplicate label within a suite gets " (N)" appended (jolt-3447 — the
# conformance fold can repeat a label, e.g. two "str/replace regex" rows). Rows are
# immutable structs, so disambiguate the label here at emit time.
(def label-seen @{})
(def out @"[\n")
(each row all
(def k (string (row :suite) "\x00" (row :label)))
(def n (get label-seen k))
(put label-seen k (if n (+ n 1) 1))
(def label (if n (string (row :label) " (" (+ n 1) ")") (row :label)))
(buffer/push out
(string " {:suite " (edn-str (row :suite))
" :label " (edn-str label)
" :expected " (if (keyword? (row :expected)) ":throws" (edn-str (row :expected)))
" :actual " (edn-str (row :actual)) "}\n")))
(buffer/push out "]\n")
# corpus.edn is JVM-sourced (regen-corpus.clj); writing the Janet-extracted answers
# here would clobber it. Only write the case list to a separate path when explicitly
# asked (then re-source :expected with regen-corpus.clj). The test runner imports
# this file but never sets the flag, so it has no side effect.
(def out-path (os/getenv "JOLT_EXTRACT_CORPUS_OUT"))
(if out-path
(do (spit out-path out)
(printf "extracted %d cases from %s into %s" (length all) spec-dir out-path))
(print "extract-corpus: set JOLT_EXTRACT_CORPUS_OUT=<path> to write the case list (corpus.edn is JVM-sourced via regen-corpus.clj)"))

View file

@ -1,126 +0,0 @@
# Chez Phase 3 inc8 (jolt-bzni) — the self-hosting bootstrap fixpoint.
#
# The zero-Janet spine (spine-test / run-corpus-zero-janet) proves the ON-CHEZ
# analyzer+emitter compile arbitrary Clojure faithfully. This proves the stronger
# property from self-hosting-bootstrap-research §4: the emitted system reproduces
# ITSELF. Two artifacts are re-emitted ON CHEZ by the loaded compiler:
#
# COMPILER IMAGE (jolt.ir + jolt.analyzer + jolt.backend-scheme)
# stage1 = Janet analyzer/emitter cross-compiles the sources (the bootstrap input)
# stage2 = the on-Chez compiler (from stage1) re-emits them
# stage3 = the on-Chez compiler (from stage2) re-emits them
# FIXPOINT: stage2 == stage3 (stage1 differs only in gensym numbering — the
# Janet build allocates more gensyms before reaching the compiler emit).
#
# CORE PRELUDE (clojure.core tiers + clojure.string/walk/template/edn/set/pprint)
# pstage2 = on-Chez compiler re-emits the prelude with the JANET prelude loaded
# pstage3 = ... with pstage2 loaded
# pstage4 = ... with pstage3 loaded
# FIXPOINT: pstage3 == pstage4. The prelude converges one stage later than the
# compiler because its MACRO expanders bake an auto-gensym id (foo#) at emit
# time, so a macro emitted by Janet (pstage2's loaded prelude) carries a
# different baked id than one emitted by Chez — only once BOTH stages load a
# Chez-emitted prelude (pstage3 onward) does it stabilize.
#
# Finally we load the FULLY Chez-emitted system (Chez prelude + Chez compiler
# image, NO Janet-emitted artifact in the loop) and run real cases, proving the
# fixpoint is a working compiler, not a degenerate stable one.
#
# janet test/chez/fixpoint-test.janet
(import ../../host/chez/driver :as d)
(import ../../host/chez/jolt-chez :as jc)
(var total 0) (var fails 0)
(defn ok [name pred &opt extra]
(++ total)
(if pred (printf "ok: %s" name)
(do (++ fails) (printf "FAIL: %s %s" name (or extra "")))))
(unless (d/chez-available?)
(print "chez not on PATH — skipping fixpoint-test")
(os/exit 0))
(def ctx (d/make-ctx))
(def jprelude (jc/ensure-prelude ctx))
# stage1: the Janet cross-compiled compiler image, cached by source fingerprint
# (same scheme as spine-test / run-corpus-zero-janet).
(defn- image-fingerprint []
(string/slice (string (hash (string/join
(map slurp ["jolt-core/jolt/ir.clj" "jolt-core/jolt/analyzer.clj"
"jolt-core/jolt/backend_scheme.clj" "host/chez/host-contract.ss"
"host/chez/compile-eval.ss"])))) 0))
(def tmp (or (os/getenv "TMPDIR") "/tmp"))
(def stage1 (string tmp "/jolt-compiler-image-" (image-fingerprint) ".ss"))
(d/ensure-compiler-image ctx stage1)
(printf "stage1 compiler image (Janet cross-compile): %d bytes" (length (slurp stage1)))
(flush)
(defn- bytes= [a b] (= (string (slurp a)) (string (slurp b))))
(defn- first-diff [a b]
(def s (string (slurp a))) (def t (string (slurp b)))
(def n (min (length s) (length t)))
(var i 0) (while (and (< i n) (= (s i) (t i))) (++ i))
(string "sizes " (length s) " vs " (length t) ", first diff at " i))
# ---- compiler-image fixpoint: stage2 == stage3 -------------------------------
(def s2 (string tmp "/jolt-fixpoint-img2-" (os/getpid) ".ss"))
(def s3 (string tmp "/jolt-fixpoint-img3-" (os/getpid) ".ss"))
(def [c2 e2] (d/emit-image-on-chez jprelude stage1 s2))
(ok "compiler image stage2 emits cleanly on Chez" (and (= c2 0) (os/stat s2))
(string "exit " c2 " " (string/slice e2 0 (min 300 (length e2)))))
(def [c3 e3] (d/emit-image-on-chez jprelude s2 s3))
(ok "compiler image stage3 emits cleanly on Chez" (and (= c3 0) (os/stat s3))
(string "exit " c3 " " (string/slice e3 0 (min 300 (length e3)))))
(when (and (os/stat s2) (os/stat s3))
(ok "compiler image: stage2 == stage3 (byte-for-byte fixpoint)" (bytes= s2 s3)
(first-diff s2 s3))
(ok "compiler image is substantial (> 80KB)" (> (length (slurp s2)) 80000)))
# ---- prelude fixpoint: pstage3 == pstage4 ------------------------------------
(def p2 (string tmp "/jolt-fixpoint-prelude2-" (os/getpid) ".ss"))
(def p3 (string tmp "/jolt-fixpoint-prelude3-" (os/getpid) ".ss"))
(def p4 (string tmp "/jolt-fixpoint-prelude4-" (os/getpid) ".ss"))
(def [pc2 pe2] (d/emit-image-on-chez jprelude stage1 p2 "jolt-emit-prelude"))
(ok "prelude pstage2 emits cleanly on Chez (from Janet prelude)" (and (= pc2 0) (os/stat p2))
(string "exit " pc2 " " (string/slice pe2 0 (min 300 (length pe2)))))
(when (os/stat p2)
(def [pc3 pe3] (d/emit-image-on-chez p2 stage1 p3 "jolt-emit-prelude"))
(ok "prelude pstage3 emits cleanly on Chez (from pstage2)" (and (= pc3 0) (os/stat p3))
(string "exit " pc3 " " (string/slice pe3 0 (min 300 (length pe3)))))
(when (os/stat p3)
(def [pc4 pe4] (d/emit-image-on-chez p3 stage1 p4 "jolt-emit-prelude"))
(ok "prelude pstage4 emits cleanly on Chez (from pstage3)" (and (= pc4 0) (os/stat p4))
(string "exit " pc4 " " (string/slice pe4 0 (min 300 (length pe4)))))
(when (os/stat p4)
(ok "prelude: pstage3 == pstage4 (byte-for-byte fixpoint)" (bytes= p3 p4)
(first-diff p3 p4))
(ok "prelude is substantial (> 250KB)" (> (length (slurp p3)) 250000)))))
# ---- the fully Chez-emitted system is a working compiler ----------------------
# Chez-emitted prelude (pstage3) + Chez-emitted compiler image (s2): no Janet
# artifact in the loop. Drive real compile+eval through it.
(def verify-cases
[["(let [x 1 y 2] (+ x y))" "3"]
["(when (> 5 3) (-> 10 (- 1) (* 2)))" "18"]
["(defn f [a b] (* a b)) (f 6 7)" "42"]
["(map inc [1 2 3])" "(2 3 4)"]
["(reduce + 0 (range 5))" "10"]
["(let [{:keys [a b]} {:a 7 :b 8}] (+ a b))" "15"]
["(filter even? (range 10))" "(0 2 4 6 8)"]
["(require '[clojure.string :as s]) (s/upper-case \"hi\")" "HI"]
["(cond (= 1 2) :a (= 1 1) :b :else :c)" ":b"]])
(when (and (os/stat p3) (os/stat s2))
(var vpass 0)
(each [src want] verify-cases
(def [code out _] (d/eval-zero-janet p3 s2 (string "(do " src ")")))
(when (and (= code 0) (= out want)) (++ vpass)))
(ok "fully Chez-emitted system (Chez prelude + Chez image) compiles+runs real cases"
(= vpass (length verify-cases))
(string vpass "/" (length verify-cases) " cases passed")))
# cleanup temp stages
(each p [s2 s3 p2 p3 p4] (when (os/stat p) (os/rm p)))
(printf "\nfixpoint-test: %d/%d checks passed" (- total fails) total)
(os/exit (if (zero? fails) 0 1))

View file

@ -1,63 +0,0 @@
# Phase 2 (jolt-cf1q.3) — which clojure.core names resolve to jolt-nil on Chez?
#
# The parity gate buckets a missing native generically as "apply non-procedure
# jolt-nil" — it doesn't NAME the fn. This probe does: it assembles the prelude,
# enumerates every clojure.core var name, then runs ONE Chez program that
# var-derefs each name (after loading prelude + post-prelude) and prints the ones
# that are still nil. That list is the shim punch-list for the next increment.
# JOLT_CHEZ_NIL_PROBE=1 janet test/chez/nil-names-probe.janet
(import ../../host/chez/driver :as d)
(import ../../src/jolt/types_ctx :as tctx)
(unless (os/getenv "JOLT_CHEZ_NIL_PROBE")
(print "skip: set JOLT_CHEZ_NIL_PROBE=1 to run the nil-names probe")
(os/exit 0))
(unless (d/chez-available?)
(print "skip: chez not on PATH")
(os/exit 0))
(def ctx (d/make-ctx))
# Collect every clojure.core var name (mapping keys are symbol structs).
(def names @{})
(each ns (tctx/all-ns ctx)
(when (= (get ns :name) "clojure.core")
(eachk sym (get ns :mappings)
(def n (cond (struct? sym) (get sym :name)
(string? sym) sym
(symbol? sym) (string sym)
nil))
(when n (put names n true)))))
(def name-list (sort (keys names)))
(printf "clojure.core has %d interned names" (length name-list))
# Assemble the prelude once.
(def [prelude-scm emitted total] (d/emit-core-prelude ctx))
(def prelude-path (string "/tmp/jolt-nil-probe-prelude-" (os/getpid) ".ss"))
(spit prelude-path prelude-scm)
(printf "prelude: %d/%d non-macro core forms emitted" emitted total)
# Build a Chez program that derefs each name and prints the nil ones.
(def buf @"")
(buffer/push buf "(import (chezscheme))\n(load \"host/chez/rt.ss\")\n")
(buffer/push buf "(set-chez-ns! \"clojure.core\")\n")
(buffer/push buf (string "(load " (string/format "%j" prelude-path) ")\n"))
(buffer/push buf "(load \"host/chez/post-prelude.ss\")\n")
(buffer/push buf "(set-chez-ns! \"user\")\n")
(each n name-list
(buffer/push buf
(string "(when (jolt-nil? (var-deref \"clojure.core\" " (string/format "%j" n)
")) (display " (string/format "%j" n) ") (newline))\n")))
(def prog-path (string "/tmp/jolt-nil-probe-" (os/getpid) ".ss"))
(spit prog-path buf)
(def proc (os/spawn ["chez" "--script" prog-path] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def nils (filter (fn [s] (> (length s) 0)) (string/split "\n" (string/trim (if out (string out) "")))))
(printf "\n%d clojure.core names resolve to jolt-nil on Chez:" (length nils))
(each n (sort nils) (print " " n))
(when (and err (> (length (string/trim (string err))) 0))
(printf "\nstderr:\n%s" (string err)))
(printf "\n(probe exit %d)" code)

View file

@ -1,212 +0,0 @@
# Phase 3 inc7 (jolt-qjr0) — FULL corpus on the ZERO-JANET spine.
#
# run-corpus-prelude.janet measures RUNTIME parity: it analyzes each case with the
# JANET-hosted analyzer (the oracle) and runs the emitted Scheme on Chez. This
# runner closes the last gap: it analyzes each case with the CHEZ-HOSTED analyzer
# (jolt.analyzer cross-compiled to Scheme, run on Chez over host-contract.ss) —
# read -> analyze -> IR -> emit -> eval, NO Janet in the loop (eval-zero-janet).
#
# So this is the real test of self-hosting: where run-corpus-prelude proves the
# RUNTIME is faithful, this proves the COMPILER-on-Chez is faithful. A case that
# the Janet analyzer compiles but the Chez analyzer can't surfaces here as a crash
# (analyzer/emitter raised) or a divergence (ran, wrong value). The buckets form
# the inc7 punch-list; genuinely host-coupled cases (Java interop, runtime eval)
# are deferred to Phase 4 / jolt-r8ku and allowlisted, like the prelude gate.
#
# JOLT_CHEZ_ZEROJANET_CORPUS=1 janet test/chez/run-corpus-zero-janet.janet
# JOLT_CORPUS_LIMIT=200 … (every-Nth stride, fast iteration)
(import ../../host/chez/driver :as d)
(import ../../host/chez/jolt-chez :as jc)
(unless (os/getenv "JOLT_CHEZ_ZEROJANET_CORPUS")
(print "skip: set JOLT_CHEZ_ZEROJANET_CORPUS=1 to run the zero-Janet corpus gate")
(os/exit 0))
(unless (d/chez-available?)
(print "skip: chez not on PATH")
(os/exit 0))
(def ctx (d/make-ctx))
(def prelude-path (jc/ensure-prelude ctx))
# Compiler image (jolt.ir + jolt.analyzer + jolt.backend-scheme cross-compiled to
# Scheme), cached by the same source fingerprint the spine-test uses.
(defn- image-fingerprint []
(string/slice (string (hash (string/join
(map slurp ["jolt-core/jolt/ir.clj" "jolt-core/jolt/analyzer.clj"
"jolt-core/jolt/backend_scheme.clj" "host/chez/host-contract.ss"
"host/chez/compile-eval.ss"])))) 0))
(def image-path
(string (or (os/getenv "TMPDIR") "/tmp") "/jolt-compiler-image-" (image-fingerprint) ".ss"))
(def t0 (os/clock))
(d/ensure-compiler-image ctx image-path)
(printf "prelude + compiler image ready (%.1fs)" (- (os/clock) t0))
(flush)
(def corpus (parse (slurp "test/chez/corpus.edn")))
(def cases
(if-let [n (os/getenv "JOLT_CORPUS_LIMIT")]
(let [stride (max 1 (math/floor (/ (length corpus) (scan-number n))))]
(seq [i :range [0 (length corpus) stride]] (in corpus i)))
corpus))
# Known divergences/crashes: cases the Chez-hosted compiler can't yet handle that
# are tracked elsewhere (NOT analyzer-faithfulness bugs). Tolerated so the gate
# fails only on a NEW regression. Keyed by label.
# - host interop (Java classes / constructors / .method on host types): Phase 4
# jolt-cf1q.7. Same family the prelude gate buckets as crashes.
# - eval / load-string / read->eval: the jolt-r8ku tail (runtime compiler entry).
(def known-fail
# Conformance gaps vs the JVM spec: cases jolt does not match because it has no
# JVM host (no Class objects / Java arrays / BigDecimal), supports the :jolt
# reader-conditional feature, or prints its own forms for transients/atoms.
@{# --- host classes: a class token resolves to a name string, not a Class ------
"class name evaluates to canonical string" true
"class number" true "class string" true "class keyword" true
"definterface defines" true
"getMessage on a thrown string" true
"type of record" true "chunked-seq? always false" true
"^Type tag on var" true "symbol hint -> :tag" true
"lists extended type" true "seq of tags" true
"close on throw" true # duck-typed with-open close, no .close interop
"macroexpand-1" true # returns a value, not the JVM Cons form
"ns-imports empty user" true
"bean is the map" true "proxy resolves nil" true
"unchecked-char" true
# --- *in* is a map on Chez, not a JVM Reader --------------------------------
"*in* is bound" true "*in* bound" true
# --- no BigDecimal type -----------------------------------------------------
"bigdec" true "bigdec int M" true "bigdec suffix M" true
# --- printer: jolt renders its own forms (transient/atom/Infinity, no
# print-method multimethod integration) where the JVM prints #object ------
"transient vector" true "transient map" true
"atom override fires nested" true "inf inside coll" true "pr-str Infinity" true
"defmethod overrides a record, top level" true
"defmethod fires nested in a map" true "defmethod fires through prn" true
"direct builtin override" true "methods table inspectable" true
# --- reader: :jolt reader-conditional feature + syntax-quote literal collapse -
"reader conditional" true "reader cond :jolt" true "reader cond no match" true
"reader cond splice" true "reader cond splice no match" true
"nil nested" true "bool nested" true
"source order through syntax-quote" true # syntax-quote map: hash, not source, order
# --- Java arrays are distinct host objects, not seqs --------------------------
"make-array" true "into-array" true "to-array" true "aclone vec" true
"boolean-array" true "int-array" true "long-array" true "double-array" true
"float-array" true "short-array" true "doubles" true "floats" true
"reader over char[]" true
# --- atom class identity not mapped on Chez ---------------------------------
"atom?" true "instance? Atom" true
# --- future-cancel races completion (timing-dependent) -----------------------
"cancel an in-flight future returns true" true
"future-cancelled? after cancel" true
# --- (fn* foo) with no param vector throws; the JVM builds a fn object --------
"no param vector" true})
# Cases that BLOCK forever on a shared-heap / JVM host (profile.edn :bucket
# :timeout) — skip them, like :throws: a single hung case would stall the whole
# batched process. (deref of an undelivered promise blocks on the JVM and now on
# Chez; Janet's non-blocking atom-shim returned nil.)
(def skip-blocking @{"promise undelivered" true})
(var pass 0)
(def crashes @[]) # nonzero chez exit (analyzer/emitter raised, or runtime gap)
(def diverged @[]) # ran, wrong value (a real Chez-compiler divergence)
(def known-hit @[])
(def crash-keys @{})
(defn- bucket [tbl k] (put tbl k (+ 1 (or (get tbl k) 0))))
# Group a chez stderr message into a coarse reason for the punch-list.
(defn- crash-reason [m]
(def m (string/trim m))
(cond
(string/find "unsupported stdlib" m) "emit: unsupported stdlib fn"
(string/find "unsupported host" m) "emit: unsupported host call"
(string/find "host-static" m) "emit: host-static"
(string/find "syntax-quote" m) "form-syntax-quote-lower"
(string/find "uncompil" m) "analyzer: uncompilable"
(string/find "Unknown class" m) "runtime: unknown class"
(string/find "No constructor" m) "runtime: no constructor"
(string/find "No method" m) "runtime: no method"
(string/find "not a fn" m) "runtime: not a fn"
(string/find "not seqable" m) "runtime: not seqable"
(string/find "not a transient" m) "runtime: not a transient"
(string/find "integer->char" m) "runtime: integer->char"
(string/find "non-condition value" m)
(let [i (string/find "non-condition value" m)]
(string "raised: " (string/slice m (+ i 20) (min (length m) (+ i 60)))))
(string/slice m 0 (min 56 (length m)))))
(def t1 (os/clock))
(var throws 0)
# Build the evaluable case list (skip :throws), keyed by index (labels aren't
# unique across suites). Each pair carries EXPECTED + ACTUAL as SEPARATE source
# strings; the runner evaluates ACTUAL as its own top-level program (so its
# top-level `do` unrolls and a macro defined in the program is usable later —
# matching certify.clj's eval-isolated) and compares to EXPECTED with =. Wrapping
# in (= E A) would nest ACTUAL's do and break runtime defmacro (jolt-cf1q.7).
(def rows-by-idx @{})
(def pairs @[])
(eachp [i row] cases
(def {:expected e :actual a :label l} row)
(if (or (= e :throws) (get skip-blocking l))
(++ throws)
(let [key (string i)]
(put rows-by-idx key row)
(array/push pairs [key e a]))))
(defn- handle [key verdict]
(def row (get rows-by-idx key))
(def l (get row :label))
(case (first verdict)
:pass (++ pass)
:crash (let [k (crash-reason (get verdict 1))] (bucket crash-keys k) (array/push crashes [l k]))
:diverge (if (known-fail l) (array/push known-hit l)
(array/push diverged [l (string "got " (get verdict 1))]))))
(if (os/getenv "JOLT_ZJ_PERCASE")
# slow per-case path (each case its own chez process) — for isolating a hang/crash.
# Eval ACTUAL and EXPECTED top-level (separate processes), compare printed forms.
(each [key e a] pairs
(def [acode aout aerr] (d/eval-zero-janet prelude-path image-path a))
(def [_ eout _] (d/eval-zero-janet prelude-path image-path e))
(handle key (cond (not= acode 0) [:crash aerr] (= aout eout) [:pass] [:diverge aout])))
# fast batched path: one chez process loads the runtime once, runs all cases
(let [{:results r :code c :stderr se :count n} (d/eval-corpus-zero-janet prelude-path image-path pairs)]
(when (< n (length pairs))
(printf "WARNING: batched runner returned %d/%d results (chez exit %d): %s"
n (length pairs) c (string/slice se 0 (min 200 (length se)))))
(each [key _] pairs
(handle key (or (get r key) [:crash (string "no result (batch aborted) " se)])))))
(def n-eval (+ pass (length crashes) (length diverged) (length known-hit)))
(printf "\nZero-Janet corpus parity: %d/%d evaluated cases pass (%.1fs)" pass n-eval (- (os/clock) t1))
(printf " crash: %d NEW divergence: %d known: %d (throws skipped: %d)"
(length crashes) (length diverged) (length known-hit) throws)
(defn- report [title tbl]
(when (> (length tbl) 0)
(printf "\n%s:" title)
(each k (sort-by (fn [k] (- (get tbl k))) (keys tbl))
(printf " %4d x %s" (get tbl k) k))))
(report "crash reasons" crash-keys)
(when (os/getenv "JOLT_DUMP_CRASH_LABELS")
(printf "\nCRASH LABELS:")
(each [l k] (sort-by (fn [pair] (get pair 1)) crashes)
(printf " [%s] :: %s" k l))
(printf "\nKNOWN-HIT LABELS:")
(each l (sort known-hit) (printf " %s" l)))
(when (> (length diverged) 0)
(printf "\nNEW divergences (ran, wrong value) — gate FAILS:")
(each [l m] (slice diverged 0 (min 40 (length diverged)))
(printf " [%s] %s" l m)))
(when (> (length known-hit) 0)
(printf "\n%d known (allowlisted) failures tolerated." (length known-hit)))
(flush)
# Regression floor: cases that pass against the JVM corpus. The gate fails on any
# NEW divergence or if pass drops below the floor. Raise as host gaps close.
(def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_ZJ_FLOOR") "2678")))
(def floor (if (os/getenv "JOLT_CORPUS_LIMIT") 0 base-floor))
(when (or (> (length diverged) 0) (< pass floor))
(printf "REGRESSION: pass %d < floor %d or %d new divergence(s)" pass floor (length diverged)))
(os/exit (if (or (> (length diverged) 0) (< pass floor)) 1 0))

View file

@ -1,97 +0,0 @@
# Chez Phase 3 inc6 (jolt-hs9n) — the zero-Janet spine.
#
# Validates that the analyzer + emitter, cross-compiled to Scheme and run ON CHEZ
# over the host contract (host-contract.ss), compile and run macro-free Clojure
# from source with NO Janet in the loop: read (reader.ss) -> analyze (jolt.analyzer
# on Chez) -> IR -> emit (jolt.backend-scheme on Chez) -> eval.
#
# Oracle = the Janet-hosted analyzer through the SAME Chez emitter/RT/printer
# (d/eval-e-with-prelude): the only difference under test is WHERE analysis runs
# (Janet vs Chez), so equal stdout means moving analysis onto Chez is behavior-
# preserving. Macros (let/when/->/defn) are inc6b (jolt-r8ku, runtime macros).
#
# janet test/chez/spine-test.janet
(import ../../src/jolt/api :as api)
(import ../../host/chez/driver :as d)
(import ../../host/chez/jolt-chez :as jc)
(var total 0) (var fails 0)
(defn ok [name pred &opt extra]
(++ total)
(if pred (printf "ok: %s" name)
(do (++ fails) (printf "FAIL: %s %s" name (or extra "")))))
(unless (d/chez-available?)
(print "chez not on PATH — skipping spine-test")
(os/exit 0))
(def ctx (d/make-ctx))
(def prelude-path (jc/ensure-prelude ctx))
# compiler image cache, keyed by the cross-compiled sources + the host contract.
(defn- image-fingerprint []
(string/slice (string (hash (string/join
(map slurp ["jolt-core/jolt/ir.clj" "jolt-core/jolt/analyzer.clj"
"jolt-core/jolt/backend_scheme.clj" "host/chez/host-contract.ss"
"host/chez/compile-eval.ss"])))) 0))
(def image-path
(string (or (os/getenv "TMPDIR") "/tmp") "/jolt-compiler-image-" (image-fingerprint) ".ss"))
(d/ensure-compiler-image ctx image-path)
# Each case: the Chez-hosted spine value must equal the Janet-hosted oracle value
# (both printed via jolt-final-str on Chez).
(defn check [src]
(def [ocode oout oerr] (d/eval-e-with-prelude ctx src prelude-path))
(def [acode aout aerr] (d/eval-zero-janet prelude-path image-path src))
(cond
(= ocode :emit-err) (ok src false (string "oracle emit-err: " oout))
(not (zero? acode)) (ok src false (string "zero-janet exit " acode ": " aerr " | out=" aout))
(ok src (= oout aout) (string "chez=" aout " oracle=" oout))))
# macro-free forms: handled specials (if/do/fn*), native ops, consts, invoke.
(each src
["(if true 10 20)"
"(if false 10 20)"
"(do 1 2 3)"
"(+ 1 2)"
"(- 10 3 2)"
"((fn* [x] (* x x)) 7)"
"((fn* [x] (+ x 1)) 5)"
"((fn* [a b] (+ a b)) 3 4)"
"(if (< 3 5) :yes :no)"
"(if (> 3 5) :yes :no)"
"((fn* [x] (if x :t :f)) true)"
"(do (if true 1 2) (* 6 7))"
"((fn* [n] (* n n n)) 4)"
"(< 1 2)"
"(= 5 5)"]
(check src))
# inc6b (jolt-r9lm): runtime macros — the on-Chez analyzer expands core macros
# (emitted into the prelude as expander fns + a macro flag). Same oracle: the
# Janet analyzer expands them at analyze time, the value must match.
(each src
["(when true 1)"
"(when false 1)"
"(when true 1 2 3)"
"(when-not false 5)"
"(let [a 1] (+ a 2))"
"(let [a 1 b 2] (+ a b))"
"(let [a 1 b (+ a 1)] (* a b))"
"(-> 1 inc inc)"
"(-> 5 (- 2))"
"(->> 3 (- 10))"
"(and 1 2 3)"
"(and 1 false 3)"
"(or nil 5)"
"(or false nil 7)"
"(cond false 1 true 2)"
"(cond false 1 :else 3)"
"(if-not false :a :b)"
"(do (defn f [x] (* x x)) (f 6))"
"(do (defn g [x y] (+ x y 1)) (g 3 4))"
"(let [a 1] (when (< a 5) (-> a inc inc)))"]
(check src))
(printf "\n%d/%d ok" (- total fails) total)
(when (> fails 0) (os/exit 1))

View file

@ -114,27 +114,27 @@
{:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.getName (io/file \"/a/b/c.txt\")))" :expected "c.txt"} {:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.getName (io/file \"/a/b/c.txt\")))" :expected "c.txt"}
{:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.getPath (io/file \"/a\" \"b\")))" :expected "/a/b"} {:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.getPath (io/file \"/a\" \"b\")))" :expected "/a/b"}
{:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.isDirectory (io/file \"docs\")))" :expected "true"} {:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.isDirectory (io/file \"docs\")))" :expected "true"}
{:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.isFile (io/file \"project.janet\")))" :expected "true"} {:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.isFile (io/file \"README.md\")))" :expected "true"}
{:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.isFile (io/file \"docs\")))" :expected "false"} {:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.isFile (io/file \"docs\")))" :expected "false"}
{:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.exists (io/file \"/no/such/path/xyz\")))" :expected "false"} {:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.exists (io/file \"/no/such/path/xyz\")))" :expected "false"}
{:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.exists (io/file \"project.janet\")))" :expected "true"} {:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (.exists (io/file \"README.md\")))" :expected "true"}
{:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (instance? java.io.File (io/file \"/a/b\")))" :expected "true"} {:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (instance? java.io.File (io/file \"/a/b\")))" :expected "true"}
{:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (instance? java.io.File \"/a/b\"))" :expected "false"} {:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (instance? java.io.File \"/a/b\"))" :expected "false"}
{:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (str (type (io/file \"/a\"))))" :expected ":jolt/file"} {:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (str (type (io/file \"/a\"))))" :expected ":jolt/file"}
{:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (every? (fn [f] (instance? java.io.File f)) (file-seq (io/file \"docs\"))))" :expected "true"} {:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (every? (fn [f] (instance? java.io.File f)) (file-seq (io/file \"docs\"))))" :expected "true"}
{:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (pos? (count (filter (fn [f] (.isFile f)) (file-seq (io/file \"docs\"))))))" :expected "true"} {:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (pos? (count (filter (fn [f] (.isFile f)) (file-seq (io/file \"docs\"))))))" :expected "true"}
{:suite "io" :expr "(do (require (quote [clojure.string :as s])) (boolean (some (fn [p] (s/ends-with? p \"project.janet\")) (file-seq \".\"))))" :expected "true"} {:suite "io" :expr "(do (require (quote [clojure.string :as s])) (boolean (some (fn [p] (s/ends-with? p \"README.md\")) (file-seq \".\"))))" :expected "true"}
{:suite "io" :expr "(string? (slurp \"project.janet\"))" :expected "true"} {:suite "io" :expr "(string? (slurp \"README.md\"))" :expected "true"}
{:suite "io" :expr "(do (spit \"/tmp/jolt-io-test.txt\" \"hello\") (slurp \"/tmp/jolt-io-test.txt\"))" :expected "hello"} {:suite "io" :expr "(do (spit \"/tmp/jolt-io-test.txt\" \"hello\") (slurp \"/tmp/jolt-io-test.txt\"))" :expected "hello"}
{:suite "io" :expr "(do (spit \"/tmp/jolt-io-test.txt\" \"a\") (spit \"/tmp/jolt-io-test.txt\" \"b\" :append true) (slurp \"/tmp/jolt-io-test.txt\"))" :expected "ab"} {:suite "io" :expr "(do (spit \"/tmp/jolt-io-test.txt\" \"a\") (spit \"/tmp/jolt-io-test.txt\" \"b\" :append true) (slurp \"/tmp/jolt-io-test.txt\"))" :expected "ab"}
{:suite "io" :expr "(flush)" :expected ""} {:suite "io" :expr "(flush)" :expected ""}
{:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (string? (slurp (io/file \"project.janet\"))))" :expected "true"} {:suite "io" :expr "(do (require (quote [clojure.java.io :as io])) (string? (slurp (io/file \"README.md\"))))" :expected "true"}
{:suite "ioreader" :expr "(str (char-array \"abc\"))" :expected "(\\a \\b \\c)"} {:suite "ioreader" :expr "(str (char-array \"abc\"))" :expected "(\\a \\b \\c)"}
{:suite "ioreader" :expr "(.read (StringReader. (apply str (char-array \"Qz\"))))" :expected "81"} {:suite "ioreader" :expr "(.read (StringReader. (apply str (char-array \"Qz\"))))" :expected "81"}
{:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (.read (io/reader (char-array \"abc\"))))" :expected "97"} {:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (.read (io/reader (char-array \"abc\"))))" :expected "97"}
{:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (.read (io/reader (StringReader. \"k\"))))" :expected "107"} {:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (.read (io/reader (StringReader. \"k\"))))" :expected "107"}
{:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (slurp (io/reader (char-array \"xyz\"))))" :expected "xyz"} {:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (slurp (io/reader (char-array \"xyz\"))))" :expected "xyz"}
{:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (string? (slurp (io/reader \"project.janet\"))))" :expected "true"} {:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (string? (slurp (io/reader \"README.md\"))))" :expected "true"}
{:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (.toString (.toURL (io/file \"/tmp/x\"))))" :expected "file:/tmp/x"} {:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (.toString (.toURL (io/file \"/tmp/x\"))))" :expected "file:/tmp/x"}
{:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (.toURI (io/file \"/tmp/x\")))" :expected "file:/tmp/x"} {:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (.toURI (io/file \"/tmp/x\")))" :expected "file:/tmp/x"}
{:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (.getPath (.toURL (io/file \"/tmp/x\"))))" :expected "/tmp/x"} {:suite "ioreader" :expr "(do (require (quote [clojure.java.io :as io])) (.getPath (.toURL (io/file \"/tmp/x\"))))" :expected "/tmp/x"}

View file

@ -1,149 +0,0 @@
(ns clojure.data-test.diff
(:require [clojure.test :refer [deftest is testing]]
;; NOTE (jolt): sequential-diff expectations corrected to match real Clojure —
;; clojure.data pads only to the max differing index (e.g. (diff [1 2 3] [1 9 3])
;; -> a=[nil 2], not [nil 2 nil]). The upstream clojurust fixtures had this wrong.
[clojure.data :refer [diff]]))
;; ── Atoms ────────────────────────────────────────────────────────────────────
(deftest test-diff-equal-atoms
(testing "equal atoms"
(is (= [nil nil :a] (diff :a :a)))
(is (= [nil nil 1] (diff 1 1)))
(is (= [nil nil "hello"] (diff "hello" "hello")))
(is (= [nil nil nil] (diff nil nil)))
(is (= [nil nil true] (diff true true)))))
(deftest test-diff-unequal-atoms
(testing "unequal atoms"
(is (= [:a :b nil] (diff :a :b)))
(is (= [1 2 nil] (diff 1 2)))
(is (= ["a" "b" nil] (diff "a" "b")))
(is (= [nil 1 nil] (diff nil 1)))
(is (= [true false nil] (diff true false)))))
;; ── Maps ─────────────────────────────────────────────────────────────────────
(deftest test-diff-equal-maps
(testing "equal maps"
(is (= [nil nil {:a 1 :b 2}] (diff {:a 1 :b 2} {:a 1 :b 2})))
(is (= [nil nil {}] (diff {} {})))))
(deftest test-diff-maps-only-in-a
(testing "keys only in a"
(let [[a b both] (diff {:a 1 :b 2} {:a 1})]
(is (= {:b 2} a))
(is (nil? b))
(is (= {:a 1} both)))))
(deftest test-diff-maps-only-in-b
(testing "keys only in b"
(let [[a b both] (diff {:a 1} {:a 1 :b 2})]
(is (nil? a))
(is (= {:b 2} b))
(is (= {:a 1} both)))))
(deftest test-diff-maps-different-values
(testing "same keys, different values"
(let [[a b both] (diff {:a 1 :b 2} {:a 1 :b 9})]
(is (= {:b 2} a))
(is (= {:b 9} b))
(is (= {:a 1} both)))))
(deftest test-diff-maps-nested
(testing "nested maps"
(let [[a b both] (diff {:a {:x 1 :y 2}} {:a {:x 1 :z 3}})]
(is (= {:a {:y 2}} a))
(is (= {:a {:z 3}} b))
(is (= {:a {:x 1}} both)))))
(deftest test-diff-maps-disjoint
(testing "completely disjoint maps"
(let [[a b both] (diff {:a 1} {:b 2})]
(is (= {:a 1} a))
(is (= {:b 2} b))
(is (nil? both)))))
;; ── Sets ─────────────────────────────────────────────────────────────────────
(deftest test-diff-equal-sets
(testing "equal sets"
(is (= [nil nil #{1 2 3}] (diff #{1 2 3} #{1 2 3})))
(is (= [nil nil #{}] (diff #{} #{})))))
(deftest test-diff-sets
(testing "overlapping sets"
(let [[a b both] (diff #{1 2 3} #{2 3 4})]
(is (= #{1} a))
(is (= #{4} b))
(is (= #{2 3} both)))))
(deftest test-diff-disjoint-sets
(testing "disjoint sets"
(let [[a b both] (diff #{1 2} #{3 4})]
(is (= #{1 2} a))
(is (= #{3 4} b))
(is (nil? both)))))
;; ── Vectors / Sequential ────────────────────────────────────────────────────
(deftest test-diff-equal-vectors
(testing "equal vectors"
(is (= [nil nil [1 2 3]] (diff [1 2 3] [1 2 3])))
(is (= [nil nil []] (diff [] [])))))
(deftest test-diff-vectors-same-length
(testing "same length, different elements"
(let [[a b both] (diff [1 2 3] [1 9 3])]
(is (= [nil 2] a))
(is (= [nil 9] b))
(is (= [1 nil 3] both)))))
(deftest test-diff-vectors-different-length
(testing "different lengths"
(let [[a b both] (diff [1 2 3] [1 2])]
(is (= [nil nil 3] a))
(is (nil? b))
(is (= [1 2] both)))
(let [[a b both] (diff [1] [1 2 3])]
(is (nil? a))
(is (= [nil 2 3] b))
(is (= [1] both)))))
(deftest test-diff-lists
(testing "lists treated as sequential"
(let [[a b both] (diff '(1 2 3) '(1 9 3))]
(is (= [nil 2] a))
(is (= [nil 9] b))
(is (= [1 nil 3] both)))))
;; ── Mixed types ─────────────────────────────────────────────────────────────
(deftest test-diff-mixed-types
(testing "different partition types treated as atoms"
(is (= [{:a 1} [1 2] nil] (diff {:a 1} [1 2])))
(is (= [#{1} [1] nil] (diff #{1} [1])))
(is (= [1 :a nil] (diff 1 :a)))))
;; ── Nil handling ────────────────────────────────────────────────────────────
(deftest test-diff-nil
(testing "nil vs non-nil"
(is (= [nil 1 nil] (diff nil 1)))
(is (= [1 nil nil] (diff 1 nil)))
(is (= [nil {:a 1} nil] (diff nil {:a 1})))))
;; ── Deeply nested ───────────────────────────────────────────────────────────
(deftest test-diff-deeply-nested
(testing "deeply nested structures"
(let [[a b both] (diff {:a {:b {:c 1}}} {:a {:b {:c 2}}})]
(is (= {:a {:b {:c 1}}} a))
(is (= {:a {:b {:c 2}}} b))
(is (nil? both))))
(testing "deeply nested with shared keys"
(let [[a b both] (diff {:a {:b 1 :c 2}} {:a {:b 1 :c 9}})]
(is (= {:a {:c 2}} a))
(is (= {:a {:c 9}} b))
(is (= {:a {:b 1}} both)))))

View file

@ -1,107 +0,0 @@
(ns clojure.edn-test.read-string
(:require [clojure.edn :as edn]
[clojure.test :refer [are deftest is testing]]))
(deftest test-read-string-scalars
(testing "nil, booleans"
(is (nil? (edn/read-string "nil")))
(is (true? (edn/read-string "true")))
(is (false? (edn/read-string "false"))))
(testing "integers"
(is (= 0 (edn/read-string "0")))
(is (= 42 (edn/read-string "42")))
(is (= -1 (edn/read-string "-1")))
(is (= 1000000000000 (edn/read-string "1000000000000"))))
(testing "floats"
(is (= 3.14 (edn/read-string "3.14")))
(is (= -0.5 (edn/read-string "-0.5")))
(is (= 1.0 (edn/read-string "1.0"))))
(testing "bigints"
(is (= 42N (edn/read-string "42N"))))
(testing "bigdecimals"
(is (= 3.14M (edn/read-string "3.14M"))))
(testing "strings"
(is (= "" (edn/read-string "\"\"")))
(is (= "hello" (edn/read-string "\"hello\"")))
(is (= "line1\nline2" (edn/read-string "\"line1\\nline2\"")))
(is (= "tab\there" (edn/read-string "\"tab\\there\""))))
(testing "characters"
(is (= \a (edn/read-string "\\a")))
(is (= \newline (edn/read-string "\\newline")))
(is (= \space (edn/read-string "\\space")))
(is (= \tab (edn/read-string "\\tab"))))
(testing "keywords"
(is (= :foo (edn/read-string ":foo")))
(is (= :bar/baz (edn/read-string ":bar/baz"))))
(testing "symbols"
(is (= 'foo (edn/read-string "foo")))
(is (= 'bar/baz (edn/read-string "bar/baz")))))
(deftest test-read-string-collections
(testing "vectors"
(is (= [] (edn/read-string "[]")))
(is (= [1 2 3] (edn/read-string "[1 2 3]")))
(is (= [1 [2 3] 4] (edn/read-string "[1 [2 3] 4]"))))
(testing "lists"
(is (= '() (edn/read-string "()")))
(is (= '(1 2 3) (edn/read-string "(1 2 3)")))
(is (= '(+ 1 2) (edn/read-string "(+ 1 2)"))))
(testing "maps"
(is (= {} (edn/read-string "{}")))
(is (= {:a 1} (edn/read-string "{:a 1}")))
(is (= {:a 1 :b 2} (edn/read-string "{:a 1 :b 2}")))
(is (= {:nested {:deep true}} (edn/read-string "{:nested {:deep true}}"))))
(testing "sets"
(is (= #{} (edn/read-string "#{}")))
(is (= #{1 2 3} (edn/read-string "#{1 2 3}"))))
(testing "mixed nested"
(is (= {:users [{:name "Alice" :age 30}
{:name "Bob" :age 25}]}
(edn/read-string "{:users [{:name \"Alice\" :age 30} {:name \"Bob\" :age 25}]}")))))
(deftest test-read-string-tagged-literals
(testing "#uuid"
(let [u (edn/read-string "#uuid \"f81d4fae-7dec-11d0-a765-00a0c91e6bf6\"")]
(is (uuid? u))
(is (= u (edn/read-string "#uuid \"f81d4fae-7dec-11d0-a765-00a0c91e6bf6\""))))))
(deftest test-read-string-eof
(testing "empty string with :eof option"
(is (= :eof (edn/read-string {:eof :eof} "")))
(is (= nil (edn/read-string {:eof nil} "")))
(is (= 42 (edn/read-string {:eof 42} ""))))
(testing "whitespace-only with :eof option"
(is (= :done (edn/read-string {:eof :done} " "))))
(testing "nil input returns nil"
(is (nil? (edn/read-string nil)))))
(deftest test-read-string-comments
(testing "comments are skipped"
(is (= 42 (edn/read-string "; this is a comment\n42"))))
(testing "discard reader macro"
(is (= 2 (edn/read-string "#_ 1 2")))))
(deftest test-read-string-only-first-form
(testing "reads only the first form"
(is (= 1 (edn/read-string "1 2 3")))
(is (= :a (edn/read-string ":a :b :c")))))
(deftest test-read-string-ratios
(testing "ratios"
(is (= 1/2 (edn/read-string "1/2")))
(is (= 3/4 (edn/read-string "3/4")))))

View file

@ -1,101 +0,0 @@
(ns clojure.walk-test.walk
(:require [clojure.test :refer [deftest is testing]]
[clojure.walk :as w]))
(deftest test-walk
(testing "walk with identity"
(is (= [1 2 3] (w/walk identity identity [1 2 3])))
(is (= '(1 2 3) (w/walk identity identity '(1 2 3))))
(is (= #{1 2 3} (w/walk identity identity #{1 2 3}))))
(testing "walk with inner transform"
(is (= [2 3 4] (w/walk inc identity [1 2 3])))
(is (= [2 3 4] (w/walk inc vec [1 2 3]))))
(testing "walk with outer transform"
(is (= [1 2 3] (w/walk identity vec '(1 2 3))))))
(deftest test-postwalk
(testing "postwalk with numbers"
(is (= [2 3 4] (w/postwalk #(if (number? %) (inc %) %) [1 2 3]))))
(testing "postwalk with nested structures"
(is (= [2 [3 4] 5]
(w/postwalk #(if (number? %) (inc %) %) [1 [2 3] 4]))))
(testing "postwalk preserves types"
(is (vector? (w/postwalk identity [1 2 3])))
(is (list? (w/postwalk identity '(1 2 3))))
(is (set? (w/postwalk identity #{1 2 3})))
(is (map? (w/postwalk identity {:a 1 :b 2}))))
(testing "postwalk on maps"
(is (= {:a 2 :b 3}
(w/postwalk #(if (number? %) (inc %) %) {:a 1 :b 2}))))
(testing "postwalk on empty collections"
(is (= [] (w/postwalk identity [])))
(is (= {} (w/postwalk identity {})))
(is (= #{} (w/postwalk identity #{})))
(is (= '() (w/postwalk identity '())))))
(deftest test-prewalk
(testing "prewalk with numbers"
(is (= [2 3 4] (w/prewalk #(if (number? %) (inc %) %) [1 2 3]))))
(testing "prewalk with nested structures"
(is (= [2 [3 4] 5]
(w/prewalk #(if (number? %) (inc %) %) [1 [2 3] 4]))))
(testing "prewalk transforms before descending"
;; prewalk applies f to the outer form first, so we can replace
;; entire subtrees before they are walked
(is (= [:replaced]
(w/prewalk #(if (= % [1 2 3]) [:replaced] %) [1 2 3])))))
(deftest test-keywordize-keys
(testing "basic keywordize"
(is (= {:a 1 :b 2} (w/keywordize-keys {"a" 1 "b" 2}))))
(testing "nested keywordize"
(is (= {:a {:b 2}} (w/keywordize-keys {"a" {"b" 2}}))))
(testing "non-string keys unchanged"
(is (= {:a 1 42 2} (w/keywordize-keys {"a" 1 42 2}))))
(testing "already keyword keys unchanged"
(is (= {:a 1} (w/keywordize-keys {:a 1})))))
(deftest test-stringify-keys
(testing "basic stringify"
(is (= {"a" 1 "b" 2} (w/stringify-keys {:a 1 :b 2}))))
(testing "nested stringify"
(is (= {"a" {"b" 2}} (w/stringify-keys {:a {:b 2}}))))
(testing "non-keyword keys unchanged"
(is (= {"a" 1 42 2} (w/stringify-keys {:a 1 42 2})))))
(deftest test-postwalk-replace
(testing "basic replacement"
(is (= [:x :y :c] (w/postwalk-replace {:a :x :b :y} [:a :b :c]))))
(testing "nested replacement"
(is (= [:x [:y :c]] (w/postwalk-replace {:a :x :b :y} [:a [:b :c]]))))
(testing "no matches"
(is (= [1 2 3] (w/postwalk-replace {:a :x} [1 2 3]))))
(testing "empty smap"
(is (= [1 2 3] (w/postwalk-replace {} [1 2 3])))))
(deftest test-prewalk-replace
(testing "basic replacement"
(is (= [:x :y :c] (w/prewalk-replace {:a :x :b :y} [:a :b :c]))))
(testing "nested replacement"
(is (= [:x [:y :c]] (w/prewalk-replace {:a :x :b :y} [:a [:b :c]]))))
(testing "replaces before descending"
;; prewalk-replace replaces the whole form first, then walks children
(is (= :replaced (w/prewalk-replace {[:a :b] :replaced} [:a :b])))))

View file

@ -1,122 +0,0 @@
(ns clojure.zip-test.zip
(:require [clojure.test :refer [deftest is testing run-tests]]
[clojure.zip :as zip]))
(deftest test-vector-zip-navigation
(let [data [[1 2] [3 [4 5]]]
z (zip/vector-zip data)]
(testing "root node"
(is (= (zip/node z) [[1 2] [3 [4 5]]]))
(is (zip/branch? z)))
(testing "down"
(is (= (zip/node (zip/down z)) [1 2])))
(testing "right"
(is (= (zip/node (zip/right (zip/down z))) [3 [4 5]])))
(testing "down into nested"
(is (= (zip/node (zip/down (zip/right (zip/down z)))) 3)))
(testing "up returns parent"
(is (= (zip/node (zip/up (zip/down z))) [[1 2] [3 [4 5]]])))
(testing "rights"
(is (= (zip/rights (zip/down z)) '([3 [4 5]]))))
(testing "lefts"
(is (= (zip/lefts (zip/right (zip/down z))) [[1 2]])))))
(deftest test-vector-zip-rightmost-leftmost
(let [z (zip/vector-zip [1 2 3])]
(testing "rightmost"
(is (= (zip/node (zip/rightmost (zip/down z))) 3)))
(testing "leftmost"
(is (= (zip/node (zip/leftmost (zip/rightmost (zip/down z)))) 1)))))
(deftest test-seq-zip-navigation
(let [z (zip/seq-zip '(1 (2 3) 4))]
(testing "root"
(is (= (zip/node z) '(1 (2 3) 4))))
(testing "down"
(is (= (zip/node (zip/down z)) 1)))
(testing "right"
(is (= (zip/node (zip/right (zip/down z))) '(2 3))))
(testing "down into nested list"
(is (= (zip/node (zip/down (zip/right (zip/down z)))) 2)))))
(deftest test-path
(let [z (zip/vector-zip [[1 2] [3 4]])]
(testing "path at root is nil"
(is (nil? (zip/path z))))
(testing "path one level down"
(is (= (zip/path (zip/down z)) [[[1 2] [3 4]]])))
(testing "path two levels down"
(is (= (zip/path (zip/down (zip/down z)))
[[[1 2] [3 4]] [1 2]])))))
(deftest test-edit
(let [z (zip/vector-zip [1 [2 3] [4 5]])]
(testing "edit a leaf"
(let [loc (-> z zip/down zip/right zip/down)
edited (zip/edit loc inc)]
(is (= (zip/root edited) [1 [3 3] [4 5]]))))
(testing "edit a branch"
(let [loc (-> z zip/down zip/right)
edited (zip/edit loc (fn [x] (vec (map inc x))))]
(is (= (zip/root edited) [1 [3 4] [4 5]]))))))
(deftest test-replace
(let [z (zip/vector-zip '[a b c])]
(is (= (zip/root (zip/replace (zip/down z) 'x))
'[x b c]))))
(deftest test-insert-left-right
(let [z (zip/vector-zip [1 2 3])
loc (-> z zip/down zip/right)]
(testing "insert-left"
(is (= (zip/root (zip/insert-left loc 'x)) [1 'x 2 3])))
(testing "insert-right"
(is (= (zip/root (zip/insert-right loc 'y)) [1 2 'y 3])))))
(deftest test-insert-child-append-child
(let [z (zip/vector-zip [1 2 3])]
(testing "insert-child"
(is (= (zip/root (zip/insert-child z 0)) [0 1 2 3])))
(testing "append-child"
(is (= (zip/root (zip/append-child z 4)) [1 2 3 4])))))
(deftest test-remove
(let [z (zip/vector-zip [1 2 3])
loc (-> z zip/down zip/right)]
(is (= (zip/root (zip/remove loc)) [1 3]))))
(deftest test-next-traversal
(let [z (zip/vector-zip [1 [2 3]])]
(testing "next enumerates depth-first"
(is (= (loop [loc z, acc []]
(if (zip/end? loc)
acc
(recur (zip/next loc) (conj acc (zip/node loc)))))
[[1 [2 3]] 1 [2 3] 2 3])))))
(deftest test-end?
(let [z (zip/vector-zip [1 2])]
(testing "not end at start"
(is (not (zip/end? z))))
(testing "end after full traversal"
(is (zip/end? (-> z zip/next zip/next zip/next))))))
(deftest test-prev
(let [z (zip/vector-zip [1 [2 3]])]
(testing "prev from second child"
(let [loc (-> z zip/next zip/next)]
(is (= (zip/node loc) [2 3]))
(is (= (zip/node (zip/prev loc)) 1))))
(testing "prev from leaf inside nested"
(let [loc (-> z zip/next zip/next zip/next)]
(is (= (zip/node loc) 2))
(is (= (zip/node (zip/prev loc)) [2 3]))))))
(deftest test-root-after-edits
(testing "root unwinds all the way after deep edits"
(let [z (zip/vector-zip [[1 2] [3 [4 5]]])
loc (-> z zip/down zip/right zip/down zip/right zip/down)
edited (zip/edit loc inc)]
(is (= (zip/root edited) [[1 2] [3 [5 5]]])))))
(run-tests)

View file

@ -1,42 +0,0 @@
# Conformance inc1 (jolt-xsfe) — gate wrapper for the JVM corpus certifier.
#
# test/conformance/certify.clj evaluates every test/chez/corpus.edn row through
# reference JVM Clojure and checks jolt's hand-written :expected against what real
# Clojure produces. Divergences are classified in test/conformance/known-divergences.edn
# (deliberate jolt-specific / host-model deltas + tracked bugs); the certifier exits
# nonzero only on a NEW (unclassified) divergence or a stale allowlist entry.
#
# This wrapper runs it in the Janet gate and skips cleanly when `clojure` (JVM) is
# not installed — same pattern as the chez tests skipping without `chez`.
#
# janet test/conformance/certify-test.janet
(defn- have-clojure? []
(def p (try (os/spawn ["clojure" "--version"] :p {:out :pipe :err :pipe}) ([_] nil)))
(if (nil? p) false
(do (def out (ev/read (p :out) :all)) (def err (ev/read (p :err) :all))
(zero? (os/proc-wait p)))))
(unless (have-clojure?)
(print "clojure (JVM) not on PATH — skipping corpus certification")
(os/exit 0))
(def proc (os/spawn ["clojure" "-M" "test/conformance/certify.clj"] :p {:out :pipe :err :pipe}))
(def out (string (ev/read (proc :out) :all)))
(def err (string (or (ev/read (proc :err) :all) "")))
(def code (os/proc-wait proc))
# Echo the summary lines so the gate log shows the certification status.
(each line (string/split "\n" out)
(when (or (string/find "certif" line) (string/find "allowlist" line)
(string/find "NEW" line) (string/find "STALE" line) (string/find "DIVERGENT" line))
(print line)))
(when (not= code 0)
(eprint "corpus certification FAILED (new/stale divergence vs reference Clojure):")
(eprint out)
(unless (= "" err) (eprint err))
(os/exit 1))
(print "corpus certification: OK (all divergences classified)")
(os/exit 0)

View file

@ -1,30 +0,0 @@
;; Fixture app for the cgen single-binary build test (jolt-a7ds).
;; count-point is a numeric leaf -> compiled to native C and statically linked;
;; run/-main stay bytecode and call into it. -main prints a deterministic total.
(ns cgapp)
(defn count-point [cr ci cap]
(loop [i 0 zr 0.0 zi 0.0]
(if (or (>= i cap) (> (+ (* zr zr) (* zi zi)) 4.0))
i
(recur (inc i)
(+ (- (* zr zr) (* zi zi)) cr)
(+ (* 2.0 (* zr zi)) ci)))))
(defn run [n]
(let [cap 200
nd (* 1.0 n)]
(loop [y 0 acc 0]
(if (< y n)
(let [ci (- (/ (* 2.0 y) nd) 1.0)
row (loop [x 0 a 0]
(if (< x n)
(let [cr (- (/ (* 2.0 x) nd) 1.5)]
(recur (inc x) (+ a (count-point cr ci cap))))
a))]
(recur (inc y) (+ acc row)))
acc))))
(defn -main [& args]
(let [n (if (seq args) (Integer/parseInt (first args)) 200)]
(println (run n))))

View file

@ -1,43 +0,0 @@
# AOT image round-trip: compile a namespace, marshal it to bytecode, load it into
# a FRESH context, and run the loaded functions without recompiling.
(use ../../src/jolt/api)
(use ../../src/jolt/aot)
(use ../../src/jolt/types)
(print "AOT image round-trip...")
(def img-path (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-aot-test.jimg"))
# 1. Compile a namespace into ctx1: a constant, a fn over it, a fn using core
# fns, and a recursive fn.
(def ctx1 (init {:compile? true}))
(ctx-set-current-ns ctx1 "demo")
(eval-string ctx1 "(def base 100)")
(eval-string ctx1 "(defn add-base [x] (+ x base))")
(eval-string ctx1 "(defn sum-sq [xs] (reduce + (map (fn [x] (* x x)) xs)))")
(eval-string ctx1 "(defn fact [n] (if (zero? n) 1 (* n (fact (dec n)))))")
(assert (= 107 (eval-string ctx1 "(add-base 7)")) "ctx1 add-base")
(assert (= 14 (eval-string ctx1 "(sum-sq [1 2 3])")) "ctx1 sum-sq")
(assert (= 120 (eval-string ctx1 "(fact 5)")) "ctx1 fact")
# 2. Save an AOT image of the compiled namespace.
(save-ns ctx1 "demo" img-path)
(assert (os/stat img-path) "image written")
# 3. Load it into a brand-new context — no recompilation of demo.
(def ctx2 (init {:compile? true}))
(load-ns-image ctx2 "demo" img-path)
(ctx-set-current-ns ctx2 "demo")
(assert (= 107 (eval-string ctx2 "(add-base 7)")) "ctx2 add-base from image")
(assert (= 14 (eval-string ctx2 "(sum-sq [1 2 3])")) "ctx2 sum-sq from image")
(assert (= 3628800 (eval-string ctx2 "(fact 10)")) "ctx2 fact from image (new arg)")
# 4. The loaded vars are live: redefining one is visible to callers compiled in
# ctx2 that reference it.
(eval-string ctx2 "(def base 1000)")
(assert (= 1007 (eval-string ctx2 "(add-base 7)")) "loaded var still redefinable")
(os/rm img-path)
(print "AOT round-trip passed!")

View file

@ -1,38 +0,0 @@
(use ../../src/jolt/api)
(use ../../src/jolt/types)
(print "1: init creates context...")
(let [ctx (init-cached)]
(assert (ctx? ctx) "init returns context")
(let [ns (ctx-find-ns ctx "clojure.core")]
(assert (ns? ns) "clojure.core namespace exists")
(assert (ns-find ns "nil?") "nil? is interned")
(assert (ns-find ns "+") "+ is interned")))
(print " passed")
(print "2: eval-string basics...")
(let [ctx (init-cached)]
(assert (= 42 (eval-string ctx "42")) "eval integer")
(assert (= true (eval-string ctx "true")) "eval bool")
(assert (= 3 (eval-string ctx "(+ 1 2)")) "eval list"))
(print " passed")
(print "3: eval-string with core fns...")
(let [ctx (init-cached)]
(assert (= true (eval-string ctx "(nil? nil)")) "nil?")
(assert (deep= [2 3 4] (normalize-pvecs (eval-string ctx "(map inc [1 2 3])"))) "map+inc")
(assert (= 6 (eval-string ctx "(reduce + [1 2 3])")) "reduce"))
(print " passed")
(print "4: eval-string with def...")
(let [ctx (init-cached)]
(eval-string ctx "(def x 42)")
(assert (= 42 (eval-string ctx "x")) "def then resolve"))
(print " passed")
(print "5: eval-string* with bindings...")
(let [ctx (init-cached)]
(assert (= 99 (eval-string* ctx "y" @{"y" 99})) "bound variable"))
(print " passed")
(print "\nAll API tests passed!")

View file

@ -1,67 +0,0 @@
# Whole-program inference scoped to app namespaces (jolt-87e).
#
# Auto-whole-program (a -m program run under direct-link) used to defer EVERY
# loaded namespace — including every transitive dependency — into one closed-
# world fixpoint, which is prohibitive on dep-heavy apps (hundreds of dep nses;
# a ~2-minute cold start on malli). With app source roots declared (JOLT_APP_PATHS
# / jolt-deps, here :app-paths), only the app's OWN namespaces join the whole-
# program batch; dependency namespaces skip inference (they stay direct-linked
# but generically typed — the open-world default). With NO app roots declared,
# every namespace is treated as app (whole-program over everything, pre-87e).
(use ../../src/jolt/api)
(var failures 0)
(defn- check [label got want]
(unless (= got want)
(++ failures)
(printf "FAIL [%s] got %q want %q" label got want)))
# Lay down an app root and a dep root under a tmp dir.
(def tmp (string (os/getenv "TMPDIR") "jolt-87e-" (os/time)))
(def app-root (string tmp "/app"))
(def dep-root (string tmp "/dep"))
(os/mkdir tmp) (os/mkdir app-root) (os/mkdir dep-root)
(spit (string dep-root "/mydep.clj")
"(ns mydep)\n(defn helper [x] (+ x 1))\n")
(spit (string app-root "/myapp.clj")
"(ns myapp (:require [mydep]))\n(defn run [x] (mydep/helper x))\n")
(defn- with-wp [f]
(def saved (os/getenv "JOLT_WHOLE_PROGRAM"))
(os/setenv "JOLT_WHOLE_PROGRAM" "1")
(defer (os/setenv "JOLT_WHOLE_PROGRAM" saved) (f)))
# --- app roots declared: only the app ns defers into the batch ---------------
(with-wp
(fn []
(let [ctx (init {:compile? true :direct-linking? true
:paths [app-root dep-root] :app-paths [app-root]})]
(check "whole-program is on" (truthy? (get (ctx :env) :whole-program?)) true)
(eval-string ctx "(require '[myapp])")
(let [deferred (or (get (ctx :env) :inferred-nses) @[])]
(check "app ns deferred to batch" (truthy? (index-of "myapp" deferred)) true)
(check "dep ns NOT in batch (per-ns inferred)"
(truthy? (index-of "mydep" deferred)) false))
# still runs correctly after the (scoped) whole-program pass
(when-let [ip (get (ctx :env) :infer-program!)] (protect (ip ctx)))
(check "scoped whole-program program still correct"
(eval-string ctx "(myapp/run 41)") 42))))
# --- no app roots declared: every ns defers (pre-87e whole-program) ----------
(with-wp
(fn []
(let [ctx (init {:compile? true :direct-linking? true
:paths [app-root dep-root]})]
(eval-string ctx "(require '[myapp])")
(let [deferred (or (get (ctx :env) :inferred-nses) @[])]
(check "no app roots: app ns deferred" (truthy? (index-of "myapp" deferred)) true)
(check "no app roots: dep ns ALSO deferred"
(truthy? (index-of "mydep" deferred)) true))
(when-let [ip (get (ctx :env) :infer-program!)] (protect (ip ctx)))
(check "unscoped whole-program still correct"
(eval-string ctx "(myapp/run 9)") 10))))
(if (pos? failures)
(do (printf "app-scope-wholeprogram: %d failure(s)" failures) (os/exit 1))
(print "app-scope-wholeprogram: all cases passed"))

View file

@ -1,81 +0,0 @@
# Bootstrap fixpoint (jolt-d0r).
#
# Soundness gate for self-hosting: the self-hosted compiler, rebuilt by compiling
# its OWN source through itself (stage2), must behave identically to the compiler
# built by the Janet bootstrap (stage1). We test this BEHAVIORALLY — run a corpus
# of programs through each stage and compare results — rather than by comparing
# emitted code, because emitted forms embed live setter/getter closures and the IR
# carries representation-level gensyms; behavioral parity is the property that
# actually matters and is representation-independent.
#
# stage1 = analyzer as built by the bootstrap.
# stage2 = analyzer rebuilt by compiling jolt.ir + jolt.analyzer through stage1
# (self-host, the fractal turn) and installing the result over itself.
# stage3 = the same self-rebuild applied again, on top of stage2.
# All three must produce identical results on the corpus.
(use ../../src/jolt/types)
(use ../../src/jolt/api)
(use ../../src/jolt/reader)
(import ../../src/jolt/backend :as be)
(import ../../src/jolt/stdlib_embed :as se)
(defn- forms [src]
(var s src) (def fs @[])
(while (> (length (string/trim s)) 0)
(def p (parse-next s)) (set s (p 1))
(when (p 0) (array/push fs (p 0))))
fs)
# Programs exercising the compiled constructs (fn/multi-arity/recur/loop/if/let/
# map+vector literals/closures/higher-order/protocol dispatch). Each is a single
# expression evaluated through the compile pipeline; we compare printed results.
(def corpus
["(let [f (fn [x] (* x x))] (map f [1 2 3 4]))"
"(loop [i 0 acc 0] (if (< i 10) (recur (inc i) (+ acc i)) acc))"
"((fn fact [n] (if (zero? n) 1 (* n (fact (dec n))))) 6)"
"(reduce + 0 (filter even? (range 20)))"
"(let [[a b & r] [1 2 3 4 5]] [a b r])"
"(mapv (juxt identity inc dec) [10 20])"
"(frequencies (concat [:a :a] [:b]))"
"(group-by odd? (range 8))"
"(get-in {:a {:b {:c 42}}} [:a :b :c])"
"(first {:x 1})"
"(into {} (map (fn [k] [k (* k k)]) (range 5)))"
"((comp inc inc) 10)"
"(apply max [3 1 4 1 5 9 2 6])"
"(str (reverse \"hello\") (count [1 2 3]))"
"(let [m {:a 1}] (assoc m :b (+ (:a m) 1)))"])
(defn- run-corpus [ctx]
(map (fn [p] (def r (protect (eval-string ctx p)))
(if (r 0) (string/format "%j" (normalize-pvecs (r 1))) (string "ERR:" (r 1))))
corpus))
# Rebuild the analyzer through the self-hosted pipeline, in place.
(defn- self-rebuild! [ctx]
(def saved (ctx-current-ns ctx))
(each nsn ["jolt.ir" "jolt.analyzer"]
(ctx-set-current-ns ctx nsn)
(each f (forms (get se/sources nsn)) (protect (be/compile-and-eval ctx f))))
(ctx-set-current-ns ctx saved))
(def ctx (init {:compile? true}))
(def r1 (run-corpus ctx)) # stage1 (bootstrap-built)
(self-rebuild! ctx)
(def r2 (run-corpus ctx)) # stage2 (self-built)
(self-rebuild! ctx)
(def r3 (run-corpus ctx)) # stage3 (self-built from stage2)
(var failures 0)
(for i 0 (length corpus)
(unless (and (= (r1 i) (r2 i)) (= (r2 i) (r3 i)))
(++ failures)
(printf "FAIL [%s]\n stage1=%s\n stage2=%s\n stage3=%s" (corpus i) (r1 i) (r2 i) (r3 i)))
# also guard against everything silently erroring
(when (string/has-prefix? "ERR:" (r1 i))
(++ failures) (printf "FAIL [%s] stage1 errored: %s" (corpus i) (r1 i))))
(if (pos? failures)
(do (printf "bootstrap-fixpoint: %d failure(s)" failures) (os/exit 1))
(printf "bootstrap-fixpoint: stage1 == stage2 == stage3 on %d programs\n" (length corpus)))

View file

@ -1,60 +0,0 @@
# A record type keeps its :type tag through depth-capping (jolt-3ko follow-up).
# cap (jolt.passes.types) truncates a deep type's field VALUES to :any so the
# inter-procedural fixpoint stays finite, but the record :type tag is identity,
# independent of field depth — so it must survive. Before this fix cap rebuilt
# the struct via mk-struct and dropped :type, degrading a record stored in a
# deep container to a plain struct: devirtualization (jolt-41m) and record?
# folding silently stopped firing on it. This drives whole-program inference
# over a vector-of-records and asserts (a) the element keeps REC identity and
# (b) a protocol call on an element devirtualizes (the inference annotates the
# call node with :devirt-type, which it can't do without the receiver's :type).
(import ../../src/jolt/api :as api)
(import ../../src/jolt/backend :as backend)
(import ../../src/jolt/types :as types)
(print "Record :type survives capping (jolt-3ko)...")
(def dir (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-cap-rec"))
(os/mkdir dir)
(spit (string dir "/cr.clj")
(string
"(ns cr)\n"
"(defrecord V3 [r g b])\n" # nested record -> forces a deep type
"(defprotocol Shape (area [s]))\n"
"(defrecord Box [^V3 lo ^V3 hi]\n"
" Shape\n"
" (area [s] (+ (:r (:hi s)) (:r (:lo s)))))\n"
"(defn total [coll] (reduce (fn [acc x] (+ acc (area x))) 0.0 coll))\n"
"(defn drive [n]\n"
" (let [world [(->Box (->V3 1.0 2.0 3.0) (->V3 4.0 5.0 6.0))\n"
" (->Box (->V3 0.0 0.0 0.0) (->V3 2.0 2.0 2.0))]]\n"
" (loop [i 0 acc 0.0] (if (< i n) (recur (inc i) (+ acc (total world))) acc))))\n"))
(os/setenv "JOLT_DIRECT_LINK" "1")
(os/setenv "JOLT_WHOLE_PROGRAM" "1")
(os/setenv "JOLT_PATH" dir)
(def ctx (api/init {:compile? true}))
(api/eval-string ctx "(require '[cr])")
(def report (backend/infer-program! ctx))
# (a) total's collection param keeps its record element identity through cap
(def coll-t (get (get report "cr/total") 0))
(def elem-t (and coll-t (get coll-t :vec)))
(assert (and elem-t (= "cr.Box" (get elem-t :type)))
(string "vec element keeps record :type through cap (got " (string/format "%p" elem-t) ")"))
# (b) the protocol call on an element devirtualizes — the inference can only
# annotate :devirt-type when it knows the receiver's record :type.
(def pns (types/ctx-find-ns ctx "jolt.passes"))
(def reinfer (types/var-get (types/ns-find pns "reinfer-def")))
(def cell (get ((types/ctx-find-ns ctx "cr") :mappings) "total"))
(def reinferred (string/format "%p" (reinfer (get cell :infer-ir) @{"coll" coll-t})))
(assert (not (nil? (string/find ":devirt-type" reinferred)))
"protocol call on a collection-read record devirtualizes (:devirt-type present)")
# correctness: the program still computes the right answer
(assert (= 5.0
(api/eval-string ctx "(cr/total [(cr/->Box (cr/->V3 1.0 0 0) (cr/->V3 4.0 0 0)) (cr/->Box (cr/->V3 0 0 0) (cr/->V3 0 0 0))])"))
"devirtualized record-in-collection computes correctly")
(print "Record :type survives capping (jolt-3ko) passed!")

Some files were not shown because too many files have changed in this diff Show more