Commit graph

26 commits

Author SHA1 Message Date
Yogthos
b82477dac3 feat(strictness): peek/pop/vec/key/val reject malformed args
- peek/pop are stack-only (vectors/lists): throw on sets/maps/strings/scalars,
  and pop throws on an empty vector/list
- vec throws on non-seqable args (numbers/keywords/transients)
- key/val require a map entry (2-element vector); throw on nil/numbers/maps/sets

pop clean; peek 9/2; vec 17/2/1; key/val 12/2/1 (remaining are the
2-vector-vs-MapEntry / tuple-from-seq divergences). pass 3824->3840.
spec: seq/accessor-strictness (16). jpm test green.
2026-06-05 13:37:27 -04:00
Yogthos
e7a58a0ed6 test: fix transient/strictness spec case (use a set, not a length-2 list) 2026-06-05 13:32:12 -04:00
Yogthos
a4a48a8520 feat(strictness): transient bang ops require a transient; conj! arities
conj!/assoc!/dissoc!/disj!/pop!/persistent! now throw on a non-transient (or
wrong transient kind) instead of falling back to the persistent op, matching
Clojure. conj! keeps its special arities: (conj!) -> (transient []), (conj! coll)
-> coll. conj! onto a transient map accepts a [k v] pair or a map (merge), and
throws on a list/set/seq.

pop_bang/dissoc_bang clean; conj_bang 13/1/22->47/3/1; persistent_bang 9/8->16/1.
clojure-test-suite pass 3781->3824. spec: transient/strictness (10). jpm test green.
2026-06-05 11:09:38 -04:00
Yogthos
fb36577ee7 feat(strictness): num/cons/realized?/symbol/keyword reject malformed args
- num throws on non-numbers (no longer coerces chars)
- cons throws when the second arg isn't seqable (a number/keyword/boolean/fn)
- realized? throws on non-IPending values (only delays/promises/lazy-seqs)
- symbol: 1-arg accepts string/symbol/keyword (->symbol), throws otherwise;
  2-arg requires string ns (nil ok) and string name
- keyword: (keyword nil) is nil, 1-arg accepts string/symbol/keyword, 2-arg
  requires string name and nil-or-string ns

keyword file clean; symbol 60/0/1; cons 18/1/1; realized? 25/1/0.
clojure-test-suite pass 3738->3781. spec: seq/strictness (13). jpm test green.
2026-06-05 11:04:06 -04:00
Yogthos
07d5b43fbb feat(strictness): numeric ops reject non-numbers/non-integers like Clojure
- zero?/pos?/neg? throw on non-numbers; odd?/even? throw on non-integers
  (nil, infinities, NaN, fractional) via need-num/need-int helpers
- comparisons < > <= >= throw on non-number args (1-arity stays true, no check)
- max/min throw on non-number args
- quot/rem/mod throw on zero divisor and non-finite operands

odd?/even?/lt/gt/lt_eq/gt_eq suite files now clean; pass 3691->3738.
Updated systematic-coverage-test (zero? nil now throws). spec:
numbers/strictness (15). jpm test green.
2026-06-05 10:58:21 -04:00
Yogthos
66c8c1157b fix: conj 0-arg, conj onto nil (builds list), conj map into map
- (conj) -> []
- (conj nil x ...) builds a list (prepends): (conj nil 1 2) -> (2 1)
- conj a map value into a map/phm merges its entries ((conj {:a 0} {:b 1}) ->
  {:a 0 :b 1}); a [k v] vector/pair still adds one entry.

conj.cljc 14/6/5 -> 21/3/1. clojure-test-suite pass 3681->3691, errors 105->98.
spec: seq/conj-edge-cases (8). jpm test green.
2026-06-05 10:30:07 -04:00
Yogthos
a4d9d5f70b fix: print Infinity/-Infinity/NaN like Clojure (str and pr-str)
Numbers printed via Janet's (string v) rendered infinities/NaN as inf/-inf/nan.
Add fmt-number so str/pr-str (and collection rendering) emit Infinity/-Infinity/
NaN. str.cljc 3->32 pass. Remaining str fails are the integer-valued-double
divergence ((str 0.0) is "0" not "0.0" since 0.0 == 0 in Janet).

