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:
parent
5c1fdfc336
commit
58d03d67be
221 changed files with 16 additions and 29925 deletions
6
.gitmodules
vendored
6
.gitmodules
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
8
Makefile
8
Makefile
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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)))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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 "$@"
|
|
||||||
|
|
@ -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` |
|
|
||||||
|
|
@ -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)])))
|
|
||||||
|
|
@ -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"))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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")
|
|
||||||
100
run-tests.janet
100
run-tests.janet
|
|
@ -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)))
|
|
||||||
|
|
@ -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"))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
(declare-project
|
|
||||||
:name "mandel-native-spike")
|
|
||||||
|
|
||||||
(declare-native
|
|
||||||
:name "mandel"
|
|
||||||
:source ["mandel.c"])
|
|
||||||
|
|
@ -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)))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
||||||
|
|
@ -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
|
|
@ -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))
|
|
||||||
|
|
@ -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})
|
|
||||||
|
|
@ -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")))})
|
|
||||||
|
|
@ -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
|
|
@ -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).
|
|
||||||
|
|
||||||
|
|
@ -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)))
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
|
|
@ -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)))))
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
|
|
@ -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 [...]}"))))
|
|
||||||
|
|
@ -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)))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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})
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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)))
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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!)
|
|
||||||
|
|
@ -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)))))
|
|
||||||
|
|
@ -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)))}))
|
|
||||||
|
|
@ -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)))}))
|
|
||||||
|
|
@ -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])))
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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))))
|
|
||||||
|
|
@ -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))))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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))))
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)))))))
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
||||||
|
|
@ -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)))
|
|
||||||
|
|
||||||
|
|
@ -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))))
|
|
||||||
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))))"}
|
||||||
|
|
|
||||||
|
|
@ -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)"))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
|
||||||
|
|
@ -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)))))
|
|
||||||
|
|
@ -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")))))
|
|
||||||
|
|
@ -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])))))
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
30
test/fixtures/cgen-build/cgapp.clj
vendored
30
test/fixtures/cgen-build/cgapp.clj
vendored
|
|
@ -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))))
|
|
||||||
|
|
@ -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!")
|
|
||||||
|
|
@ -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!")
|
|
||||||
|
|
@ -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"))
|
|
||||||
|
|
@ -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)))
|
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue