Commit graph

950 commits

Author SHA1 Message Date
Yogthos
10592fa746 drop (symbol var) corpus row — certify harness mis-evals (var ...); value is correct 2026-06-27 20:48:53 -04:00
Yogthos
48908f3a9b spec.alpha: (symbol var), Compiler/demunge, MultiFn .dispatchFn/.getMethod, fn .applyTo
General fixes from clojure.spec.alpha's test suite.

- (symbol a-var) returns the var's qualified symbol (clojure.spec.alpha/->sym).
- clojure.lang.Compiler/demunge reverses Clojure's name munging
  ("clojure.core$odd_QMARK_" -> clojure.core/odd?); spec's fn-sym uses it.
- clojure.lang.MultiFn .dispatchFn / .getMethod — spec's multi-spec walks a
  multimethod through them.
- (.applyTo f args) applies a fn to a seq of args (spec instrument).

Most of spec.alpha's conform/explain/describe suite passes. Remaining gaps:
explain-data's :pred for a BARE fn predicate (jolt fns don't carry their defining
symbol, so fn-sym can't recover it), #inst form rendering, and instrument — follow-up.

make test green (+3 corpus rows, 0 new divergences), runtime only (no re-mint).
2026-06-27 20:46:33 -04:00
Yogthos
4d61145e9c proxy [ThreadLocal] via thread-parameter; clojure.test/*testing-vars*
- (proxy [ThreadLocal] [] (initialValue [] body)) now builds a real per-thread
  store backed by a Chez thread-parameter, with a lazy initialValue; .get/.set/
  .remove work. Other proxies stay nil. test.check's no-seed PRNG (next-rng) uses
  one, so gen/sample and gen/generate (and everything built on them) now work.
- clojure.test/*testing-vars* (+ *report-counters*) are bound vars now, so a
  defspec run through its :test metadata / default reporter doesn't hit an unbound
  var.

make test green (+1 corpus row), shakesmoke byte-identical. One re-mint (proxy).
2026-06-27 19:51:49 -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
992fc0af34 *unchecked-math* on macro-emitted arithmetic + local shadowing a bare native op
Two general fixes shaken out by clojure.test.check's own suite (its splittable
PRNG mixes 64-bit longs and binds locals named min/max).

- *unchecked-math* now wraps arithmetic a macro emits. The analyzer rewrote a
  bare (+/-/*) to its wrapping unchecked-* under *unchecked-math*, but a macro's
  syntax-quote produces clojure.core/* (qualified), which was skipped — so e.g.
  test.check's mix-64 multiply grew to a bignum instead of a 64-bit long. The
  rewrite now also fires on the clojure.core-qualified form.
- A local binding named like a bare-emitted native op no longer shadows it. ops
  where native-ops maps the name to itself (+ - * / < > min max …) emit as the
  bare Scheme name; a local `max` emitted the same token, so
  (fn [max] (clojure.core/max …)) called the param. munge-name now prefixes such
  locals, like reserved words (derived from native-ops so they can't drift).

make test green (+1 corpus row, 0 new divergences), shakesmoke byte-identical.
One re-mint (analyzer + backend).
2026-06-27 18:19:14 -04:00
Dmitri Sotnikov
75652de1ad
Merge pull request #261 from jolt-lang/conformance/algo-monads
algo.monads: a seq reports IPersistentList for protocol dispatch
2026-06-27 21:43:15 +00:00
Yogthos
d38402eb57 algo.monads: a seq reports IPersistentList for protocol dispatch
algo.monads' writer monad extends a protocol to clojure.lang.IPersistentList,
but jolt's lists only reported ASeq/ISeq in value-host-tags, so writer-m-add
didn't dispatch ("No method writer-m-add"). jolt models every seq as a list (no
distinct LazySeq — (class (map inc xs)) is PersistentList), so a seq now also
reports PersistentList / IPersistentList / IPersistentStack, in value-host-tags
and host-type-set. extend-protocol clojure.lang.IPersistentList then dispatches
on a list.

algo.monads passes its whole suite (11/11) over tools.macro. Listed in docs +
site. Runtime only, no re-mint. make test green (+1 corpus row, 0 new
divergences), shakesmoke byte-identical.
2026-06-27 17:38:48 -04:00
Dmitri Sotnikov
65cf6ac3d4
Merge pull request #260 from jolt-lang/conformance/letfn-star
letfn is a macro over a letfn* special form (Clojure semantics)
2026-06-27 21:30:58 +00:00
Yogthos
21cd88deee letfn is a macro over a letfn* special form (Clojure semantics)
jolt modelled letfn as a special form directly, so (macroexpand-1 '(letfn …))
returned the form unchanged. Clojure's letfn is a macro that expands to letfn*,
and macroexpansion tooling (tools.macro, tools.analyzer) depends on that — its
special-form handlers key on letfn*, not letfn.

Split it the Clojure way:
- letfn* is now the special form (analyzer), taking flat name/fn-form pairs
  [name1 fn1 name2 fn2 …] — the letrec :let lowering is unchanged.
- letfn is a macro (00-syntax) turning each (name [params] body*) spec into a
  name + (fn name [params] body*) binding, so it expands to letfn*.

So (macroexpand-1 '(letfn [(f [x] x)] (f 1))) now yields
(letfn* [f (fn f [x] x)] (f 1)), and clojure.tools.macro passes its whole suite
(macrolet / symbol-macrolet / mexpand-all). Listed in docs + site.

make test green (+1 corpus row, 0 new divergences), shakesmoke byte-identical.
One re-mint (analyzer + the letfn macro); selfhost holds.
2026-06-27 17:26:18 -04:00
Dmitri Sotnikov
7891fa0d55
Merge pull request #259 from jolt-lang/conformance/ns-vector-clauses
ns: accept vector reference clauses; add Compiler/specials
2026-06-27 21:14:01 +00:00
Yogthos
192ef66e7e ns: accept vector reference clauses; add Compiler/specials
Two general fixes shaken out by clojure/tools.macro.

- The ns macro now accepts a vector reference clause [:require …] / [:use …],
  not just the list form (:require …). Clojure dispatches on (first clause) and
  accepts both; jolt silently dropped vector clauses, so a ns written with them
  loaded with nothing required/used (tools.macro's test ns uses [:use …]).
- clojure.lang.Compiler/specials is now a static whose keys are the special-form
  symbols (matching Clojure 1.2/1.3). Macroexpansion tooling reads
  (keys Compiler/specials) to know which heads not to expand.

tools.macro itself isn't fully passing yet — its mexpand-all works, but the
macrolet/symbol-macrolet tests need letfn to macroexpand to letfn* (jolt models
letfn as a special form, not a macro over letfn*), so it stays off the list.

make test green (+1 corpus row, 0 new divergences), shakesmoke byte-identical.
One re-mint (the ns macro).
2026-06-27 17:08:50 -04:00
Dmitri Sotnikov
1ba79aa223
Merge pull request #258 from jolt-lang/conformance/data-priority-map
data.priority-map: deftype interop fixes (rseq, arity-overload, empty, Sorted)
2026-06-27 20:52:54 +00:00
Yogthos
75f6bc79d1 data.priority-map: deftype interop fixes (rseq, arity-overload, empty, Sorted)
data.priority-map's whole suite passes (4/4). It leans on deftype/collection
interop jolt got wrong; four general fixes:

- rseq dispatches to a deftype's clojure.lang.Reversible.rseq method instead of
  always demanding a vector/sorted-coll (natives-seq.ss).
- a deftype method declared at two arities from two interfaces now dispatches by
  arity: the priority-map has seq[this] (Seqable) and seq[this ascending]
  (Sorted), so (.seq pm false) must reach the 2-arg one. find-method-any-protocol
  now matches the call's arg count via procedure-arity-mask, and a deftype's own
  declared method wins over the generic collection interop in dot-forms.
- (empty x) on a deftype/record with its own empty method uses it rather than
  returning {} (jolt.host/jrec-method? gate in clojure.core/empty).
- clojure.lang.Sorted (comparator / entryKey / seqFrom) works on jolt's
  sorted-map/set, so subseq/rsubseq run — including the priority-map delegating
  .comparator to its backing sorted-map (dot-forms.ss + host-static.ss).

Listed in docs/libraries.md + the site. One re-mint (clojure.core/empty);
everything else runtime. make test green (0 new divergences), shakesmoke
byte-identical.
2026-06-27 16:48:14 -04:00
Dmitri Sotnikov
8a4df7b204
Merge pull request #257 from jolt-lang/numeric/unchecked-math-wrap
Long compatibility: *unchecked-math* wrapping + ^long is 64-bit (unblocks test.check)
2026-06-27 20:10:07 +00: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
Dmitri Sotnikov
86dd9650b6
Merge pull request #256 from jolt-lang/conformance/tools-reader
conformance: general fixes from tools.reader, core.contracts, data.zip, data.csv (+ math.combinatorics)
2026-06-27 19:06:19 +00:00
Yogthos
44837f01ab data.csv: fully passes, three general fixes
clojure.data.csv runs its whole suite on jolt (4/4 reading/writing/eof/line-
endings). Three general gaps fixed, all runtime, no re-mint, JVM-certified:

- The prefix-list form of :require/:use — (:require (clojure [string :as str]))
  means clojure.string :as str — now expands (loader.ss). It silently failed
  before, trying to load a "clojure" namespace.
- extend-protocol to java.io.Reader / Writer / StringReader / PushbackReader now
  dispatches: those reader/writer host tags carry the right class names in
  value-host-tags AND are in host-type-set, so extend-protocol registers under
  the canonical tag instead of a local ns tag (records.ss). data.csv's
  Read-CSV-From protocol extends to String / Reader / PushbackReader.
- (str StringWriter) returns its accumulated content (register-str-render for the
  "writer" jhost), not the opaque host object — data.csv writes CSV to one and
  reads it back.

Listed in docs/libraries.md + the site.

make test green (+2 corpus rows, 0 new divergences), shakesmoke byte-identical.
2026-06-27 15:02:32 -04:00
Yogthos
745d22260f data.zip: add clojure.zip/xml-zip; clojure.xml lives in jolt-lang/xml
clojure.zip was missing xml-zip — a zipper over xml {:tag :content} elements,
which clojure.data.zip and any xml-zipper code needs. Added (runtime, loaded on
require). clojure.data.zip's whole xml suite (9/9) then passes, once XML parsing
is provided: clojure.xml/parse now ships in jolt-lang/xml over its
javax.xml.stream pull parser (committed there).

Listed in docs/libraries.md + the site.
2026-06-27 14:49:49 -04:00
Yogthos
a83ff6ce40 core.contracts: fully passes, two general fixes
clojure.core.contracts (over core.unify) now runs its whole suite on jolt —
14/14 across contracts/constraints/with-constraints/provide tests. Two general
gaps fixed:

- Symbol and Keyword now report IFn (and Fn/Runnable/Callable) in the modeled
  class hierarchy, so a (class x)-dispatched multimethod with an IFn method
  matches a symbol or keyword, like the JVM (both implement IFn — they're
  callable). core.contracts' funcify* dispatches on (class constraint) and a
  bare predicate symbol must hit the IFn arm. Runtime, no re-mint.
- A live Var value spliced into a form by a macro (defcurry-from resolves a var
  and emits (~v l r)) now compiles: analyze treats a var-cell form as a
  :the-var reference by ns+name, the same node as (var ns/name), mirroring the
  existing spliced-namespace (~*ns*) case. analyzer.clj + host-contract.ss,
  re-mint (prelude stays byte-identical; only the analyzer image changes).

Listed in docs/libraries.md + the site.

make test green (+2 corpus rows, 0 new divergences), shakesmoke byte-identical.
2026-06-27 14:32:57 -04:00
Yogthos
2c5b7dd918 libraries: add math.combinatorics
Its full suite (18 deftests) passes on jolt unchanged — pure Clojure over
seqs, no host interop.
2026-06-27 14:18:05 -04:00
Yogthos
e16085402b General fixes shaken out by clojure/tools.reader
Running clojure.tools.reader's own suite on jolt surfaced a batch of general
gaps (all runtime, JVM-certified, no re-mint — reader.ss is loaded at runtime
and jolt-core has no octal literals, so selfhost holds):

Reader:
- (load "rel") resolves a non-/ path against the current namespace's directory,
  like Clojure — (load "common_tests") from clojure.tools.reader-test loads
  clojure/tools/common_tests.clj. Was resolved against the roots directly.
- Octal integer literals: 042 reads as 34, not decimal 42; octal string escapes
  (\377 is one char, not \0 + "00"). \oNNN char octal already worked.
- (symbol nil name) now equals (symbol name) and the reader literal — a nil
  namespace is the #f no-ns sentinel, not jolt-nil (jolt= compares ns by equal?).

clojure.test:
- thrown-with-msg? honors the class hierarchy (instance?) before falling back to
  a simple-name match, so (thrown-with-msg? RuntimeException ...) matches an
  ExceptionInfo, like thrown? already did.

Host interop (java layer):
- java.util.regex: Pattern.matcher / Matcher.matches / .group / .groupCount /
  .find, and Pattern/compile.
- clojure.lang: RT/map, PersistentList/create, PersistentHashSet/createWithCheck.
- java.lang.Character: digit / isDigit / isWhitespace / valueOf.
- java.util.LinkedList (Deque surface over the ArrayList backing); ArrayList /
  LinkedList are now seqable.
- BigInteger 2-arg ctor (string, radix) + .negate / .bitLength / .signum / .abs;
  BigInt/fromBigInteger and Numbers/reduceBigInt (identity on jolt's exact ints).

Suite: reader_test 22/30, reader-edn_test 13/16. The remaining failures are
fundamental numeric-model differences (no BigDecimal type; BigInt and Long are
one exact-integer type) or need JVM reflection (record/ctor tagged literals via
getConstructors) — out of scope.

make test green (+8 corpus rows, 0 new divergences), shakesmoke byte-identical.
2026-06-27 14:11:02 -04:00
Dmitri Sotnikov
850a84c272
Merge pull request #255 from jolt-lang/conformance/core-typed
core.typed runtime contracts: instance? Object + Compiler/LINE
2026-06-27 17:37:18 +00:00
Yogthos
720734a481 Two general fixes shaken out by core.typed's runtime contract suite
Running clojure.core.typed's runtime contract tests (typed/runtime.jvm,
test_contract — 5/5 pass) surfaced two general jolt gaps, both runtime, both
JVM-certified:

- instance? Object / java.lang.Object returned false for everything. Object is
  the root of the type hierarchy: every non-nil value is an instance of Object,
  nil is not. core.typed's (instance-c Object) contract depends on this; many
  libraries do.
- @Compiler/LINE and @Compiler/COLUMN (clojure.lang.Compiler statics — Vars on
  the JVM holding the line/column of the form being compiled) were unresolved.
  Macros read @Compiler/LINE as a fallback when &form carries no position. Now
  backed by derefable cells updated per top-level form, like *current-source*.

The core.typed type checker itself (tools.analyzer.jvm + ASM bytecode +
clojure.lang.Compiler internals) and the cljs runtime are not portable, so the
checker/check-ns surface is out of scope; this is the runtime contract layer.

make test green (+4 corpus rows, 0 new divergences), shakesmoke byte-identical.
2026-06-27 13:33:30 -04:00
Dmitri Sotnikov
9805620997
Merge pull request #254 from jolt-lang/license/epl-2.0
Relicense under EPL-2.0
2026-06-27 17:21:02 +00:00
Yogthos
5c99ff8c79 Relicense under EPL-2.0
EPL-1.0 is superseded by EPL-2.0 (clearer jurisdiction/patent terms). Updates
the LICENSE file and README. Clojure-derived files under stdlib/ and the
vendored sci keep their original EPL-1.0 headers per the weak-copyleft terms.

Closes #206
2026-06-27 13:17:19 -04:00
Dmitri Sotnikov
c479010536
Merge pull request #253 from jolt-lang/conformance/core-async
core.async: higher-level API over native channels + general fixes
2026-06-27 17:09:20 +00: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
Dmitri Sotnikov
4007af8d6a
Merge pull request #252 from jolt-lang/conformance/core-logic-fd
core.logic fd + &env: instance? boundary, ~ unquote, macro &form/&env
2026-06-27 16:06:04 +00:00
Yogthos
438742702a Macros receive &form and &env
A macro body can now read &form (the call form) and &env (a map of the in-scope
local symbols), like Clojure. This is what core.logic's matche/defne use to tell
a pattern symbol that names an enclosing local from a fresh pattern var — so
locals-membero and the recursive checko in `matches` now compute correctly. The
suite reaches 535/2/0 (the last two are constraint reification ORDER, where the
constraint set is right but it is spliced from a set whose iteration order differs
from the JVM — a host set-ordering divergence, not a bug).

&form/&env are clojure.core dynamic vars bound around each expander call rather
than prepended params, so the macro calling convention is unchanged and the mint
stays consistent (the seed prelude is byte-identical; only the analyzer carries
the env into form-expand-1). macroexpand-1 passes an empty env.

corpus.edn: the ~@ unquote row is now a boolean compare (a bare clojure.core/
unquote-splicing symbol evaluates to an unbound var, not the symbol).
2026-06-27 11:54:47 -04:00
Yogthos
f46772d576 fd subsystem: instance? name-boundary + ~ reads as clojure.core/unquote
Two general fixes that clear core.logic's finite-domain -difference, safefd, and
the defne quoted-list patterns (form->ast), taking the suite to 532/5/0.

- instance? on a deftype matched a simple type name against the qualified tag by
  raw string suffix, so "a.b.MultiIntervalFD" tested true for IntervalFD. The
  suffix must land on a "." boundary. core.logic's fd dispatches on
  (interval? x) = (instance? IntervalFD x), and a MultiIntervalFD wrongly counted
  as an interval, so -difference/safefd computed the wrong set.
- the reader reads ~ / ~@ as clojure.core/unquote(-splicing), like the JVM reader,
  instead of a bare unquote. Code that inspects quoted pattern/template data —
  core.logic's defne checks (= f 'clojure.core/unquote) — now sees the symbol it
  expects, so '(fn ~args . ~body) patterns compile. hc-head-is? accepts the
  qualified head in syntax-quote lowering; the value-preserving change leaves the
  minted seed byte-identical.

corpus.edn: 2 JVM-certified unquote rows. unit.edn: two reader rows updated to the
qualified unquote. make test + shakesmoke green, 0 new divergences, self-host holds.
2026-06-27 11:22:01 -04:00
Dmitri Sotnikov
42c163bacb
Merge pull request #251 from jolt-lang/conformance/core-logic-clp
core.logic constraint layer: CLP/unifier fixes
2026-06-27 15:07:32 +00:00
Yogthos
580e3a1407 A defrecord exposes its clojure.lang interfaces for protocol dispatch
value-host-tags returned only ("Object") for a record, so a protocol extended to
clojure.lang.IRecord / IPersistentMap / Associative / Seqable / … (and not to the
record's own type) dispatched to the Object default. core.logic extends IWalkTerm
to IRecord, and walking a record value hit Object's (walk-term [v f] (f v)) — which
re-enters walk* and loops forever (the test-53-lossy-records hang).

A defrecord now carries the map/record interface tags it genuinely satisfies. The
record's own type is still tried first (jrec-tag, before these tags), so a direct
extend to the record type wins, and record equality / map? / record? are unchanged.
A bare deftype stays opaque (its tag + Object; declared interfaces dispatch via its
inline methods). Runtime only, no re-mint.
2026-06-27 10:57:37 -04:00
Yogthos
5411db3729 Track :refer-clojure :exclude so syntax-quote qualifies an excluded name to the current ns
A name a namespace excludes from clojure.core (:refer-clojure :exclude) is not
clojure.core/name even before the ns defines its own — syntax-quote must qualify
it to the current ns, like Clojure. refer-clojure was a no-op, so a syntax-quoted
excluded name (core.logic.fd's `==`, referenced by a constraint's -rator before fd
defines ==) resolved to clojure.core/==.

jolt-refer-clojure now records the :exclude set per ns; hc-sq-symbol consults it
before falling back to clojure.core. Fixes core.logic's fd constraint -rator names.
Runtime only, no re-mint.
2026-06-27 10:46:52 -04:00
Yogthos
e6aa2aace7 core.logic constraint layer: fixes for the CLP/unifier failures
Follow-on to the core.logic relational-engine work. These clear every crash in
core.logic's constraint-logic-programming and unifier layers (33 errors -> 0) and
most of the value mismatches; the suite goes 504 -> 523 passing assertions. All
are general gaps, not core.logic-specific.

- symbols intern their ns/name strings (JVM Symbol.intern .intern()s them): two
  separately-read `?a` symbols now share one name-string object. core.logic's
  non-unique lvars compare names by identity (via (str sym)), so without this a
  term's lvar and a constraint's lvar built from different `?a` reads never matched
  and constraints silently never fired.
- (str x) of a single arg returns its rendering directly instead of copying through
  string-append, and a symbol stringifies to its (interned) name — JVM (str x) is
  x.toString(). Needed for the identity comparison above.
- a clojure.core-qualified special form dispatches correctly: syntax-quote
  namespace-qualifies a macro like letfn to clojure.core/letfn (matching Clojure,
  where it's a macro), and the analyzer now maps that back to the special form
  instead of treating it as an invoke of a nil var. core.logic's fnc/defnc emit
  (clojure.core/letfn ...). Re-mint.
- (disj nil ...) is nil (JVM), instead of crashing in the set path — core.logic's
  constraint store does (disj (get km v) id) where the get can be nil.

corpus.edn: 4 JVM-certified rows. make test + shakesmoke green, 0 new divergences,
self-host fixpoint holds.
2026-06-27 10:37:32 -04:00
Dmitri Sotnikov
36105ba702
Merge pull request #250 from jolt-lang/conformance/core-logic
General fixes shaken out by running core.logic
2026-06-27 14:00:15 +00:00
Yogthos
9dbfd7e5c1 General fixes shaken out by running core.logic's test suite
Running clojure/core.logic's own suite surfaced a batch of general jolt gaps.
None are core.logic-specific; each is a language/host behavior that was wrong or
missing. With these, the core relational engine (unify, run/fresh/conde,
conso/membero/appendo, reification to _0/_1, lcons) runs; the remaining failures
are in core.logic's constraint-logic-programming and finite-domain layers
(tracked separately).

- analyzer: accept the list-member dot form (. target (method args)), sugar for
  (. target method args). Re-mint.
- identical? is reference identity (eq?), not value equality. It was aliased to =,
  which infinite-loops when a deftype's .equals short-circuits on (identical? this o)
  (core.logic's Substitutions) and is wrong for distinct equal collections.
- jrecs use a deftype's declared hashCode/equals/equiv for map/set keying instead
  of structural field comparison, so metadata-wrapped keys still match (core.logic
  keys substitutions on lvar id, ignoring metadata).
- meta/with-meta dispatch to a deftype's clojure.lang.IObj meta/withMeta methods
  when present, so metadata threaded through the type's own assoc/withMeta survives
  (previously kept in an identity side-table the reconstructed instances didn't share).
- coll?/seqable? on a deftype require IPersistentCollection (cons) or ISeq (first);
  ILookup(valAt)/Indexed(nth)/Counted(count)/Seqable(seq) alone no longer qualify,
  matching the JVM.
- syntax-quote resolves a bare symbol to the compile ns's own def before
  clojure.core, so a name the ns excluded and redefined (core.logic's == after
  :refer-clojure :exclude) qualifies correctly in macro output.
- reader: record literals #ns.Type{...} / #ns.Type[...] expand to the map->/->
  factory call.
- structmap API: defstruct/create-struct/struct-map/struct/accessor (map-backed,
  insertion-ordered). Re-mint.
- .hashCode on strings/symbols (Java String.hashCode, Symbol Util.hashCombine);
  Class.isInstance; java.util.Collection.contains over vector/list/set;
  clojure.lang.RT/nextID and clojure.lang.Util hash/hasheq/equiv/identical statics.

corpus.edn: 8 JVM-certified rows. unit.edn: a Counted+Seqable deftype is coll?=false
(was a stale expectation encoding the old behavior).
2026-06-27 09:20:11 -04:00
Dmitri Sotnikov
af91dbbaa6
Merge pull request #249 from jolt-lang/conformance/map-insertion-order
Small maps preserve insertion order
2026-06-27 12:14:26 +00:00
Yogthos
bfa2cbf49d Small maps preserve insertion order
jolt maps were HAMTs with hash iteration order; Clojure keeps small maps as
PersistentArrayMap (insertion order), converting to PersistentHashMap past a
threshold. Map literals, array-map, assoc, into/transient, merge, zipmap,
select-keys, update-keys/vals, frequencies and group-by now iterate in insertion
order for <=8 entries, matching the JVM. hash-map and >8-entry maps stay hash
order; sets stay hash order.

The pmap record gains an order field (the insertion-order key list, or #f once
hashed); the HAMT still backs the values so equality/hash/lookup are unchanged.
pmap-fold visits an array-mode map last-to-first so the runtime's cons-accumulate
idiom reconstructs insertion order without touching its many call sites, and
hash-mode output stays byte-identical; pmap-fold-fwd visits in order for the few
sites that build a value directly. Transient maps track insertion order and
promote to hash past max(8, source-count), matching TransientArrayMap.

The hash-map native-op retargets to a hash-order builder so (hash-map ...) stays
hash-ordered while {...} literals are ordered; syntax-quote builds maps via the
hash builder (Clojure expands `{...} to apply hash-map). The core overlay map
builders seed from {} instead of (hash-map) to keep order.

Threshold is 8 for any key (the keyword exception in newer Clojure isn't in
1.12.5). honeysql now passes 832/0/0; 19 JVM-certified corpus rows added.
2026-06-27 05:48:17 -04:00
Dmitri Sotnikov
e2efff6c8e
Merge pull request #248 from jolt-lang/conformance/reitit-aero-honeysql
Conformance: reitit 327/0/0, aero 59/0/0, honeysql 638/6
2026-06-27 08:57:08 +00:00
Yogthos
a99991a818 defn- marks :private; ns-publics drops private vars
defn- now adds :private to the var metadata (like Clojure), and ns-publics
filters those out while ns-interns/ns-map keep them — they were all the same
unfiltered scan before. A lib that introspects ns-publics (honeysql asserts
every public helper has a docstring, and that the clause set matches the public
helpers) saw the private defn- helpers and failed; now honeysql 636/8 -> 638/6
(the rest are map key-order).
2026-06-27 01:27:47 -04:00
Yogthos
4df3d0fa34 Add a java.util.Locale shim (no-op default locale)
jolt's case ops are codepoint-based and locale-independent, so the default
locale is a no-op token: getDefault/setDefault/forLanguageTag + ROOT/US/ENGLISH.
honeysql sets and restores the locale around formatting to assert output is
locale-stable (its Turkish-İ regression guard) — that test errored on the
missing Locale/setDefault static, now passes (honeysql 635/8/1 -> 636/8/0).
2026-06-27 01:21:42 -04:00
Yogthos
135bad9d3a edn: read raw forms so a #tag goes through :readers/:default
clojure.edn/read built the built-in #inst/#uuid eagerly (via read-string), so a
:readers override couldn't win and #inst applied to a non-string form (aero's
#inst ^:ref […]) threw. Read the raw form instead and let edn->value route every
tag through :readers then :default then the built-in — matching clojure.edn,
where a reader from opts wins. edn->value now also converts the (recursively
converted) metadata, since the raw path skips the read-string data seam. aero
suite: 59/0/0 (full pass). clojure.edn baked, re-minted.
2026-06-27 01:15:33 -04:00
Yogthos
3491312ca1 with-meta on a list/seq returns a fresh copy, not the original
meta-copy keyed metadata on the SAME cseq/lazyseq object (the else branch), so
(with-meta xs m) mutated the original list in place — Clojure's PersistentList
is immutable and withMeta returns a new list. (with-meta xs {:k xs}) thus built
a self-referential cycle (the list's metadata pointed at the list), which looped
*print-meta* printing forever — the root of aero's meta-preservation hang. Now
copies the cseq/lazyseq node like the other collections. aero suite completes:
58/0/1 (was hanging).
2026-06-27 01:10:34 -04:00
Yogthos
afc733a439 edn: apply tag readers inside a set literal
clojure.edn/read with :readers/:default recursed into vectors/maps/seqs but
not a constructed set, so a tagged literal in #{…} (aero's #ref in a set) kept
its raw form. edn->value now recurses into a set. clojure.edn is baked into the
seed, re-minted. Fixes aero #ref-in-set + falsey-user-return.
2026-06-27 00:07:49 -04:00
Yogthos
eb64240e29 Read metadata as data, consistently (sets, empty lists)
Clojure's reader reads a ^{…} map with the same read() as any value, so a set/
tagged literal in metadata is a value, not a form. jolt's data seam converted a
set-form to a set in the VALUE but left it as the tagged form inside the
METADATA, and dropped metadata on an empty list entirely (a wrong 'interned, =
Clojure' special case in rdr-attach-meta — Clojure's MetaReader withMetas () via
IObj). rdr-form->data now always converts + carries the (recursively converted)
metadata, whether or not the value structure changed; rdr-attach-meta no longer
skips (). Fixes aero's meta-preservation (set/map/vector/empty-list ds round-
trip). All runtime .ss (data seam), no re-mint.
2026-06-26 23:52:22 -04:00
Yogthos
6b99591266 Fix [_ _] inline method field binding + Var protocol dispatch
Two gaps reitit-core surfaced (now 322/0/1 -> 327/0/0):

- A deftype/defrecord inline method with two _ params, (m [_ _] field), read
  the field as nil: mk-clause bound fields off (get _ :field) where _ was the
  first param, but the second _ shadowed it. Each _ param is now renamed to a
  fresh symbol so the instance is unambiguous.

- A var did not dispatch to a protocol's clojure.lang.Var extension (reitit
  extends Expand to Var for a #'handler route): value-host-tags gained a var arm
  (Var/clojure.lang.Var/IDeref/IFn) and host-type-set gained Var/IDeref so the
  extension keys under Var.

deftype/defrecord is a seed source, re-minted.
2026-06-26 23:22:22 -04:00
Dmitri Sotnikov
9404512b97
Merge pull request #247 from jolt-lang/conformance/lib-fixes
More conformance fixes: defn attr-map, set-in-macro, record interop, Byte/Short
2026-06-27 03:09:09 +00:00
Yogthos
2fd9763d94 Add java.lang.Byte / Short / Float class tokens + Byte/Short statics
jolt had Long/Integer/Double class tokens but not Byte/Short/Float, and no
Byte/Short MIN_VALUE/MAX_VALUE/valueOf/parse* statics. clojure.test.check (a
malli dependency) references Byte/MIN_VALUE and Byte/MAX_VALUE. The values are
plain integers on jolt; the statics expose the JVM ranges (127/-128, 32767/
-32768).
2026-06-26 23:04:55 -04:00
Yogthos
ed1ea46ca2 Records delegate their clojure.lang interface methods to the map fns
A defrecord is Associative/ILookup/IPersistentMap/Seqable/Counted on the JVM,
so (.assoc r k v) / (.valAt r k) / (.without r k) / (.containsKey r k) /
(.cons r x) / (.count r) / (.seq r) / (.equiv r o) / (.entryAt r k) now work
via Java interop, delegating to the map fns when not overridden by a declared
method. reitit's impl calls (.assoc match k v) directly. A bare deftype uses
its own declared methods (record-only branch). reitit-core 58/1/18 -> 321/1/1
with the router lib's Trie shim.
2026-06-26 22:49:51 -04:00