spec: numbers/printing-of-inf-&-nan (5). jpm test green.
2026-06-05 10:23:57 -04:00
Yogthos
bcdace7543 fix: case composite constants, associative?/reversible?, update non-fn args, nth nil
- case: quote list literals (read as arrays) in constant position so a wrapped
  list ((a b c)) matches by value instead of being evaluated as a call; symbol
  constants already quoted. Vector/map/set constants already worked. case errors
  in the suite drop to 0 (60 pass).
- associative?: true only for vectors (pvec) and maps (phm/struct/sorted-map),
  not lists/tuples-from-seq-fns/lazy-seqs/sets.
- reversible?: true for vectors and sorted-map/sorted-set only.
- update: coerce f via as-fn so (update m k :kw)/(update m k a-set) work; extra
  args already handled.
- nth: (nth nil i)/(nth nil i default) returns nil/default instead of throwing.

clojure-test-suite pass 3649->3678, errors 122->105, clean files 44->46.
associative?/reversible? files now fully clean. spec: predicates + control/case.
jpm test green.
2026-06-05 10:19:43 -04:00
Yogthos
2ccfa675f7 fix: keyword/set/map as IFn in higher-order fns; nil-seq; take-nth/interpose/sort-by/min-key arities
Biggest fix: jolt keywords are Janet keywords and maps are Janet structs/phm, but
(:a struct) at the Janet level returns nil (not a Clojure accessor) and errors on
a phm — so core-map/filter/sort-by/group-by/etc. calling (f x) directly broke the
ubiquitous keyword/set/map-as-function idioms ((map :a coll), (sort-by :k coll),
(filter a-set coll), (group-by :type coll)). Added as-fn coercion (keyword/symbol
-> key lookup, map -> key lookup, set -> membership) applied at the entry of
map/filter/remove/keep/mapv/filterv/sort-by/group-by/partition-by/some/
not-any?/not-every?/take-while/drop-while/min-key/max-key.

Also:
- realize-for-iteration treats nil as an empty seq (Clojure semantics), fixing
  nth/nthrest/nthnext/take-last/reduce/doseq over nil.
- take-nth and interpose gained their 1-arg transducer arities.
- sort-by gained the 3-arg (keyfn comparator coll) form.
- min-key/max-key: single item returns it without calling f; ties keep the last.
- underive gained the 2-arg global-hierarchy form.

clojure-test-suite pass 3535->3649, errors 177->122, clean files 39->44.
spec: seq/IFn-values-as-functions (11). jpm test green.
2026-06-05 10:10:22 -04:00
Yogthos
acfcf2f94b feat: read N/M/ratio/radix/exponent number literals; clean suite measurement
Reader gaps caused the clojure-test-suite worker to crash whole deftests on
literals it could not parse (0N, 1.5M, 2r1010, 1/2), losing every assertion in
the file. read-number now handles:
- N (bigint) / M (bigdec) suffixes -> plain number (Jolt has no bignum/bigdec)
- ratios a/b -> double quotient
- radix integers NrDDD (2r1010, 16rFF, 36rZ) parsed by base
- exponents (1e3, 1.5e-2) and 0X hex

