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.
This commit is contained in:
Yogthos 2026-06-27 15:41:35 -04:00
parent 86dd9650b6
commit a028cab04f
12 changed files with 588 additions and 517 deletions

View file

@ -26,6 +26,7 @@
form-bigdec? form-bigdec-source
form-ns-value? form-ns-value-name
form-var-value? form-var-value-ns form-var-value-name
unchecked-math?
form-macro? form-expand-1 resolve-global
form-sym-meta form-coll-meta host-intern! form-syntax-quote-lower
record-type? record-ctor-key form-position late-bind?
@ -611,6 +612,18 @@
(var-ref (compile-ns ctx) nm)
(uncompilable (str "Unable to resolve symbol: " nm " in this context"))))))))
;; The wrapping unchecked-* name a core arithmetic op rewrites to under
;; *unchecked-math*, or nil. n is the full item count (head + args); unary - is a
;; negate.
(defn- unchecked-arith [hname n]
(cond
(= hname "+") "unchecked-add"
(= hname "*") "unchecked-multiply"
(= hname "-") (if (= n 2) "unchecked-negate" "unchecked-subtract")
(= hname "inc") "unchecked-inc"
(= hname "dec") "unchecked-dec"
:else nil))
(defn- analyze-list [ctx form env]
(let [items (vec (form-elements form))]
(if (zero? (count items))
@ -626,8 +639,15 @@
(= "clojure.core" (form-sym-ns head))
(contains? handled (form-sym-name head)))
(form-sym-name head)))
shadowed (and hname (local? env hname))]
shadowed (and hname (local? env hname))
;; under *unchecked-math*, a core +/-/*/inc/dec becomes its wrapping
;; unchecked-* (computed once; nil when off or not such an op).
unm (when (and hname (not shadowed) (unchecked-math?))
(unchecked-arith hname (count items)))]
(cond
;; *unchecked-math* rewrite, before macro/special dispatch (these are
;; ordinary core fns). The unchecked-* form re-analyzes normally.
unm (analyze ctx (cons (symbol unm) (rest items)) env)
;; Canonical order (Clojure/CLJS analyze-seq): macroexpand FIRST, then
;; dispatch special forms / interop / invoke. A local shadows the macro.
;; A true special form is NOT shadowable by a same-named macro, matching