Commit graph

868 commits

Author SHA1 Message Date
Dmitri Sotnikov
af11aaa7ff
Devirtualize monomorphic protocol calls (#227)
When the inference proves a protocol call's receiver is one record type, the
back end resolves the impl by that static tag (find-protocol-method) instead of
routing through the protocol var -> jolt-invoke -> protocol-resolve, which
re-derives the tag and walks the type table. Same table lookup, minus the
var-deref, the rest-cons, and the receiver-type computation.

Fires only on a monomorphic site: a megamorphic receiver joins to :any and
carries no :devirt-type, so it keeps ordinary dispatch (the dispatch bench is
unaffected). The annotation comes from the whole-program fixpoint typing a
reduce/HOF element or a ctor return as a specific record.

Modest on the dispatch benchmarks (~6% on mono-dispatch) — float boxing in the
reduce accumulator dominates there, a separate numeric lever — but it removes
the dispatch overhead wherever a typed receiver is known.

run-devirt.ss gate: emitted form uses find-protocol-method, and evaluating it
matches ordinary dispatch for an inline impl, an extend-type impl, and the
non-devirt path. make test / shakesmoke green, 0 new divergences.

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-26 11:16:19 +00:00
Dmitri Sotnikov
09712ec575
Whole-program param-type inference (closed world) (#226)
Re-derive each app fn's param types from its call sites under --opt, so a
record type flows across fn boundaries: a ctor's return reaches a callee
param, and a typed vector's element reaches a HOF closure's param. The back
end can then bare-index field reads and devirtualize protocol calls at those
sites (it reads the resulting :hint/:devirt annotations; consuming them is
separate work).

This rebuilds the inter-procedural driver the Janet host had — the API
(infer-body/reinfer-def) survived the rehost but nothing drove it, and the
record-shapes/protocol-methods registries were empty stubs.

- records.ss: populate record-shapes (ctor key -> fields/tags/type, resolving
  nested record field tags) and protocol-methods (method var -> [proto method])
  registries at deftype/defprotocol load time; jolt.host accessors materialize
  them.
- passes/types.clj: wp-infer! runs a closed-world fixpoint joining call-site
  arg types into callee params; reinfer-def re-seeds each def at emit. Self-
  recursive calls and fn-level recur are collected so a recursive fn's params
  are constrained by its recursion, not just external callers — else a param
  the recursion widens (e.g. binary-trees check-tree, whose untagged child can
  be nil) would be unsoundly typed non-nil. A fn used in value position keeps
  :any params (callers unknown). Megamorphic sites join to :any.
- build.ss: analyze all app forms and run the fixpoint before per-form emit.
- run-wp.ss: gate (cross-fn propagation, escape soundness, self-recursion).

make test / shakesmoke green, 0 new divergences, selfhost holds.

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-26 10:57:45 +00:00
Dmitri Sotnikov
32ef74b9b0
Persistent vector as a 32-way trie (#225)
The persistent vector was a flat Scheme vector with copy-on-write: every conj
copied the whole backing array, so building an n-element vector was O(n^2). On the
collections bench that's vec-sum building 7500 elements with 227MB of copies, 90%
of the bench's allocation.

Replace it with Clojure's PersistentVector — a 32-way trie plus a trailing tail
chunk. conj appends to the tail and, when it fills, path-copies it into the trie,
so conj is O(1) amortized and a linear build is O(n). nth/assoc/pop are
O(log32 n). make-pvec (build a trie from a flat vector) and pvec-v (materialize
back) stay as compatibility shims, so the ~14 callers that read the backing array
— all one-shot conversions in =, hash, seq, meta-copy, transients, the reader —
are untouched; only this file's internals change.

vec-sum 70ms/227MB -> 1ms/3MB; collections 10.4x -> 4.0x vs JVM, under the 5x
target. 5 new corpus rows plus boundary stress (level transitions at 32 and 1024,
pop-collapse, assoc at every index) cover the trie.

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-26 06:32:18 +00:00
Dmitri Sotnikov
93be40f3fe
See through or/and in truthy-elision (#224)
A loop test like (or (>= i cap) (> ... 4.0)) desugars to
(let* [g (>= i cap)] (if (truthy? g) g (> ... 4.0))) and the whole thing was
wrapped in jolt-truthy? because returns-scheme-bool? only looked at :const and
:invoke nodes, not the let*/if an or/and expands to. The wrapper defeats Chez's
branch inlining on the hot loop edge.

Make returns-scheme-bool? recursive over :if (both branches bool), :let (body
bool, tracking which bound locals hold a Scheme boolean), and :local (in that
set). or/and over bool-returning ops then read as Scheme booleans and the outer
wrapper drops. Still sound: eliding only when the value is provably #t/#f — a
jolt-nil is a truthy record in Chez, so a false positive would be a real bug, and
the recursion only proves bool-ness through ops already known to return one.

No bench regression; the win lands on hinted float loops where the branch, not
boxed arithmetic, is the cost.

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-26 06:11:35 +00:00
Dmitri Sotnikov
0a7e818700
Fixed-arity protocol dispatch shims (#223)
defprotocol emitted one variadic (fn [this & rest] (protocol-dispatch P m this
(list->cseq rest))) per method, so every protocol call — even a no-extra-arg one
like (area s) — consed a rest list, wrapped it in a cseq, var-deref'd
protocol-dispatch, and jolt-invoke'd it (consing again). On mono-dispatch that was
2.07GB of allocation, ~65% of the benchmark.

Emit one fixed-arity clause per declared arglist instead. The 1/2/3-param arities
call positional protocol-dispatch{1,2,3}, which resolve the impl (by record tag,
reify method, or host-tag extension — factored into protocol-resolve) and apply it
directly; no rest-list, no seq round-trip. The dispatchN entry points are in the
native-op table so the shim calls bind straight to the records.ss procedures
rather than var-deref. 4+ params fall back to the variadic protocol-dispatch.

mono-dispatch 1.5s/2.07GB -> 0.69s/280MB; dispatch 26x -> 12.2x, mono-dispatch
111x -> 51x vs JVM. 5 new corpus rows pin multi-arity methods, host-type args,
and protocol-method-as-value against JVM Clojure.

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-26 05:57:42 +00:00
Dmitri Sotnikov
8bea1abe12
Native record representation + inline nil?/some? (#222)
Records were a jrec holding an alist of (kw . val) conses: ~113B/node, built
fresh per construction, field reads a list scan. Replace that with a shared
per-type descriptor (tag + field keywords + an eq?-keyed keyword->index table)
plus a flat per-instance value vector and an extension map for any non-field
keys assoc'd on (jolt-nil when there are none). Construction now allocates one
vector instead of a cons chain and a field read is an index lookup. binary-trees
construction allocation drops 2.085GB -> 1.19GB.

That alone barely moved binary-trees wall-time: profiling showed the read loop,
not allocation, dominates, and the read loop's own allocation came from (nil? l)
lowering to (jolt-invoke (var-deref "clojure.core" "nil?") l), which conses its
args every call. Add nil?/some? to the backend native-op table so they inline to
jolt-nil?/jolt-some? (and drop the truthy wrapper, like the other predicates).
check-tree's read loop goes from 1.476GB allocated to zero; binary-trees 18.9x
-> 9.7x vs JVM. The remaining gap is the field-read dispatch chain (jolt-c3mw).

Two JVM divergences fixed along the way, both certified:
- dissoc of a declared field downgrades a record to a plain map (was kept as a
  record); an extension key still drops cleanly.
- map->R keeps extension keys (was dropping anything outside the declared basis).

16 new corpus rows pin assoc/dissoc/count/keys/seq/=/hash/extension-field
behavior against JVM Clojure.

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-26 05:42:24 +00:00
Dmitri Sotnikov
eacfa04e5b
Perf round 1: self-call, keyword interning, fast record field reads (#221)
* Make the benchmark harness build optimized binaries on Chez

bench/run.sh was Janet-era: it invoked a 'jolt' binary and set
JOLT_DIRECT_LINK/JOLT_WHOLE_PROGRAM, none of which exist on Chez, where
'joltc run -m' runs fully unoptimized (direct-link and inline default off). So
the suite was measuring jolt's unoptimized path.

run.sh now compiles each benchmark to an optimized AOT binary (joltc build
--direct-link --opt) and times it against JVM Clojure on the same portable
source, auto-detecting the Chez kernel dev files like build-smoke.sh. Adds
bench/deps.edn so joltc resolves the namespaces, NO_JVM to skip the reference.

mandelbrot.clj dropped its jolt.png require so the JVM reference can run it; the
picture demo moved to mandelbrot_png.clj (jolt-only). README scorecard refreshed
with current Chez numbers and the two-regime read (compute ~8-10x substrate floor;
dispatch/alloc ~120-330x architectural gaps the passes don't touch). Stale
'jolt -m' header lines point at bench/run.sh.

* Emit direct self-calls for named-fn self-recursion

A self-recursive call to a named fn compiled to (jolt-invoke fib ...) instead of
a direct (fib ...): emit-invoke handled a :local callee only when it was NOT a
known proc, so a :local that IS in *known-procs* (the letrec-bound self-name) fell
through to the :else jolt-invoke branch. Now a :local known proc emits a direct
Scheme call — no jolt-invoke, no per-call arg-list consing; case-lambda handles
arity.

fib 30: 63.3ms -> 4.7ms (faster than JVM Clojure's 7.1ms; was 9x slower). The win
is on every self-recursive non-loop fn, including the compiler's own. No semantic
change — selfhost holds, make test green, shakesmoke/buildsmoke byte-identical.

Re-mint (backend is seed). Corpus rows pin self-recursion across fixed/multi/
variadic arities.

* Intern no-ns keywords without per-call allocation

(keyword #f name) built a fresh combined-key string (string-append) on every
call just to do the intern-table lookup — ~80 bytes of garbage per (:kw x), map
literal, keyword arg, etc. A no-ns keyword now interns in a table keyed by the
name string directly, so a lookup of an already-interned keyword is one
hashtable-ref with no allocation. The ns table keeps the combined key; both share
the keyword-t khash (equal-hash of the combined key) so hash values are unchanged.

Small time win on its own (the field-read dispatch dominates hot record code —
see jolt-unx4) but removes per-call keyword allocation everywhere. Runtime .ss,
no re-mint; identity/=/hash unchanged, make test green.

* Fast record field reads: single eq? scan, skip the get-arm walk

(:field rec) / (get rec :field) lowers to (jolt-get rec kw), which walked the
get-arm list to reach the jrec arm, then did jrec-has? + jrec-lookup — TWO linear
scans, each comparing keys through the generic jolt=2 equality dispatcher. Field
keys are interned keywords, so:

- jrec-key=? compares a keyword query by eq? (jolt=2 only for non-keyword keys),
- jrec-ref does ONE scan (vs has?+lookup) and runs a deftype's ILookup valAt only
  when the field is genuinely absent (present-nil still returns nil, not default),
- jolt-get-dispatch checks jrec? first, skipping the get-arm walk for the hottest
  get target. jrec-lookup/jrec-has? (used by =, contains?, etc.) get the fast
  compare too.

binary-trees 135x->18.9x, dispatch 121x->26.4x, mono-dispatch 327x->108x vs JVM.
Runtime .ss (collections.ss + records.ss), no re-mint; make test + shakesmoke +
buildsmoke green, record get/assoc/keys/=/count semantics unchanged.

---------

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-26 05:00:28 +00:00
Dmitri Sotnikov
93ddf2c85a
Make the benchmark harness build optimized binaries on Chez (#220)
bench/run.sh was Janet-era: it invoked a 'jolt' binary and set
JOLT_DIRECT_LINK/JOLT_WHOLE_PROGRAM, none of which exist on Chez, where
'joltc run -m' runs fully unoptimized (direct-link and inline default off). So
the suite was measuring jolt's unoptimized path.

run.sh now compiles each benchmark to an optimized AOT binary (joltc build
--direct-link --opt) and times it against JVM Clojure on the same portable
source, auto-detecting the Chez kernel dev files like build-smoke.sh. Adds
bench/deps.edn so joltc resolves the namespaces, NO_JVM to skip the reference.

mandelbrot.clj dropped its jolt.png require so the JVM reference can run it; the
picture demo moved to mandelbrot_png.clj (jolt-only). README scorecard refreshed
with current Chez numbers and the two-regime read (compute ~8-10x substrate floor;
dispatch/alloc ~120-330x architectural gaps the passes don't touch). Stale
'jolt -m' header lines point at bench/run.sh.

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-26 04:59:52 +00:00
Dmitri Sotnikov
f3084f8043
Collection fns: JVM-faithful return types + laziness (#219)
A type-aware audit (~190 collection expressions vs reference Clojure) found four
divergences the corpus missed — value-equality (= [0 1] '(0 1)) hides type and
laziness differences. Fixed, with type-predicate + over-infinite corpus rows that
pin them.

- partition-all [n coll] built vector chunks; JVM chunks are seqs. (The [n step
  coll] arity was already correct, as is the partition-all transducer, whose
  chunks are vectors in JVM too.) Now builds seq chunks.
- replace always returned a vector (mapv) and was eager; JVM is type-preserving —
  a vector maps to a vector, any other seqable to a lazy seq.
- sequence eagerly realized its source (into-xform), so (first (sequence (map inc)
  (range))) hung. Rewrote as a transformer iterator: pull one input at a time,
  buffer the step outputs, emit lazily, run the completion to flush a stateful
  xform. eduction builds on it (lazy, no longer an eager vector).
- mapcat and (apply concat coll-of-colls) hung over an infinite source because
  jolt-apply seq->lists the trailing arg and mapcat seq->lists the map result.
  Added lazy-concat-seq (lazily flatten a seq of colls); mapcat uses it directly,
  and apply special-cases concat (its result is lazy) to route through it.

Docs: a cross-cutting return-type + laziness contract in docs/spec/09-core-library;
SPEC.md notes that = masks type/laziness so they need predicate / over-infinite
rows. EBNF is reader syntax only — unaffected.

Seed change (partition-all/replace/eduction are clojure.core overlay) -> re-mint;
selfhost holds. make test + shakesmoke + buildsmoke green, 0 new divergences.

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-26 03:01:36 +00:00
Dmitri Sotnikov
8180c85393
Source locations: reader positions, error locations, native stack traces (#218)
* Reader records source line/column on list forms

The reader stamps 1-based :line/:column metadata on every list form (plus
:file when load-jolt-file is reading a file), and jolt.host/form-position
reads it back so the analyzer's :pos scaffold finally gets real data. A
left-to-right cursor counts newlines over the delta between successive forms,
so it stays O(n). Vector/map/set literals are untouched (their metadata is a
runtime value the analyzer would have to wrap in with-meta); empty () can't
carry meta. ^meta now merges onto the position keys instead of clobbering them.

Re-mint is byte-identical (the backend doesn't emit :pos), so this is a pure
scaffold for the error-location work that follows.

* Report source location on uncaught errors

Each top-level form records its source position (thread-local) before it
compiles+evals, and cli.ss jolt-report-uncaught appends 'at file:line:col'
when an error propagates out. Covers joltc -e, joltc run <file>, and
load-string — every interpreted path. Top-level granularity, one set per
form; deeper frames come from the Phase 2 frame walk.

Runtime .ss only, no re-mint.

* Clojure stack traces via source registry + native frame walk

A direct-link build emits (jolt-register-source! short-name ns name file line)
once per fn def — at definition time, so zero per-call cost. On an uncaught
error the reporter walks Chez's native continuation frames (jolt-throw captures
the live continuation via call/cc; host conditions carry their own
&continuation), maps each frame's procedure name through the registry, and
prints a Clojure backtrace 'ns/name (file:line)'. Wired into both the cli and a
built binary's launcher.

Frames are keyed by the short munged fn name Chez actually reports (emit-fn's
letrec self-binding), not jv$ns$name; a cross-namespace collision degrades to
the bare frame name rather than a wrong attribution. The analyzer carries the
original form's position through defn macroexpansion onto the def node.

Calling a non-fn now throws a catchable ClassCastException (via jolt-throw)
naming the operator, instead of a raw Chez error.

Caveats (documented in source-registry.ss): names map only in direct-link/AOT
closed-world builds — the open-world -e/repl/run path falls back to the
top-level location; and pervasive TCO erases tail-call frames, so a mapped
trace shows only the non-tail spine. JOLT_DEBUG_FRAMES dumps raw frame names.

Re-mint (analyzer + backend); prelude byte-identical (direct-link off during
mint). Corpus rows certified, build-smoke asserts the trace.

* Propagate source position through macroexpansion

hc-expand-1 now carries the macro call form's :line/:column onto the top of a
list expansion that has none of its own (merged under any meta the macro set),
so errors and stack traces in macro-generated code point at the call site —
Clojure parity. The analyze recursion re-expands inner macros, so each level's
top form picks it up, matching the reference compiler. (meta (macroexpand-1
'(when x y))) now reports the call-site line.

A direct-link fn defined through a user macro (build-app's defguarded) registers
with a real line, so build-smoke's trace assertion covers macro-defined fns.

Runtime .ss (host-contract.ss) — no re-mint; selfhost holds.

Phase 3's optional items are deferred: :line-in-ex-data has no clean consumer
(it would pollute ex-data, break = and printing, and positions already surface
via the trace + top-level location), and Chez source-object emission is a large
backend change the jv$-name registry already sidesteps.

* Review fixes: registration key, thread-locals, debug flag timing

- Register a fn under the name Chez actually reports for its frame, not the def
  name: a named fn literal whose name differs from the def (def foo (fn bar …))
  is framed as 'bar', and an anonymous fn def (def foo (fn …)) as jv$ns$foo.
  Both previously registered under the def name and so never appeared in traces.
- rdr-source-file / rdr-pos-cursor are thread parameters, so concurrent compiles
  (futures, core.async) don't clobber each other's file/line attribution.
- Read JOLT_DEBUG_FRAMES at call time: a built binary evaluates top-level forms
  at heap-build time, where a load-time getenv is always unset.

Re-mint (backend + reader); prelude byte-identical, selfhost holds.

---------

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-26 02:14:34 +00:00
Dmitri Sotnikov
bdf436e242
Merge pull request #217 from jolt-lang/feat/bigdec-arithmetic
BigDecimal arithmetic (value-position + type-directed call-position)
2026-06-26 00:26:18 +00:00
Dmitri Sotnikov
838ff37207
Merge pull request #216 from jolt-lang/refactor/host-chez-java-folder
Group the JVM interop shims under host/chez/java/
2026-06-26 00:23:30 +00:00
Yogthos
09a9ad8c75 Add bigdec min/max (review follow-up)
Review found (< 1M 2M) worked but (min 1M 2M) threw — incoherent. Wire min/max
the same way as the other ops: value-position jolt-min/jolt-max shims (new in
seq.ss, added to core-value-procs) and call-position via bd-spec/bd-ops ->
jbd-min/jbd-max.

min/max return the original operand by value, not a coerced copy, matching
Clojure: (min 1M 2.0) -> 1M, (max 1M 2.0) -> 2.0, (min 1.50M 2M) -> 1.50M; a tie
keeps the second operand ((max 1.5M 1.50M) -> 1.50M). bigdec mixed with a flonum
in call position stays in the documented :any/contagion gap (value position
handles it). Re-mint; 6 more JVM-certified rows.
2026-06-25 20:22:26 -04:00
Yogthos
6fcc9fa8e6 BigDecimal call-position arithmetic via :bigdec type (Phase 2)
A direct (+ 1.5M 2.5M) emits a raw Chez + that rejects the bigdec record. Rather
than guard every arithmetic call site (measured 2-4x on unhinted fixnum loops),
let the analyzer dispatch where it can prove the type.

jolt.passes.numeric seeds a :bigdec kind from the M-literal and flows it through
let/loop/if like the existing :double/:long kinds; an arithmetic/comparison invoke
whose operands are all bigdec (integer literals allowed) gets :num-kind :bigdec.
The back end (bd-ops + emit-numeric) lowers those to the bigdec.ss engine
(jbd-add/-sub/-mul/-div, jbd-lt?/…, jbd-zero?/-pos?/-neg?, jbd-quot/-rem).

Zero cost on non-bigdec code: with no bigdec literals present the kind never
arises, so emission is byte-identical — the re-mint leaves prelude.ss unchanged,
only image.ss (the compiler) moves. Gaps (filed): a bigdec mixed with a flonum in
call position, and a bigdec the analyzer types :any, still hit the raw op and
throw; use value position or a literal-typed let.

Re-mint (numeric/backend are seed sources). 16 JVM-certified corpus rows.
2026-06-25 19:49:17 -04:00
Yogthos
bd7b75fb5d BigDecimal arithmetic: value-position + compare (Phase 1)
bigdec values existed but +,-,*,/ and compare threw — the header even said
"arithmetic contagion is not modelled". Add the scale-aware engine on the
{unscaled, scale} pair (jbd-add/-sub/-mul/-div + comparison helpers) following
java.math.BigDecimal's rules: add/sub align to the larger scale, multiply adds
scales, divide gives the exact quotient at minimal scale or throws
ArithmeticException on a non-terminating expansion. Clojure contagion: a bigdec
mixed with an integer stays bigdec, a flonum operand wins (result is a double).

Wire it into the value-position shims only — jolt-add/-sub/-mul/-div (what
(reduce + bigs)/(apply * bigs) lower to) and compare — so the inlined native hot
path is untouched. A call-position (+ 1.5M 2.5M) still reaches the raw Chez op;
that needs the analyzer's :bigdec type (next).

Runtime .ss only, no re-mint. 13 JVM-certified corpus rows.
2026-06-25 19:42:12 -04:00
Yogthos
ec9fde9e7e Group the JVM interop shims under host/chez/java/
The host/chez directory mixed jolt's own runtime (value model, seq, reader,
vars, ns, multimethods) with the shims that emulate the JVM: java.* / javax.*
classes, clojure.lang interfaces, and the host-class registry they hang off.
Move that JVM-emulation layer into host/chez/java/ so it reads as a distinct
unit instead of being interleaved with the platform runtime.

Moved (content unchanged): host-static, host-static-methods,
host-static-classes, host-class, dot-forms, records-interop, byte-buffer,
io, io-streams, inst-time, java-time, bigdec, natives-queue, natives-str,
natives-array, math, concurrency, async, ffi.

The load paths in rt.ss/cli.ss and the build.ss runtime manifest are updated
to point at java/; the build inliner follows the (load ...) strings, so the
AOT path needs no other change. All runtime shims, no seed source touched
(the three .clj edits are doc comments), so no re-mint.

Gate green: make test (selfhost fixpoint, certify 0-new, sci 211, infer),
shakesmoke (4 apps byte-identical).
2026-06-25 18:35:44 -04:00
Dmitri Sotnikov
5b77efa499
Merge pull request #215 from jolt-lang/cleanup/dedup-fresh-sym
Drop duplicate fresh-sym; clarify group-by-head vs parse-extend-impls
2026-06-25 21:38:23 +00:00
Yogthos
d77fd22bfe Drop the duplicate fresh-sym; clarify group-by-head vs parse-extend-impls
A post-conformance review (chiasmus) flagged fresh-sym defined byte-identically
in 00-syntax and 30-macros; 00-syntax loads first, so the second is redundant.
Also note why deftype uses group-by-head while extend-protocol uses
parse-extend-impls (the latter must treat a computed class type in head position).
No behavior change.
2026-06-25 17:35:18 -04:00
Dmitri Sotnikov
dcfe205a61
Merge pull request #214 from jolt-lang/fix/defn-docstring-assert-seqable
defn docstrings, assert throws AssertionError, Seqable covers collections
2026-06-25 21:26:41 +00:00
Yogthos
14ce46fb2a defn docstrings, assert throws AssertionError, Seqable covers collections
Conformance gaps surfaced re-running the library suites:

- defn now keeps a leading docstring as :doc metadata — it was dropped, so
  (:doc (meta #'f)) was always nil. Rides the def docstring slot.
- assert (and :pre/:post) throw a real AssertionError instead of an ex-info, so
  (catch AssertionError …) / (thrown? AssertionError …) match, with Clojure's
  "Assert failed: <msg>\n<form>" message.
- instance? clojure.lang.Seqable was conflated with ISeq, so a vector/map read
  as not-Seqable. Split them: Seqable covers every persistent collection, ISeq
  only seqs.
2026-06-25 17:23:24 -04:00
Dmitri Sotnikov
60e129a95c
Merge pull request #213 from jolt-lang/conformance/aws-api-host-shims
Host shims and protocol fixes shaken out by aws-api
2026-06-25 21:03:37 +00:00
Yogthos
829c251bca Host shims and protocol fixes shaken out by aws-api
Running cognitect aws-api's pure test namespaces (signing/shapes/protocols/
util/retry/endpoints) surfaced general gaps:

- extend-protocol/extend-type accept a computed class type, e.g.
  (Class/forName "[B") for the byte-array class — the byte-array idiom data.json
  and aws-api use. The macro grouping handled only symbol/nil heads (it crashed on
  a list type); type->name resolves a Class value via .getName; a byte-array
  dispatches on the "[B" host tag.
- java.nio.ByteBuffer over a jolt byte-array (wrap/allocate/get/put/array/
  remaining/position/limit/duplicate/flip), plus extend-protocol to it.
- java.util.Arrays (equals/copyOf/copyOfRange/fill) and java.util.Random
  (nextBytes/nextInt/…).
- java.net.URI/create and clojure.lang.RT/baseLoader statics.
- clojure.core.async/promise-chan (deliver-once, peek-don't-pop).
- a failed java.time parse throws DateTimeParseException (typed), so
  (catch DateTimeParseException …) matches it instead of leaking an untyped
  condition.

The XML side lives in the jolt-lang/xml library (libxml2 over jolt.ffi); ByteBuffer
stays in core as a generic java.nio primitive.

Gate: make test green (corpus +6 JVM-certified rows, 0 NEW divergence; unit
553/553; SCI 211).
2026-06-25 16:56:48 -04:00
Dmitri Sotnikov
05f29d1bcb
Merge pull request #212 from jolt-lang/conformance/core-memoize
Run core.memoize's test suite on jolt
2026-06-25 17:26:22 +00:00
Yogthos
d21ab77e7e Run core.memoize's test suite on jolt
Shaking out clojure.core.memoize (207 assertions, 0 fail) cleared several
general gaps:

- deref/@ on a deftype or reify implementing clojure.lang.IDeref dispatches to
  its deref method (RetryingDelay / make-derefable).
- deftype mutable fields (^:unsynchronized-mutable / ^:volatile-mutable) are
  read live: a set! within a method is observed by a later read in the same
  invocation, not the entry-time capture. Needed for double-checked locking.
  Immutable fields stay let-bound. Field reads rewrite to (.-field inst) with
  lexical-shadow tracking.
- def metadata values are evaluated, like Clojure: ^{:k (f)} stores (f)'s
  result and ^{:af some-fn} the fn. :tag stays a literal hint.
- try dispatches catch clauses by class in order via the exception supertype
  hierarchy; a non-matching value re-throws, an untyped host condition is caught
  by a RuntimeException/Exception/Throwable clause. Previously the last clause
  won and the class was ignored.
- locking takes a real per-object monitor (recursive Chez mutex) now that
  futures/agents/threads share one heap; it was a no-op.
- supers/ancestors reflect a small modeled JVM interface hierarchy, so
  (ancestors (class f)) yields Runnable/Callable (core.memoize's arg check).
- AssertionError / Error constructors.

JOLT_FEATURES is gone from the docs: it isn't read anywhere on Chez, and the
reader already includes :clj in its default feature set. RFC 0002's
{:jolt :default} design was reverted in the reader; docs now match the code.

Raises the SCI floor 205 -> 210.
2026-06-25 13:23:05 -04:00
Dmitri Sotnikov
3dde290f1a
Merge pull request #211 from jolt-lang/docs/host-interop-concurrency-refs
docs: host-interop for the new concurrency + reference shims
2026-06-25 15:46:26 +00:00
Yogthos
d06c7a0acc docs: host-interop — Thread/CountDownLatch, Soft/WeakReference + ReferenceQueue, ConcurrentHashMap, System/gc, Class/forName 2026-06-25 11:42:44 -04:00
Dmitri Sotnikov
ec792d28a0
Merge pull request #210 from jolt-lang/feat/gc-weak-references
Soft/WeakReference: real GC eviction via Chez weak pairs + guardians
2026-06-25 15:36:00 +00:00
Yogthos
80b2dfa9f9 Soft/WeakReference: real GC eviction via Chez weak pairs + guardians
Replace the strong-ref stub with genuine reclamation. The referent is held
through a weak-cons, so Chez's generational collector reclaims it once it is
otherwise unreachable (the pair's car becomes the bwp object, and .get returns
nil). A guardian registered on the referent makes the reference itself available
the instant its referent is collected, which ReferenceQueue.poll surfaces as
enqueued — the same hook clojure.core.cache's clear-soft-cache! drains.

Chez has no softer-than-weak reference, so a SoftReference clears on
unreachability rather than under memory pressure: a SoftCache evicts more eagerly
than the JVM's but is now real GC eviction, not an unbounded strong cache.
WeakReference gets the same (faithful) semantics. Added System/gc -> a full
collect so callers (and the queue) can force the cycle.

core.cache stays 1314/0/0 (its test values are immortal literals). Corpus row for
System/gc; make test + shakesmoke green.
2026-06-25 11:32:32 -04:00
Dmitri Sotnikov
e955b2072a
Merge pull request #209 from jolt-lang/fix/delay-exn-deftype-method-merge
core.cache: full conformance (delay/deftype/dispatch fixes + Thread/CountDownLatch/SoftReference)
2026-06-25 15:19:27 +00:00
Yogthos
774c6c0795 docs: list core.cache 2026-06-25 11:16:17 -04:00
Yogthos
9312ad0937 Thread/CountDownLatch + SoftReference/ConcurrentHashMap so core.cache fully passes
Closes the last clojure.core.cache gaps (now 1314/0/0, including the 1000-thread
cache-stampede):

- java.lang.Thread over Chez fork-thread (shared heap): (Thread. thunk) + start/
  join/run/isAlive, on a "user-thread" tag distinct from Thread/currentThread's
  interrupt shim. java.util.concurrent.CountDownLatch (count/mutex/condition).
- java.util.concurrent.ConcurrentHashMap = the mutable HashMap shim; get / count /
  contains? read it (clojure.core), which SoftCache uses on its backing map.
- java.lang.ref.SoftReference / ReferenceQueue: no JVM GC reference semantics, so
  the referent is held strongly (a SoftCache is unbounded rather than GC-evicting),
  but enqueue / poll work so clear-soft-cache! drains the queue.

JVM-certified corpus rows. make test + shakesmoke green.
2026-06-25 11:15:12 -04:00
Yogthos
5d0989a860 delay exception memoization, deftype cross-protocol method merge, more map-like dispatch
Further clojure.core.cache fixes (198 -> 257 of its assertions):

- delay: a throwing body re-ran on every force and never became realized?. Run it
  once like Clojure's Delay — cache the exception, mark realized, re-throw the same
  on each deref. Fixes value-fn memoization / cache-stampede protection.
- deftype/defrecord: a method name appearing in two protocols with different
  arities (data.priority-map's seq is in IPersistentMap [this] AND Sorted
  [this asc]) registered per-protocol and shadowed; merge clauses by name across
  all protocols into one multi-arity fn.
- empty?/peek/pop (IPersistentStack) dispatch through a deftype's methods; (= a-
  deftype other) uses its equiv method (so caches compare to their backing map);
  seq handles a host iterator (iterator-seq over .iterator).
- pop of an empty PersistentQueue returns it, like the JVM (was an error).

JVM-certified corpus rows. make test + shakesmoke green.
2026-06-25 10:57:31 -04:00
Dmitri Sotnikov
c445aaeaa2
Merge pull request #208 from jolt-lang/fix/destructure-or-prepost-deftype-dispatch
Destructuring :or, fn :pre/:post, deftype field access + map-like dispatch
2026-06-25 14:09:39 +00:00
Yogthos
b21b99b275 destructuring :or, fn :pre/:post, deftype field access + map-like dispatch
General fixes shaken out running clojure.core.cache (66 -> 198 of its assertions):

- Map destructuring applied an :or default only for :keys/:strs/:syms, not a
  direct {x :x} binding — so {x :x :or {x 9}} (and the & {…} kwargs form) ignored
  the default. Apply it for the direct binding too.
- fn didn't implement :pre/:post: a leading conditions map was evaluated as a body
  literal (so % was unbound and (.q %) blew up). Recognize it and assert pre
  before the body, bind % to the result, assert post, return %.
- (.q inst) on a deftype field with no matching method reads the field, like the
  JVM (was "No method q").
- A deftype implementing the clojure.lang collection interfaces now dispatches
  dissoc (without), contains? (containsKey), peek/pop (IPersistentStack), and
  keys/vals (via its Seqable seq) through its methods — they were field-only, so
  core.cache's caches and data.priority-map didn't behave as maps.

JVM-certified corpus rows for each. make test + shakesmoke green.
2026-06-25 10:06:33 -04:00
Dmitri Sotnikov
93b4d101a1
Merge pull request #207 from jolt-lang/fix/io-copy-htable-input-stream
io/copy: drain a byte-input-stream shim source
2026-06-25 13:04:21 +00:00
Yogthos
7952b1fe03 io/copy: drain a byte-input-stream shim source
input-bytes handled in-stream/bytevector/byte-array sources but not a host
byte-input-stream table (:jolt/input-stream — http-client's ByteArrayInputStream),
so io/copy fell through to rendering it as text (#[chez-htable …]) instead of its
bytes. Drain it like slurp does. Fixes http-client's response-body handling
(jolt-bjbi): its suite goes 100/16-fail -> 116/0, and the ring adapter's.
2026-06-25 09:01:02 -04:00
Dmitri Sotnikov
19b19fb83f
Merge pull request #204 from jolt-lang/feat/ring-defaults-host-interop
Host interop + deps fixes for ring-defaults on jolt
2026-06-25 10:40:46 +00:00
Yogthos
b2f671989d docs: list ring-defaults (via jolt-crypto) 2026-06-25 06:37:27 -04:00
Yogthos
65c8072ec8 io/copy: write to a byte-output-stream shim
io/copy handled file/stream/writer targets but not a host byte-output-stream
table (jolt-lang/http-client's ByteArrayOutputStream shim, :jolt/output-stream),
erroring 'don't know how to write to'. Dispatch through the shim's .write method,
byte-exact for a byte source — the JVM's io/copy writes to any OutputStream.
2026-06-25 06:29:57 -04:00
Yogthos
635cab0e49 host interop + deps fixes for running ring-defaults on jolt
Shaken out getting ring-defaults (and its ring-core/anti-forgery/session stack)
to load and serve static resources on jolt. All general fixes, all runtime:

- Class/forName throws a catchable ClassNotFoundException for a class jolt can't
  back (it returned a broken truthy value for any name, and crashed on use). Lets
  the common (try (Class/forName "optional.Dep") (catch ...)) probe libraries use
  to detect an absent dependency work — e.g. ring's joda-time check.
- deps: reconcile native libs (and source roots) in one step, deduped by library
  identity, instead of the ad-hoc distinct at each call site. An app pulling two
  libs that declare the same shared object (libcrypto via both jolt-crypto and
  http-client) now includes and loads it once.
- io: a File answers getProtocol ("file") / getFile so resource-serving
  middleware that expects io/resource to hand back a file: URL works; the
  classloader gains getResources (every source root holding the resource).
- clojure.string/replace accepts a char match/replacement, like the JVM.

JVM-certified corpus rows for the Class/forName and string/replace behavior.
2026-06-25 04:42:35 -04:00
Dmitri Sotnikov
16528e8637
Merge pull request #203 from jolt-lang/docs/libraries-conformance-directive
core.match: full support (regex + array patterns) + library-conformance directive
2026-06-25 04:50:41 +00:00
Yogthos
47b4971367 remove agent files 2026-06-25 00:47:23 -04:00
Yogthos
67e642bdfb core.match: regex + array patterns (full support); library-conformance directive
Finishes core.match — its full test suite (115/115) now passes, including the
two patterns the earlier work left out:

- Regex-literal patterns. A #"…" now reads as a regex VALUE (Clojure parity: the
  reader constructs the Pattern, so a macro receives a regex, not jolt's tagged
  form), and the analyzer compiles a regex value to the same :regex IR leaf via
  its source. emit-quoted handles a quoted regex; a regex value carries the
  java.util.regex.Pattern host tag so extend-protocol/instance? dispatch on it.
- Primitive-array patterns. A ^Type hint's :tag is now the SYMBOL (e.g. `ints`),
  matching the JVM, so core.match's array-tag lookup engages the array
  specialization (alength/aget). jolt's :tag consumers already tolerate a symbol
  (hc-cell-num-ret normalizes; tag->nkind/def-meta handle both).

Also: a library-conformance directive in CLAUDE.md, and the supported-libraries
list (docs + site) simplified to one-line entries — a listed library is assumed
to work fully, so no tallies or feature enumerations. core.match + transit-jolt
added to the list.

Seed change (reader/backend/30-macros) -> re-minted; the rest runtime. JVM-
certified corpus rows; the stale `symbol hint -> :tag` divergence is dropped from
the allowlist (jolt now matches the JVM). make test + shakesmoke green.
2026-06-25 00:46:10 -04:00
Yogthos
5737a39b7c docs: list core.match; add library-conformance directive to CLAUDE.md 2026-06-25 00:19:59 -04:00
Dmitri Sotnikov
d8683b0598
Merge pull request #202 from jolt-lang/feat/deftype-clojure-lang-interfaces
deftype/record: clojure.lang collection interfaces + protocol identity
2026-06-25 04:17:58 +00:00
Yogthos
f5455115a0 deftype/record: clojure.lang collection interfaces + protocol identity
Running clojure.core.match (a macro-heavy library that builds its compiler out
of deftypes implementing clojure.lang interfaces) shook out a cluster of general
gaps. Its own suite goes from not-loading to 111/115 assertions.

- deftype/defrecord implementing a clojure.lang collection interface now drives
  the core fns: Indexed -> nth, Counted -> count, Associative -> assoc, ILookup
  -> get/valAt (non-field keys only, so a method's own field bindings don't
  recurse), ISeq -> seq/first/rest, IPersistentCollection -> conj, IFn -> the
  value is callable. A jrec is still a map of fields by default; the interface
  method wins when declared.

- Multi-arity inline methods are grouped into one fn (a type with (nth [_ i]) and
  (nth [_ i x]) kept only the last before). Built as data, not a nested
  syntax-quote, so a `(= ~ocr ~l) method body keeps its unquotes.

- instance?/satisfies? recognize a protocol a type implements, including a MARKER
  protocol with no methods (core.match's IPseudoPattern) — deftype/defrecord now
  record protocol satisfaction even with zero methods. Added ILookup/Indexed/
  Counted to the instance? taxonomy for the built-in collections.

- Syntax-quote: a fully-qualified class name (clojure.lang.ILookup) stays bare
  instead of being namespace-qualified; (unquote x) is detected in a lazy seq
  (a macro that builds its template with map, e.g. deftype's rewrite-set).

- clojure.set union/intersection/difference are variadic (& sets) + union 0-arity.
- java map view methods: keySet/values/entrySet/size/isEmpty.
- deprecated java.util.Date getters (getYear/getMonth/...) + the multi-arg
  (Date. year-1900 month0 date hrs min) constructor.

Seed change (deftype/defrecord macros + clojure.set) -> re-minted; the rest are
runtime. 11 JVM-certified corpus rows; make test + shakesmoke green.
2026-06-25 00:14:19 -04:00
Dmitri Sotnikov
3cbfa8719c
Merge pull request #201 from jolt-lang/fix/edn-unknown-tag
edn: clean exception for an unknown reader tag
2026-06-25 03:13:35 +00:00
Yogthos
fb2749ac4c edn: clean exception for an unknown reader tag
clojure.edn's __read-tagged seam called (empty-pmap) — applying the empty-pmap
VALUE as a procedure — so an unknown tag (e.g. #object[...] from a JVM-printed
object, or any unregistered #foo) crashed the Chez VM with "attempt to apply
non-procedure" and surfaced a malformed condition (class :object, nil message)
instead of a catchable error.

Throw a clean ex-info naming the tag, matching the JVM's "No reader function
for tag <tag>". A reader port over edn (transit-jolt's read-conformance skip
path, aero, etc.) now catches a real exception instead of aborting.

clojure.core/read-string stays lenient (returns the tagged form) so clojure.edn
can apply :readers / :default before falling through to this throw.
2026-06-24 23:09:07 -04:00
Dmitri Sotnikov
9c48440a43
Merge pull request #200 from jolt-lang/feat/java-io-streams
java.io: full File API + byte/char streams over Chez ports
2026-06-25 02:17:02 +00:00
Yogthos
1853d827bd java.io: full File API + byte/char streams over Chez ports
Expand java.io so libraries that touch the filesystem work unchanged.

File: the full method surface — length, lastModified, can{Read,Write,Execute},
isHidden, list, mkdir(s), delete, createNewFile, renameTo, getParentFile,
get{Absolute,Canonical}File, compareTo/equals/hashCode — plus the statics
separator / pathSeparator / createTempFile / listRoots. A File now keeps the
path as given (new File("rel").getPath() is "rel", .isAbsolute false); a
relative path resolves against JOLT_PWD only when the filesystem is touched,
matching the JVM. slurp/spit and the dir helpers go through the same
resolution, fixing a spit-vs-slurp inconsistency.

Streams (host/chez/io-streams.ss) — each a jhost wrapping a Chez port, so
buffering, EOF and binary<->char transcoding come from Chez:
- FileInputStream / FileOutputStream / ByteArrayInputStream /
  ByteArrayOutputStream / BufferedInputStream / BufferedOutputStream
- FileReader / FileWriter / InputStreamReader / OutputStreamWriter /
  BufferedReader / BufferedWriter
Buffered* return the wrapped stream (Chez ports are already buffered).

clojure.java.io: input-stream/output-stream now yield real byte streams (were
aliased to the char reader/writer); added copy (byte-exact for byte sources),
make-parents, delete-file. with-open also closes file-writer/port-writer/
print-writer (a pre-existing gap).

All runtime shims, no re-mint. 15 JVM-certified corpus rows; make test +
shakesmoke green.
2026-06-24 22:12:46 -04:00