Also fixed suite measurement: when-var-exists now skips silently (its SKIP
print to stdout was corrupting the worker's count line, dropping whole files),
and the worker emits counts on an @@COUNTS sentinel line (robust against test
bodies that print, e.g. with-out-str). Runner parses the sentinel; deftest
crashes now report the underlying message.

Impact: clojure-test-suite 210->231 files run, pass 1955->3535, clean files
24->39. Baseline raised to 3450/38.

spec: numbers/literal-syntax (13 cases). jpm test green.
2026-06-05 09:54:14 -04:00
Yogthos
03652dce5d feat: ##Inf/##-Inf/##NaN literals, infinite?/NaN?, fix intval? for infinity
The reader now reads the symbolic float values ##Inf, ##-Inf and ##NaN. Added
infinite? and NaN? predicates. Fixed intval? to exclude infinity (floor(inf)=inf
but inf isn't integer-valued), so float?/double? are true for ##Inf and
int?/pos-int?/nat-int?/neg-int? are false for it.

This unblocked many number-test files whose  forms previously failed to
READ (##Inf/##NaN literals), so clojure-test-suite jumped from 2241 to 2539
assertions and pass 1719 -> 1955. Baseline raised to 1900. NaN_qmark now runs.

float?/double? on integer-valued doubles (1.0) remain false: Janet represents
an integer and an integer-valued double identically, so they're inherently
indistinguishable — documented in the README Numbers section.

spec: numbers/floats-&-symbolic-values (15 cases). jpm test green.
Closes jolt-fy8 (fixable parts; int-vs-float ambiguity is a documented divergence).
2026-06-05 09:35:44 -04:00
Yogthos
09532dac05 fix: transient invokable lookup, assoc! odd args, use-after-persistent! invalidation
Real transient correctness gaps surfaced by the clojure-test-suite:

- Transients are now invokable for read-only lookup like their persistent forms:
  ((transient v) i), ((transient m) k [default]), (:k (transient m)),
  ((transient s) x). jolt-invoke/coll-lookup gained a transient branch
  (transient-lookup) that indexes :arr / canon-keyed :tbl. Added phm/canon as a
  public canonicalizer so collection keys compare by value here too.
- assoc! accepts an ODD arg count (a missing final value is nil), unlike assoc —
  core-assoc! now uses nil-safe get for the value instead of erroring.
- Using a transient after persistent! (or a second persistent!, or pop! on an
  empty transient vector) now throws, via a :jolt/persistent invalidation flag
  checked by the mutating ops. Catches the classic transient footgun.

spec: transients-spec gains invokable-lookup (7), assoc!-odd-args (4),
invalidation (4). clojure-test-suite pass 1704->1719.

Remaining transient fails are accepted lenient divergences (jolt doesn't throw
on bad-shape conj!/assoc!/pop! of wrong-typed args; nil set elements). jpm test green.
2026-06-05 09:29:36 -04:00
Yogthos
e75771084e fix: merge nil/empty + conj semantics to match Clojure
core-merge returned {} for (merge) and errored on nil args. Rewrite to follow
Clojure: (when (some identity maps) (reduce conj (or (first maps) {}) (rest maps))).

- (merge) and (merge nil nil) -> nil
- nil args elsewhere are no-ops: (merge {} nil) -> {}, (merge nil {:a 1}) -> {:a 1}
- later args follow conj semantics: a map merges its entries, a [k v]
  vector/map-entry adds that entry ((merge {} [:foo 1]) -> {:foo 1},
  (merge {} (first {:a 1})) -> {:a 1})
- collection keys preserved (first arg's phm type kept; entries via core-assoc
  which promotes struct->phm for collection keys)

spec: 9 new merge cases in maps-spec. merge.cljc 13/18/11 -> 29/11/2.
clojure-test-suite pass 1688->1704. jpm test green.

Remaining merge.cljc fails are the lenient-where-Clojure-throws class (atomic
args, >2-element vectors). Fixes jolt-dxx.
2026-06-05 09:22:27 -04:00
Yogthos
2885650ef6 fix: transducers/reduce short-circuit over infinite seqs via reduced
transduce-reduce and core-reduce both called realize-for-iteration, fully
realizing the coll before the reduce loop — so an infinite lazy seq hung before
the reduced short-circuit could fire. (into [] (take 5) (range)) etc. never
terminated.

Add reduce-with-reduced, which steps a lazy seq one cell at a time
(realize-ls/ls cell protocol), checking reduced? after each element so a take/
take-while transducer (or any reducing fn returning reduced) terminates over an
infinite seq. Route transduce-reduce and both core-reduce arities through it;
2-arg reduce now seeds from the first element and reduces the rest lazily.

spec: transducers/short-circuit + reduce/honors-reduced (13 cases).
clojure-test-suite timeouts 7->6, pass 1683->1688. jpm test green.

Fixes jolt-kxb.
2026-06-05 09:18:35 -04:00
Yogthos
f38d402445 feat: real transients backed by Janet arrays/tables (interop)
Replace the correctness-only transient aliases with real mutable scratch
collections via host interop:
- transient vector -> a Janet array; conj!/assoc!/pop! mutate in place
- transient map -> a Janet table keyed by canonical key (collection keys still
  compare by value); assoc!/dissoc!/conj! mutate in place
- transient set -> a Janet table; conj!/disj! mutate in place
- persistent! freezes back to a pvec / phm / phs
- count/nth/get/contains? work on transients; transient? predicate added

Building a map/set this way avoids the persistent path's per-step bucket-array
copying (transient map build ~35% faster at 20k here); vectors are comparable
since pvec conj is already ~O(1). The mutating ops return the transient and the
source collection is untouched.

spec/transients-spec (34 cases). conformance 218/218, jpm test green.
2026-06-05 08:02:21 -04:00
Yogthos
a0943cb944 test: fold ported-clojure batteries into spec
Mine the remaining integration 'ported Clojure' batteries into the spec layer
and delete them (clojure-atom/control/for/logic/macros, core, logic). A broad
function-coverage diff confirmed they exercised no clojure.core fn the spec
lacked; their distinctive value was the truthiness/boolean contract, now
captured in a dedicated spec.

New/expanded spec coverage:
- spec/truthiness-spec: only nil/false are falsy (0, "", [], {}, #{} are truthy);
  not / and / or return-value & short-circuit semantics; if-not/when-not/boolean
- assert (exceptions-spec), get-validator (state-spec)

Layout now: spec 23 files / 732 cases; integration trimmed to 10 genuine
cross-cutting batteries (conformance, SCI bootstrap/runtime, jank, compile-mode,
api, namespace, bootstrap, features, systematic-coverage). conformance 218/218,
jpm test green.
2026-06-05 07:50:16 -04:00
Yogthos
555cd7631e fix: catch binds unwrapped thrown value; var-set targets thread binding
Close jolt-dd5.
- catch now binds the originally-thrown value (unwrapping the :jolt/exception
  envelope), so (catch ... e (throw e)) rethrows the same exception instead of
  nesting another envelope, and (catch ... e e) on (throw 42) yields 42.
- var-set updates the innermost thread-binding frame for the var (replacing the
  stack slot) when the var is dynamically bound, matching Clojure; it falls back
  to the root otherwise.

Restored spec cases: exceptions rethrow + catch-binds-thrown-value, namespaces
var-set-in-binding. conformance 218/218, jpm test green.
2026-06-05 01:23:52 -04:00
Yogthos
a681daf7b9 test: fold cljs ports into spec; add exceptions spec + gap coverage
Mine the cljs port batteries into the spec layer and delete them (their behavior
is covered by spec; the unique functions they exercised are now specced).
Removed test/integration/ports/ entirely.

New/expanded spec coverage from the mining:
- spec/exceptions-spec: try/catch/finally, throw, ex-info/ex-message/ex-data/ex-cause
- doto, pr-str, keyword/symbol constructors, atom?, dynamic var binding

Two rare edges filed (jolt-...): rethrow of a caught ex-info re-wraps it; var-set
on a dynamic var inside binding no-ops. Core try/catch/ex-info and binding work.

Test layout is now spec (22 files, ~677 cases) / integration / unit / support.
conformance 218/218, jpm test green.
2026-06-05 01:12:39 -04:00
Yogthos
bcd0e42b33 test: fold phase ports into spec; promote compile-mode test
Mine the phase* port batteries into the spec layer and delete them (their
behavior is now covered by spec): phase5 (hierarchies), phase6 (tagged
literals/reader-conditionals), phase7 (lazy/sets), phase8+phase12 (protocols;
phase12 was a duplicate of phase8), phase10 (strings/set), phase13
(reify/walk). Unique cases mined into spec: reader #inst/#uuid/#?@ splice,
(Type. args) dot constructor, lazy-seq body-runs-once, keywordize-keys/
stringify-keys, custom :default key and explicit :hierarchy dispatch.

phase6-final tested the COMPILE-MODE path ({:compile? true}), distinct from the
interpreter-based spec — kept and renamed integration/compile-mode-test.

jpm test green, conformance 218/218.
2026-06-05 01:07:09 -04:00
Yogthos
0db7eb6ac8 fix: value-semantics for collection keys/elements; set literals evaluate
Close jolt-do7. Maps/sets keyed by a collection (a map, vector, ...) now compare
by value instead of Janet identity:
- PHM/PHS hash and compare keys through an injected canonicalizer (collection
  keys -> value-hashable struct/tuple); keys are still stored as-is
- map literals and core-assoc promote to a phm when a key is a collection
- frequencies/group-by use a phm base so collection elements/keys dedup by value
- set equality is value-based (from earlier)

Real bugs found and fixed along the way:
- set literals #{(inc 1)} did not evaluate their elements (stored raw forms!)
- the REPL printer rendered phm maps as {} (they hit the deftype branch); now a
  phm branch prints entries

Added spec cases (maps/collection-keys, sets/literals & value elements).
conformance 218/218, jpm test green.
2026-06-05 01:02:00 -04:00
Yogthos
ad5539b0de test(spec): macros, reader, host-interop, namespaces — fix def-doc, resolve, %&
Final spec areas. Bugs caught and fixed:
- (def name docstring value) used the docstring as the value; now the 3-arg
  docstring form binds the value and records :doc meta
- resolve was a nil stub; now a special form resolving a symbol to its var
  (nil if unresolved). Added find-ns (non-creating lookup) and ns-name.
- in-ns didn't evaluate its arg, so (in-ns 'foo) failed; now evaluates it per
  Clojure (the integration test's unquoted form updated to the quoted idiom)
- #(... %& ...) built %& as a positional param instead of a & rest param;
  now emits (fn* [... & gen] ...) so %& captures the rest

Full public-API spec layer now in place. conformance 218/218, jpm test green.
2026-06-05 00:35:48 -04:00
Yogthos
e3d2aa8d14 test(spec): lazy sequences, transducers, metadata
Add spec suites covering lazy-seq/lazy-cat laziness, infinite + self-referential
seqs (ones/nats/fib), realized?; transducers (map/filter/take/cat/keep/mapcat as
xforms, comp, into/transduce/sequence/eduction/completing); and metadata
(with-meta/meta/vary-meta/^ reader). All passed against jolt as written — no
implementation changes needed. jpm test green.
2026-06-05 00:26:44 -04:00
Yogthos
a24e9bfba0 test(spec): state, multimethods, protocols — fix reify, get-method, hierarchies
Add spec suites for stateful refs (atoms/volatiles/delays/promises), multimethods
(dispatch + hierarchies), and protocols/types/records. Bugs caught and fixed:
- reify with multiple methods registered only the first (stepped i by 2 over
  one-element specs); now collects every method spec
- get-method/methods/remove-method evaluated their arg to the dispatch fn, but a
  multimethod's methods live on its var — now resolve the var (get-method and
  methods are special forms; remove-method fixed). get-method/methods added.
- no global hierarchy: 2-arg derive modified a throwaway, isa? was hardcoded
  false, parents missing, ancestors/descendants returned arrays (so contains?
  failed), and dispatch ignored derive. Now a global hierarchy backs the 1/2-arg
  derive/isa?/parents/ancestors/descendants (returning sets) and multimethod
  dispatch falls back to it.

Corrected a phase5 port assertion that encoded the old isa? bug.
conformance 218/218, jpm test green.
2026-06-05 00:24:18 -04:00
Yogthos
50d63d896f test(spec): control-flow, functions, destructuring — fix loop destructuring
Add spec suites for conditionals/logic/let/loop/iteration/threading, functions
(definition, application, combinators), and destructuring across let/fn/loop/
doseq/for. Bug caught and fixed: loop* assumed simple-symbol bindings, so
(loop [[a b] [1 2]] ...) errored; it now destructures each binding via
destructure-bind, supporting patterns in loop bindings (and recur). jpm test green.
2026-06-05 00:16:15 -04:00
Yogthos
68d6b1d6c1 test(spec): numbers, strings, predicates — fix variadic <, seq?, index-of
Add spec suites for numbers/arithmetic, strings (str + clojure.string), and
type/value predicates. Bugs they caught and fixed:
- <, >, <=, >= were binary-only; now variadic ((< 1 2 3) chains)
- seq? was true for vectors; vectors are not ISeq in Clojure, so (seq? [1])
  is now false (true only for lists/lazy-seqs)
- clojure.string/index-of and last-index-of were 1-based (stray inc); now
  0-based per Clojure

Corrected a systematic-coverage assertion that encoded the old seq? bug.
jpm test green, conformance 218/218.
2026-06-05 00:13:19 -04:00
Yogthos
b353d625f1 test(spec): collections contract (sequences, vectors, lists, maps, sets)
Add behavioral spec suites for the collection API (~180 cases). Writing them
surfaced and fixed real bugs:
- (count nil) errored; now 0 (Clojure semantics)
- (repeat x) and (repeatedly f) — the infinite 1-arg arities — were unsupported;
  now return lazy infinite seqs
- set equality used deep= (representation-sensitive), so a set of map literals
  could differ from an equal set built differently; now value-based (each
  element value-equal to some element of the other)

Known limitation filed: phm-typed maps (from hash-map) used as set elements /
map keys hash by identity (map *literals* are structs and work).

jpm test green.
2026-06-05 00:08:37 -04:00