Commit graph

35 commits

Author SHA1 Message Date
Yogthos
e66a91750e Numbers-style category dispatch for binary numeric ops
Arithmetic and comparisons lowered to raw Chez ops, so an operand outside
Chez's tower (BigDecimal) crashed with a raw condition, and Chez contagion
leaked: (* 1.0 0) gave exact 0 where the JVM gives 0.0, (* ##Inf 0) gave 0
instead of ##NaN, (/ 1 0) raised an untyped error.

One seam now (host/chez/seq.ss): call position emits jolt-n* macros with the
both-Chez-numbers fast path open-coded; value position folds through the same
binary ops. Anything outside the tower falls to per-op slow hooks that
java/bigdec.ss extends, so bigdec arithmetic works in every position (the old
static-only :bigdec typing limitation is gone). JVM rules patched into the
fast path: a double operand wins, an exact zero divisor throws
ArithmeticException while a double zero divisor yields Inf/NaN, quot/rem/mod
cover ratios and doubles, min/max return the original operand with NaN
winning, a nil operand is NPE and a non-number CCE, zero-arg -// throw
ArityException at runtime instead of failing expansion.

Also: with-precision now binds *math-context* and bigdec results round with
real RoundingMode semantics (UNNECESSARY throws; division rounds to precision
instead of throwing); rationalize goes through the shortest decimal print
like BigDecimal.valueOf (the identity stub is gone); ratios coerce to bigdec
like Numbers.toBigDecimal; min/max int-literal operands no longer coerce to
flonum in the numeric pass.

Perf neutral: fib and seq benches unchanged (the fast path is two type checks
the optimizer folds); hinted fl/fx paths untouched. 19 JVM-certified corpus
rows; cts baseline 5614->5730 pass, 192->88 errors, 84->79 baselined
namespaces.
2026-07-02 06:41:45 -04:00
Yogthos
8ffc2f68c5 Fix general divergences surfaced by clojure-test-suite
Five fixes shaken out by running jank-lang/clojure-test-suite:

- = short-circuits on identity like Util.equiv's k1 == k2, so (= s s) on an
  infinite lazy seq answers true instead of walking forever. Numbers keep the
  exactness-aware arm ((= ##NaN ##NaN) stays false like the JVM's).
- Calling a non-fn names the operator's CLASS in the ClassCastException, like
  the JVM — never the value, whose printed form may be unbounded: ((range))
  must throw, not hang rendering an infinite seq.
- realized? on a seq cell answers by its forced flag (the rest of a realized
  lazy chain is a cseq under jolt's seq model), and the overlay's unsupported-
  type error names the class, not the (possibly infinite) value.
- clojure.test/is dispatches a REGISTERED assert-expr method before its by-name
  inline paths, like clojure.test where the built-ins are just pre-registered
  methods — so an alias-qualified p/thrown? (the suite's portability helper)
  isn't captured by the built-in thrown? path, which read its body as a class.
- clojure.test tracks tests and fixtures per namespace: deftest records its
  defining ns, use-fixtures registers under the calling ns (no more cross-ns
  clobbering), (run-tests 'ns ...) runs only those namespaces like clojure.test,
  and each run-tests call prints/returns its own summary (global counters stay
  cumulative for the n-pass/n-fail harness API).

Re-mint (20-coll.clj is seed; prelude only). +2 JVM-certified corpus rows;
the clojure-test fixture pins the alias-qualified assert-expr, per-call
summaries, and ns filtering.
2026-07-02 03:46:57 -04:00
Yogthos
01f98c2e89 Route deftype/reify interface dispatch through one seam
The "does value V declare method M; if so call it" decision was re-derived in a
dozen places with two different lookup helpers — records.ss jrec-cl and
collections.ss rec-coll-method were a byte-for-byte duplicate — and reduce only
honored a reify's own reduce method, not a deftype's:

  (reduce + 100 (->Rng 5))  ; Rng deftype implementing IReduceInit
  => "not seqable"          ; JVM: 110

Add iface-method / iface-call: one lookup that resolves a method for a deftype OR
a reify, with arity, and is the seam a core fn's interface arm collapses to.
jrec-cl now aliases rec-coll-method (the duplicate is gone). reduce routes its
IReduceInit arm through iface-method, so a deftype's reduce drives the reduction
like a reify's, reduced short-circuit included.

Runtime only, no re-mint. make test green (0 new/stale), +2 corpus rows.
2026-07-01 15:59:01 -04:00
Yogthos
9bcac13fd2 Fix seven more JVM divergences (rewrite-clj full suite)
Running the whole rewrite-clj test suite (159 tests) surfaced seven more bugs;
with these it passes 3377/0/0. Each is a general jolt/JVM divergence:

- *out* was pinned to the startup stdout port, so (.write *out* …) escaped a
  with-out-str capture (z/print writes via *out*). It now resolves the live
  current-output-port, like print/__write, so a redirect is seen.
- nth / assoc past the end of a vector or seq threw a bare Chez error (class
  :object). Throw IndexOutOfBoundsException, matching the JVM.
- A number's .toString(radix) ignored the base. Render in the base, lowercase
  (rewrite-clj rebuilds 0xff / 0377 / 2r1001 through it).
- A required namespace's own :as aliases leaked into its requirer: the loaded ns
  form compiles while (chez-current-ns) is still the requirer, so ce-scan-requires!
  registered the loaded ns's aliases under the wrong ns and clobbered a same-named
  alias there. Register an (ns NAME …) form's aliases under NAME.
- A quoted collection dropped its metadata; now it keeps USER metadata (drops the
  reader's :line/:column/:file), like a Clojure quoted constant.
- enumeration-seq only did (seq e); it now drives a java.util.Enumeration through
  hasMoreElements/nextElement, and StringTokenizer implements them.

Regressions: corpus rows (with-out-str/*out*, nth/assoc bounds, toString radix,
quote metadata, enumeration-seq) certified against JVM; a smoke fixture for the
alias leak (a required ns's alias must not leak). tools.reader + rewrite-clj added
to docs/libraries.md. make test green.
2026-07-01 14:17:03 -04:00
Yogthos
bd33d605ef Chunk range/map/filter to match JVM Clojure
range, map, and filter were fully element-by-element lazy, so
(map f (range 1 50)) realized one element per first/nth where JVM
Clojure realizes a whole 32-element chunk. range is a chunked
LongRange on the JVM and map/filter are chunk-preserving, so the
observable side-effect timing differed.

Following clojure.lang.LongRange, ChunkedCons, ChunkBuffer and
core.clj, this adds a crest field to the cseq record and a
cseq-chunked constructor modeling ChunkedCons (a standalone chunk
pvec, an offset, and the after-chunk seq). The chunk accessors move
to seq.ss next to the representation they read. map/filter/remove
take a chunked branch when the source is chunked, realizing the whole
chunk and chunk-cons'ing it onto a lazy rest, so their output is
itself chunked and chained transforms each batch by 32. Bounded range
is now an eager chunked seq, and the reduce fast path flows through a
ChunkedCons rest. The chunk-buffer/chunk/chunk-cons builder API in
natives-array.ss now produces a real ChunkedCons.

Single-arg (range), multi-coll map, and plain lazy seqs stay
element-by-element, like the JVM.

Adds a lazy / chunking suite to the corpus that observes realization
timing via an atom counter: first over a chunked map realizes 32,
crossing a chunk boundary realizes 49, chained maps batch [32 32],
filter applies the predicate to the whole first block, and a plain
lazy seq still realizes one element at a time. Two cases that
documented the old over-laziness now assert the JVM value of 32 and
were dropped from the allowlist. certify against JVM Clojure 1.12.3
reports 0 new and 0 stale divergences.
2026-06-29 22:02:06 -04:00
Dmitri Sotnikov
83ff96c3c8
Merge pull request #265 from jolt-lang/conformance/lazy-map
seq fns are lazy by default (LazySeq), like Clojure
2026-06-28 05:31:25 +00:00
Yogthos
b879430618 seq fns are lazy by default, like Clojure (LazySeq, not eager-headed)
map/filter/remove/take/drop/concat/take-while/drop-while/mapcat/partition
built an eager-headed cseq: the first element (and the fn application) ran
at construction, so a side-effecting (map f coll) fired f immediately and
(class (map …)) was PersistentList instead of LazySeq. This diverged from
Clojure, which wraps the whole body in lazy-seq. It went unnoticed because
the conformance gate certifies values, not realization — eager and lazy
heads produce identical values — and unit.edn even baked PersistentList in
as expected. test.check's for-all-takes-multiple-expressions (which counts
side effects in a for-all body) exposed it.

Wrap each native producer's result in a lazy-seq node so the body, incl.
the first element, defers until forced — the forced cseq still has eager
heads, so reduce/count/dorun/etc. force on walk and there's no per-element
cost. dedupe's (seq coll) is moved inside its lazy-seq. A jolt LazySeq is
now recognized by coll?/empty, the analyzer's form predicates (a macro can
build its expansion with map), value-host-tags + instance? (LazySeq/ISeq/
Sequential), and reports clojure.lang.LazySeq.

Kept the native Scheme implementations rather than porting Clojure's: a
straight lazy-seq+cons port is 3x slower and Clojure's chunked fast path is
288x slower because jolt's chunk machinery is unoptimized (filed jolt-j9dz);
the wrapped natives are Clojure-lazy at native speed.

+12 corpus rows (laziness at construction, LazySeq type, both JVM-certified).
make test + shakesmoke green, selfhost holds, 0 new divergences.
2026-06-28 00:16:47 -04:00
Yogthos
a49ca3b5ea jolt-wrap64 fast path: skip the mask when already in signed-64 range
Chez fixnums are 61-bit, so the bignum bitwise-and mask allocates for any
value past 2^60 — and unchecked-* ran it on every result, even small
in-range ones. An exact integer already in [-2^63, 2^63) is its own wrap,
so return it directly; only an out-of-range result (a multiply overflowing
into 128 bits) needs the mask. ~30% on in-range unchecked-add loops,
neutral on full-64-bit multiply.

Note: the 64-bit arithmetic floor on Chez stays ~31ns/multiply (bignum, no
native 64-bit int); the test.check distribution hangs are dominated by
generator/dispatch overhead, not arithmetic — this is a general win for
long-heavy code, not a fix for those.
2026-06-27 23:12:16 -04:00
Yogthos
253d64b1e7 unchecked-* on a ratio (or any non-long) shouldn't wrap to 64-bit
jolt-unc{add,sub,mul,inc,dec,neg}2 wrapped every non-flonum result to a
signed 64-bit integer, so (unchecked-add 2/3 2/3) truncated to 1 instead
of 4/3. Under *unchecked-math* the analyzer rewrites +/-/* to unchecked-*,
so any ratio arithmetic in such a file silently floored. Clojure's
unchecked-add falls back to regular arithmetic for non-primitives; only
long math wraps. Wrap iff both operands are exact integers.

Shaken out by test.check's gen/ratio monoid property (the + and 0 monoid
held for small-integers but failed for ratios).
2026-06-27 22:49:42 -04:00
Yogthos
f32bd335e3 test.check generators: rand-double, take +Inf, UUID/Long/shiftLeft, transient
More general fixes from clojure.test.check's own suite.

- *unchecked-math* on doubles: unchecked-* only wrap integer math; on a flonum
  operand they're an ordinary float op (Clojure: (unchecked-multiply 1.5 2.0) =>
  3.0). test.check's rand-double is (* double-unit shifted) under *unchecked-math*
  and was truncating to a long 0, so every distribution-driven generator (choose,
  vector, …) collapsed to its lower bound.
- (take Double/POSITIVE_INFINITY coll) takes the whole coll instead of throwing
  on the infinite count coercion (rose-tree unchunk relies on it).
- (java.util.UUID. msb lsb) 2-long constructor (the uuid generator), formatted as
  the canonical lowercase 8-4-4-4-12 string; (Long. n) constructor; BigInteger
  .shiftLeft / .shiftRight (size-bounded-bigint); number methods now receive args.
- A transient (ITransientSet) responds to .contains / .valAt / .count
  (distinct-collection generators).

make test green (+3 corpus rows, 0 new divergences), runtime only (no re-mint).
2026-06-27 19:08:34 -04:00
Yogthos
3340635714 ^long is a 64-bit long: fast-path-with-fallback ops + logical unsigned shift
Completes the JVM long-compatibility gap so clojure.test.check (and the
property-based suites built on it, e.g. data.codec) run on jolt.

A ^long is 64-bit but a Chez fixnum is only 61-bit, so the backend's fast fx
comparison / quot / min / max / inc / dec ops raised on a full-width long (one
from the PRNG or wrapping arithmetic). They now go through the jolt-l* macros
(host/chez/seq.ss): the fx fast path when the operands ARE fixnums, the generic
op otherwise — so e.g. ((fn [^long a ^long b] (< a b)) Long/MAX 1) is false, not
an error. Arithmetic +/-/* keep the raw fx ops (under *unchecked-math* they're
already the wrapping unchecked-*).

Also fixes unsigned-bit-shift-right: it was an arithmetic (sign-propagating)
shift, now a logical shift over the 64-bit two's-complement window, so
(unsigned-bit-shift-right -1 1) is 2^63-1 like the JVM.

Result: test.check 1.1.3 loads and runs (generators, quick-check, shrinking);
data.codec's base64 property suite passes (12/12 defspecs; the 2 deftests check
clojure.lang.IFn$OLLOL, a JVM primitive-fn interface, N/A). Both added to
docs/libraries.md + the site.

re-mint (backend/seed). make test green (+3 corpus rows, 0 new divergences,
numeric gate updated to the jolt-l* ops), shakesmoke byte-identical.
2026-06-27 16:04:19 -04:00
Yogthos
a028cab04f Unchecked / *unchecked-math* arithmetic wraps to signed 64-bit
clojure.core's unchecked-* (and +/-/*/inc/dec under *unchecked-math*) are long
ops that WRAP on overflow; jolt's checked arithmetic is arbitrary-precision and
its unchecked-* were plain non-wrapping (+ x y), diverging from the JVM. Now they
truncate to the low 64 bits as a signed long, matching Clojure:

  (unchecked-add 9223372036854775807 1)        => -9223372036854775808
  (unchecked-multiply 9223372036854775807 …)   => 1

- host/chez/seq.ss: jolt-wrap64 + binary jolt-unc{add,sub,mul,inc,dec,neg}2 and
  the variadic clojure.core/unchecked-* fns (def-var!'d in natives-seq.ss, where
  def-var! is bound). The overlay's plain unchecked-* defns are removed.
- backend lng-ops: unchecked-+/-/* emit the wrapping jolt-unc* helpers (the
  raising fx ops can't wrap on Chez's 61-bit fixnums); unchecked-inc/dec too.
- *unchecked-math* is honored: the analyzer reads it (jolt.host/unchecked-math?)
  and rewrites +/-/*/inc/dec to their unchecked-* for the rest of a file that
  (set!)s it, like the JVM.
- jolt->fx: a ^long value that overflows the 61-bit fixnum range passes through
  as an exact integer instead of erroring (a full-width long from wrapping math).

Also adds Long/bitCount / numberOfLeadingZeros / reverse and Math/getExponent /
scalb (test.check's splittable PRNG uses them).

This lets clojure.test.check load and run quick-check on jolt. re-mint (analyzer/
backend/overlay are seed sources). make test green (+6 corpus rows, 0 new
divergences, numeric gate updated), shakesmoke byte-identical.
2026-06-27 15:41:35 -04:00
Yogthos
4cf95dc27c core.async: higher-level API over native channels + two general fixes
Adds clojure.core.async's higher-level dataflow API as a Clojure overlay
(stdlib/clojure/core/async.clj) over jolt's native channel primitives, plus
clojure.core.async.lab. The native layer (host/chez/java/async.ss) gains
offer!/poll!, put specs and :priority/:default in alts!, a transducer
ex-handler arg to chan, unblocking-buffer?, promise-buffer, and on-caller?
handling for put!/take!. The overlay covers alts!/pipe/pipeline/split/
reduce/transduce/into/take/mult/mix/pub-sub/map/merge/onto-chan/to-chan and
the deprecated map</map>/filter>/... family (rewritten as go-loops since the
JVM versions reify the impl handler protocol jolt doesn't expose).

Loading: the native primitives pre-seed clojure.core.async, so the loader now
drops it from the loaded set and a require pulls the overlay from the source
roots like clojure.test (AOT-bundled into built binaries).

Running clojure/core.async's own suite shook out two general bugs:
- :refer with a list form, (:require [ns :refer (a b c)]), dropped the names
  (only the vector form was handled) — chez-register-spec! now accepts both.
- (range 0) / (range 5 5) returned nil instead of the empty seq () — empty
  ranges now match Clojure, so (= () (range 0)) holds.

Suite: async_test 15/20, pipeline_test 7/7, timers_test 2/2, lab_test 2/2.
The five non-passing async_test cases all assert JVM go-machine limitations
jolt's thread-based model is a superset of (the 1024 pending-op cap, parking
ops that must throw outside a go block, expanding-transducer buffer
backpressure) or dispatch-thread identity, not data semantics.

make test green (0 new divergences, +4 range corpus rows), shakesmoke
byte-identical.
2026-06-27 13:05:19 -04:00
Yogthos
b74dbfd2f0 Symbols are IFn (invoke as a map lookup)
('sym coll) / ('sym coll default) now do (get coll 'sym ...), like keywords —
a symbol is IFn on the JVM. jolt threw "cannot be cast to clojure.lang.IFn".
Pre-existing gap (not a regression), surfaced by honeysql's :checking mode,
which does ('where dsl) to look up a clause. honeysql 623/13/8 -> 635/8/1.
Corpus rows added.
2026-06-26 22:05:21 -04:00
Yogthos
448611a5df Match Clojure's lazy seq realization model
jolt's seq layer realized one element ahead of Clojure, so a side-effecting
lazy seq ran its producer too eagerly. Four changes bring it in line:

- rest is Clojure's more(): it returns the tail without realizing it. An
  unforced tail (vector / string / lazy-seq cell) comes back as a deferred
  seq, so (rest (iterate f x)) does not call f. next still realizes one.
- iterate applies f lazily, inside the tail thunk, so (first (iterate f x))
  is x with no call to f (clojure.lang.Iterate parity).
- take realizes exactly n: the last element terminates without touching the
  rest, instead of forcing one more element of the source.
- an empty realized lazy seq is still a sequence value, printing "()" not
  "nil" (a JVM LazySeq is never nil).

Also: the map transducer's step fn now takes multiple inputs
([result input & inputs]) so a multi-collection transduce applies f across
all of them. Fixes medley's join/window/sequence-padded laziness and
multi-input transducer tests (now 293/293). The rest change also fixed a
latent overrun in distinct/dedupe over a map's empty tail.

iterate is a seed source, re-minted.
2026-06-26 19:41:02 -04: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
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
7b1ec9a1d3 java.time DST + data readers: make tick pass fully
Shaking out tick's api and alpha.interval suites (api 353->359, interval
0->103 passing) cleared a set of general gaps:

- Named-zone DST. Zones resolved to a fixed representative offset, so
  America/New_York in August read -05:00 not -04:00. Add US/EU DST rules
  (compact transition-date math) and make instant<->zoned, the zone rules'
  getOffset, and the zoned equality arm DST-aware.

- Nanosecond zoned/offset times. Instant is nanos but atZone/atOffset/
  toInstant/withZoneSameInstant and Instant/parse went through epoch-ms,
  truncating sub-ms. Route them through nanos.

- Locale month/day names. A formatter dropped its Locale; carry it and add
  French names so MMM under Locale/FRENCH renders "mai".

- Callable records. A defrecord implementing clojure.lang.IFn (tick's
  GeneralRelation) is now invokable: jolt-invoke dispatches to its inline
  invoke method. Also give collections the Iterable host tag so a protocol
  extended to Iterable matches vectors/seqs.

- Imported class short names. (:import [java.time ZonedDateTime]) then
  (. ZonedDateTime parse s) resolved to nil; an otherwise-unresolved bare
  Capitalized name that's a registered host class now resolves as a class.

- Data readers. A project's data_readers.{clj,cljc} is loaded into
  *data-readers* (reader namespaces required eagerly); registered #tag
  literals in source rewrite to (reader-fn 'form). clojure.core/read-string
  now applies #inst/#uuid/#"regex" and *data-readers* like Clojure.

- Duration/between accepts zoned/offset date-times.

All runtime shims, no re-mint. docs/libraries.md: tick full pass + aero.
2026-06-24 21:30:05 -04:00
Yogthos
4ae3d3116e Collection ops carry the receiver's metadata
conj/assoc/dissoc/disj/pop/into and empty now thread the receiver's
metadata onto the result, matching Clojure (each op constructs a new
collection with meta() carried forward; coll.empty() is
EMPTY.withMeta(meta())). The metadata side-table is now weak so meta on
intermediate collections is reclaimed with them, and empty-list-t carries
an (unused) field so a metadata-bearing () is a distinct identity from the
shared singleton instead of leaking meta onto every ().

Unblocks metadata-driven walks (aero/integrant): (into (empty form) ...)
now preserves a vector/map/set's metadata, so a postwalk whose outer fn
reads (meta x) sees it.
2026-06-24 13:46:58 -04:00
Yogthos
e93006b4be Dead-code removal, perf fixes, deterministic seed emission
Round 1 (correctness + dead code):
- Fix duplicate java.util.HashMap registration in host-static.ss: the alist
  impl shadowed the hashtable ctor while leaving the hashtable methods bound,
  so .keySet/.values/.remove/.clear crashed. Drop the alist version.
- Delete jolt-core/jolt/reader.clj: a 463-line dead duplicate reader, never
  required or compiled (the live reader is host/chez/reader.ss) and drifted.
- Remove dead defs: ir/rt + :rt op + unused ir/op; the Janet branch in
  clojure.edn/drain-reader; a shadowed first clojure.string/trim-newline;
  io.ss jolt-char-array + the reader def-var (both shadowed by natives-array);
  concurrency.ss jolt-future-done?*; compile-eval.ss jolt-analyze-emit.

Round 2 (perf + determinism):
- emit-quoted-map-value / quoted sets now emit sorted by emitted text instead
  of host-hash order, which isn't stable across Chez versions (jolt-8479).
- jolt-into folds through a transient, so into/vec/mapv/filterv onto a vector
  are O(n) instead of O(n^2).
- deps resolve-deps walks its queue with an index cursor (was subvec-per-pop).
- async channel and agent action queues use amortized-O(1) FIFOs; ArrayList is
  backed by a growable vector (O(1) add/get) instead of a list.
2026-06-23 01:05:45 -04:00
Yogthos
33eff7c7d8 Clean up codebase: rename stdlib layer, strip porting residue, fix tooling
Rename src/jolt -> stdlib (the runtime-loaded layer; jolt-core stays the
seed-baked layer) and update the loader / emit-image / doc paths. Drop dead
code: the spike/ experiments, the duplicate clojuredocs-export.edn (json moves
to tools/), the Janet-era jolt.http binding, and the orphaned
persistent_vector.clj whose ns/path didn't even match.

Strip porting residue from comments and docstrings across host/chez, jolt-core,
stdlib, tests, and docs: internal issue ids, "Phase N" markers, and the "vs
Janet" historical exposition, leaving present-tense descriptions and the real
JVM-Clojure semantic contrasts. Same pass over the corpus suite labels. The seed
is unchanged (docstrings/comments aren't emitted), so the self-host fixpoint and
corpus are untouched.

Port tools/spec_coverage.py off the dead janet probe to bin/joltc and regenerate
coverage.md; drop the dead :host/janet rule from certify.clj and regenerate the
conformance profile. Add docs/host-interop.md (the JVM shims and how to register
your own host class from a library) and a writing-style note in CLAUDE.md.

Stabilize the four racy concurrency corpus cases (future-cancel and agent
send/send-off): give the future a sleeping body and the agent a slow action, so
cancel reliably catches an in-flight future and deref reliably reads the
pre-update snapshot. They certify deterministically now, so drop their :flaky
allowlist entries and the orphaned legend.
2026-06-22 22:18:00 -04:00
Yogthos
6ab65a30e3 Fix arg evaluation order + host interop gaps so reitit/selmer/honeysql run
Shaking the ring-app example's real library stack out against jolt surfaced a
batch of divergences from JVM Clojure, the biggest being evaluation order.

backend_scheme: call and recur arguments were emitted as bare Scheme operands,
so Chez's unspecified (right-to-left) order won out. Clojure evaluates left to
right, which selmer's reader loop relies on: (recur (add-node ... rdr) (read-char
rdr)) consumed a char early and dropped the first chars of every {{tag}}. Bind
operands to fresh temps in a let* (only when two or more can have side effects,
so hot calls over locals/consts stay un-wrapped). emit-ordered already did this
for collection literals; generalize it.

host-contract: syntax-quote now resolves the alias part of a qualified symbol
(impl/foo -> clojure.tools.logging.impl/foo) instead of leaving it bare, which
limped along via short-name matching until two loaded namespaces (reitit.impl,
clojure.tools.logging.impl) shared the short name and it broke.

collections: key-hash masks with bitwise-and, not fxand — jolt-hash is set!-
decorated per type (records return their own hash) and Chez's equal-hash can be a
bignum, so a key's hash isn't always a fixnum.

seq: even?/odd? handle bignums (JVM accepts any integer; the fxand crashed).

records: Keyword/Symbol .sym/.getName/.toString (honeysql's :clj branch reads
(.sym k)); Throwable .getMessage/.toString over a Chez condition.

host-static: __register-class-ctor!/__register-class-statics! so a host shim
(reitit.trie-jolt) can mirror a Java class.

natives-str: String.intern returns the string.

sqlite: jdbc.core fetch/fetch-one kebab-case column keys (the jolt-lang/db
convention; created_at -> :created-at).

io: a relative io/file path resolves against JOLT_PWD (the user's cwd), not the
repo root the launcher cd'd to — matches JVM cwd semantics, so config.edn loads.

cli: render an uncaught jolt throw (ex-info message + ex-data, or a condition)
instead of Chez's opaque "non-condition value" dump.
2026-06-22 05:26:09 -04:00
Yogthos
3721423a64 real chunked seqs for vector seqs
A vector's seq is now a real chunked-seq (chunked-seq? true), matching Clojure/
CLJS. Each vector-seq cell carries its backing vector + element index as two
cseq fields (cvec/ci, no extra allocation vs the old lazy cell), so:
  - chunk-first hands out a 32-element block (a pvec slice), chunk-rest is the
    seq at the next block boundary — the ChunkedSeq contract (chunk-first ++
    chunk-rest == the seq);
  - reduce/transduce take a fast path that walks the backing vector by index in
    a tight loop with no per-element seq cells (reduce over a 1M-vector ~0.4s).
The seq cell stays a cseq, so first/rest/count/printing and the ~26 cseq?
dispatch sites are untouched. The eager chunk-buffer model (chunk-buffer/chunk/
chunk-cons) is preserved for the round-trip case. No seed change (runtime only).
2026-06-22 00:21:05 -04:00
Yogthos
f15a4e7747 reduce dispatches to a reify's IReduceInit reduce method (jolt-z1zu)
(reduce f init (reify clojure.lang.IReduceInit (reduce [_ f i] ...))) tried to
seq the reify and threw 'not seqable'. When the coll is a reify carrying a reduce
method, drive the reduction through it. Corpus 2693->2694.
2026-06-21 17:46:20 -04:00
Yogthos
48e2ef5910 Scrub dangling Janet references; drop dead Janet-coupled files
Rephrase comments that pointed at deleted Janet files (emit.janet, the seed
sources, 'the Janet back end punts ...') to present-tense descriptions of the
Chez behavior. Comment/docstring-only; the self-host fixpoint is unchanged
(comments don't affect the compiled seed).

Delete five files that were Janet-host shims with no Chez path: clojure.java.io
(provided natively by host/chez/io.ss), and jolt.{nrepl,png,interop,shell}
(the janet.* bridge, os/shell, janet.net — none exist on Chez).

jolt-cf1q.6
2026-06-21 12:01:04 -04:00
Yogthos
467ad75ff7 Chez numeric tower: exact ints / Ratio / double for JVM parity (jolt-n6al)
jolt was all-flonum (one :number type, inherited from Janet whose only number
type is a double). The Chez runtime has a full numeric tower, so the zero-Janet
path now carries it = JVM Clojure semantics:

  (/ 1 2)      => 1/2      (exact Ratio, was 0.5)
  (integer? 3) => true   (integer? 3.0) => false   (float? 3.0) => true
  (ratio? (/ 1 2)) => true   (= 3 3.0) => false   (== 3 3.0) => true
  (+ 1 2) => 3 (exact)   (/ 1.0 2) => 0.5 (double)

jolt= was already exactness-aware (values.ss) and == is value-equality, so
=/== match the JVM split. The reader preserves exactness (integer literals exact,
a/b ratios exact rationals, decimals/exponents flonums); backend_scheme emit-const
renders exact ints/ratios and flonums faithfully; the value-position arithmetic,
count, int, compare, bit ops, parseLong, string .length/.indexOf, range,
timestamps, and array bytes return exact integers (= JVM int/long) instead of
coercing to flonum. double/parseDouble/clojure.math floor|ceil|signum stay double.

Only the zero-Janet path carries the tower (the Janet reader loses exactness into
a double before emit). The prelude/all-flonum path is unaffected for compiled code;
the runtime reader is shared, so a couple of all-flonum reader assertions become
value (==) assertions. ~16 numeric corpus cases now give the JVM tower value vs the
Janet-era :expected and are allowlisted as tower divergences (Chez == reference
JVM) pending the corpus flip to JVM (jolt-ecz0). No BigDecimal type (1M).

Re-minted. zero-janet 2682 (floor 2698->2682, the reclassified tower cases), 0 new
divergences; fixpoint 10/10, bootstrap 6/6, spine 35/35, cli 49/49; Janet gate 155
files 0 failed.
2026-06-20 23:09:27 -04:00
Yogthos
1ba19759c8 Chez parity: missing native core fns + ns runtime fns (jolt-cf1q.7)
Native Chez shims for clojure.core fns that live in the Janet seed but had none
on the zero-Janet spine (they resolved to nil -> "not a fn"):

- host/chez/natives-parity.ss: hash / hash-combine / hash-ordered-coll /
  hash-unordered-coll (24-bit masked like core_extra), transient?, rseq (vectors +
  sorted colls), cat (transducer).
- jolt-invoke now dispatches a TRANSIENT vec/map/set as a fn (callable on the JVM):
  ((transient [10 20 30]) 1) -> 20.
- ns.ss: ns-resolve, ns-imports, remove-ns, intern, alias, ns-unalias, refer,
  ns-refers, refer-clojure, alter-meta!, reset-meta!, and a real ns-aliases (was a
  stub returning {}).
- runtime (require ...)/(use ...) now register :as/:refer into the Chez ns tables
  (was a no-op). The Chez analyzer already pre-registers at analyze time, but when
  the JANET analyzer compiled the form (prelude path) the Chez tables stayed empty,
  so ns-aliases/ns-resolve over an alias diverged — this fixes both paths.

Seed unchanged (overlay doesn't reference these at mint time). zero-Janet corpus
2567->2600, prelude 2557->2590, 0 new divergences on either; Janet gate + JVM cert
green. Filed jolt-vgrp for the pre-existing var-get-of-scalar-native-op quirk.

jolt-cf1q.7
2026-06-20 15:46:11 -04:00
Yogthos
8b86174b91 Chez Phase 3 inc7: corpus on the Chez-hosted analyzer + a 50x-faster gate
Point the Chez-HOSTED analyzer at the full parity corpus (read -> analyze ->
emit -> eval, all on Chez, no Janet) and close the divergences so the
self-hosted compiler is faithful: 0 divergences, 2159/2494 pass.

Keystone: the on-Chez emitter ran with prelude-mode off, so every call to a
non-native clojure.core fn tripped the "unsupported stdlib fn" out-of-subset
guard. The zero-Janet spine always has the full prelude loaded, so turn
prelude mode on in compile-eval.ss (22% -> 84% pass on a sample).

Faithfulness fixes (each was the Chez host/reader diverging from the Janet
analyzer; fixed in the keeper, not the seed):
- emit-const read a char's codepoint via (get v :ch) — the Janet rep; on Chez a
  char is native. Route through a new form-char-code host-contract fn (41 cases).
- next over a lazy seq returned the empty-list terminator (truthy), not nil, so
  butlast and other (if (next s) ...) loops ran one step too far — broke
  some->/some->>/cond->>.
- reader: radix literals (2r1010/16rFF/36rZ), #^ deprecated metadata, ^meta on
  collections (lowers to a runtime with-meta form like the Janet reader),
  map-literal source order (values eval left-to-right), and nested syntax-quote
  over a literal collapses at read time.
- keyword "a/b" splits into ns/name like the seed (destructure {:keys [x/y]}).
- form-syntax-quote-lower implemented on Chez (was a throwing stub).

7 divergences allowlisted: the same print-method-multimethod / host-class set
the prelude gate defers. 328 crashes remain = shared runtime breadth (host
interop, missing core fns, eval/load-string) deferred to Phase 4 / jolt-r8ku,
not compiler faithfulness.

Gate + speedup: test/chez/run-corpus-zero-janet.janet (floor 2159). Its batched
runner (driver/eval-corpus-zero-janet) runs every case in ONE chez process —
load the runtime once, guard + reset the user namespace per case — instead of a
fresh process per case: 1379s -> 1.6s.

spine-test 35/35; Janet gate 151/0; prelude parity 2295/2494 unchanged, 0 new
divergences.
2026-06-20 01:11:54 -04:00
Yogthos
77026fa9ec Chez Phase 2 (inc U): list? + map-entry-as-vector + clojure.walk (jolt-75sv)
list? was nil on Chez because one cseq record backs both lists and lazy/
realized seqs. Add a list? marker field (cseq v2) set only on the HEAD cell
of a list -- (list ...), quoted list literals, cons, reverse, conj onto a
list. rest/next/seq/map therefore yield unmarked seq cells, so they are
seqs and not list?, matching the seed (where rest-of-a-list is a non-list
seq). Empty () is treated as a list.

vector?: drop the map-entry exclusion. Clojure's MapEntry implements
IPersistentVector and the seed agrees -- (vector? (first {:a 1})) is true.
Only dot-forms' coll dispatch read jolt-vector?, where a 2-vector entry is
correct.

clojure.walk + clojure.template join the prelude stdlib tier. The driver
now evals each stdlib ns's requires -- and the ns form's (:require ...)
clause -- so an aliased ref (template's walk/postwalk-replace) resolves at
emit time instead of lowering to an Unknown class host-static. ns forms are
evaled for that side effect but not emitted, so the runtime *ns* doesn't
leak to the last stdlib ns.

Parity 2163 -> 2176, 0 new divergences. New test/chez/_walk.janet 39/39.
2026-06-19 09:30:18 -04:00
Yogthos
e434a7d352 Chez Phase 2 (inc G): lazy-seq bridge (make-lazy-seq / coll->cells)
The lazy-seq macro expands to (make-lazy-seq (fn* [] (coll->cells body)))
and lazy-cat to (concat (lazy-seq c) ...); both seed natives were nil on
the prelude, so every overlay fn built on lazy-seq — repeat/iterate/
cycle/dedupe/take-nth/keep/interpose/reductions/map-indexed/distinct/
interleave/tree-seq(->flatten)/partition-all/lazy-cat — hit apply-jolt-nil.

lazy-bridge.ss bridges to the cseq model: a jolt-lazyseq is a deferred
seq forced once by an extended jolt-seq; jolt-cons defers a lazyseq tail
so an infinite (repeat/iterate/cycle) stays lazy. A lazyseq is a new
value type, so the dispatchers that don't route through jolt-seq learn it
(sequential? for =/hash, plus count/empty?/nth/printers) or a raw
unrealized lazyseq escapes — the corpus compares (= [1 3 5] (take-nth …))
against it directly.

seq.ss: jolt-concat is now fully lazy (the rest isn't forced until the
first coll is exhausted), so a self-referential lazy-cat — fib =
(lazy-cat [0 1] (map + (rest fib) fib)) — no longer memoizes its tail as
empty by reading fib before its def binds.

Prelude parity 1837 -> 1886, 0 new divergences. Floor raised to 1886.
2026-06-18 13:53:02 -04:00
Yogthos
9b0d6acadc Chez Phase 2 (inc A): collection ctors + real map entries
set/hash-map/hash-set/array-map/rand resolved to jolt-nil on the prelude
(the apply-jolt-nil crash bucket) — the pmap/pset ctors already existed in
collections.ss, just bind the public clojure.core names to them.

Map entries are now a distinct type: a pvec carries an ent flag (default #f),
so an entry equals its [k v] vector and walks like one (nth/count/seq/=/hash/
print read only v) but is not vector? and is map-entry? — matching Clojure's
MapEntry. seqing a map produces flagged entries; vector? excludes them. This
unblocks key/val (overlay fns gated on map-entry?) and the every? map-entry?
cases.

Prelude parity 1534 -> 1593, 0 new divergences. jolt-agw6.
2026-06-18 10:44:33 -04:00
Yogthos
739b219d0e Chez Phase 1 (increment 3n): seq-native shims + reduced
The dominant prelude-parity crash bucket was 'apply non-procedure jolt-nil':
core fns calling seed-native seq fns (core_coll.janet) that have no Chez RT
shim, so var-deref returns jolt-nil. A static scan of the assembled prelude
turned up 52 referenced-but-undefined clojure.core names.

host/chez/natives-seq.ss shims the safe seq fns over the existing seq layer:
mapcat, take-while, drop-while, partition (collection arities only — the 1-arg
transducer forms are jolt-kxsr), and sort (compare default; a comparator may
return a 3-way number or a boolean less-than). reduced/reduced? is a jolt-reduced
record in seq.ss that reduce short-circuits on and deref unwraps, so unreduced
works. identical? = jolt= (the seed's definition).

Deferred list?: a Chez lazy seq and a list are both cseq, so it can't be told
apart without a distinct list type — a real divergence risk.

Parity 1407 -> 1467/2497, 0 new divergences. emit-test 263/263.
2026-06-17 23:16:26 -04:00
Yogthos
930800f9a6 Chez Phase 1 (increment 3f): quoted literals
Emit a :quote node by reconstructing the raw reader form as RT constructor
calls: symbol -> jolt-symbol, list (array) -> jolt-list, vector (tuple) ->
jolt-vector, map -> jolt-hash-map, set -> jolt-hash-set, scalars via emit-const.
The runtime value of a quote is just that literal data (the interpreter returns
the reader form verbatim).

Quote exposed a latent seq.ss bug: empty map/filter results were jolt-nil, but
Clojure's (map f []) is an empty seq, so (= () (map f [])) must be true. Return
jolt-empty-list (which seqs back to nil, so it's still a valid lazy-tail
terminator) instead — matching jolt-take/drop/rest/list.

Prelude emit reach 334 -> 342/355. Subset probe 632 -> 664/664 compiled, 0
divergences (quote + the seq fix pull 32 corpus cases into the subset). emit-test
110/110 (added 16 quote cases). corpus.edn regenerated (the 3 malformed-catch
spec rows). Full gate green.
2026-06-17 17:43:34 -04:00
Yogthos
cb3cfaf0c2 Chez Phase 1 (increment 3b): seq tier + dynamic IFn dispatch on the Chez RT
Brings up the seq layer on the Chez runtime. host/chez/seq.ss adds one
lazy-capable node (cseq) that models Clojure's list, cons, and lazy seq -
all print as (...), all sequential-= to each other and to vectors. seq
coerces any seqable (vector/map/set/string/list/seq/nil) to a cseq or nil;
the empty seq is a distinct value printing () (rest of a 1-elem coll is ()
not nil, seq of empty is nil). Leaf ops: first/rest/next/seq/cons/list,
reverse/last, map/filter/remove/reduce/into, range/take/drop/concat/apply,
keys/vals, plus nth/peek/pop extended over seqs. map/filter/reduce apply
their fn arg through jolt-invoke, so a procedure, keyword, or collection all
work as the fn.

Dynamic IFn dispatch: a keyword/vector/coll held in a local (let binding or
fn param) and called as a fn now routes through the jolt-invoke fallback
(procedure? -> apply; keyword/coll -> lookup). The emitter only routes a
:local callee that isn't a known procedure - a named fn's self-recursion
name stays a direct call, so the fib hot path is untouched. Closes the 3
ex-known IFn divergences.

emit.janet: seq/pred ops added to native-ops with arity gates; value-position
clojure.core refs resolve to the RT procedure (native-ops names one for each),
with +/-/*// routed to flonum-coercing wrappers so higher-order arithmetic
((reduce + [])) keeps the all-double model. values.ss: cross-type sequential
=/hash so a vector and a list of the same elements are jolt= and hash alike.
rt.ss: printer learns seqs; top-level nil prints as the empty string (jolt -e
str-style). Fixed latent bug: (conj nil ...) now builds a list, not a vector.

Gates: emit-test 69/69 (fib/mandelbrot/collections/seq/IFn parity vs the jolt
oracle, fib(30) ~24ms unchanged). Subset probe 433/436 -> 595/595 compiled,
0 divergences (was 3 known), 2060/2655 out of subset. Full run-tests green
(125 files, conformance + suites included).
2026-06-17 15:19:18 -04:00