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.
This commit is contained in:
parent
38abc1be84
commit
e66a91750e
13 changed files with 1106 additions and 729 deletions
|
|
@ -3515,4 +3515,23 @@
|
|||
{:suite "exceptions / hierarchy catch" :label "a typed throwable matches its superclasses, not unrelated ones" :expected "[true true true false]" :actual "(let [e (try (Long/parseLong \"z\") (catch Throwable e e))] [(instance? NumberFormatException e) (instance? IllegalArgumentException e) (instance? Exception e) (instance? java.io.IOException e)])"}
|
||||
{:suite "equality / identity" :label "= short-circuits on identity without realizing a lazy seq (Util.equiv k1 == k2)" :expected "0" :actual "(let [n (atom 0) s (map (fn [x] (swap! n inc) x) [1 2 3])] (= s s) @n)"}
|
||||
{:suite "realized? / lazy seq" :label "realized? reads a lazy seq's realization flag" :expected "[false true]" :actual "(let [s (map inc [1 2 3])] [(realized? s) (do (doall s) (realized? s))])"}
|
||||
{:suite "numbers / ops dispatch" :label "double contagion beats exact-zero shortcut" :expected "[0.0 0.0 0.0 0.0]" :actual "[(* 1.0 0) (* 0 1.5) (/ 0 1.5) (- 0.0 0)]"}
|
||||
{:suite "numbers / ops dispatch" :label "Inf times zero is NaN" :expected "[true true]" :actual "[(NaN? (* ##Inf 0)) (NaN? (* 0 ##-Inf))]"}
|
||||
{:suite "numbers / ops dispatch" :label "zero over NaN is NaN" :expected "true" :actual "(NaN? (/ 0 ##NaN))"}
|
||||
{:suite "numbers / ops dispatch" :label "double division by exact zero is signed Inf" :expected "[##Inf ##-Inf]" :actual "[(/ 1.0 0) (/ -1.0 0)]"}
|
||||
{:suite "numbers / ops dispatch" :label "exact division by zero throws ArithmeticException" :expected "[:ae :ae :ae]" :actual "[(try (/ 1 0) (catch ArithmeticException _ :ae)) (try (/ 0) (catch ArithmeticException _ :ae)) (try (quot 1.0 0) (catch ArithmeticException _ :ae))]"}
|
||||
{:suite "numbers / ops dispatch" :label "non-number operand is ClassCastException" :expected "[:cce :cce]" :actual "[(try (+ 1 \"a\") (catch ClassCastException _ :cce)) (try (< \"a\" 1) (catch ClassCastException _ :cce))]"}
|
||||
{:suite "numbers / ops dispatch" :label "quot over ratios truncates" :expected "[6 -2 1]" :actual "[(quot 3 1/2) (quot -3 4/3) (quot 37/2 15)]"}
|
||||
{:suite "numbers / ops dispatch" :label "quot/rem double contagion" :expected "[3.0 1.5 0.0]" :actual "[(quot 10.0 3) (rem 5.5 2) (quot 1 ##Inf)]"}
|
||||
{:suite "numbers / ops dispatch" :label "mod takes the divisor's sign on doubles" :expected "[0.5 -0.5 1.5]" :actual "[(mod -5.5 2) (mod 5.5 -2) (mod 5.5 2)]"}
|
||||
{:suite "numbers / ops dispatch" :label "min/max return the original operand" :expected "[1 4.0 1M]" :actual "[(min 1 2.0) (max 3 4.0) (min 1M 2)]"}
|
||||
{:suite "numbers / ops dispatch" :label "NaN wins min/max" :expected "[true true]" :actual "[(NaN? (min 1.0 ##NaN)) (NaN? (max ##NaN 1.0))]"}
|
||||
{:suite "numbers / ops dispatch" :label "bigdec call position mixes with doubles" :expected "[3.5 1.0 true]" :actual "[(+ 1.5M 2.0) (/ 2.0M 2.0) (< 1.5M 2.0)]"}
|
||||
{:suite "numbers / ops dispatch" :label "runtime bigdec reaches call-position ops" :expected "[4M 2M true]" :actual "(let [x (bigdec 3)] [(+ x 1) (- x 1) (< 1 x)])"}
|
||||
{:suite "numbers / ops dispatch" :label "inc/dec on bigdec" :expected "[2.5M 0.5M]" :actual "[(inc 1.5M) (dec 1.5M)]"}
|
||||
{:suite "numbers / with-precision" :label "rounding modes" :expected "[2M -1M 1M -2M 2M 1M]" :actual "[(with-precision 1 :rounding UP (* 1.1M 1M)) (with-precision 1 :rounding CEILING (* -1.1M 1M)) (with-precision 1 :rounding DOWN (* 1.9M 1M)) (with-precision 1 :rounding FLOOR (* -1.9M 1M)) (with-precision 1 :rounding HALF_EVEN (* 2.5M 1M)) (with-precision 1 :rounding HALF_DOWN (* 1.5M 1M))]"}
|
||||
{:suite "numbers / with-precision" :label "UNNECESSARY throws when rounding needed, passes exact" :expected "[:ae 2M]" :actual "[(try (with-precision 1 :rounding UNNECESSARY (* 1.5M 1M)) (catch ArithmeticException _ :ae)) (with-precision 1 :rounding UNNECESSARY (* 2M 1M))]"}
|
||||
{:suite "numbers / with-precision" :label "division rounds to precision (default HALF_UP)" :expected "\"0.3333\"" :actual "(str (with-precision 4 (/ 1M 3M)))"}
|
||||
{:suite "numbers / rationalize" :label "doubles go through shortest decimal print" :expected "[11/10 3/2 1 0 -1]" :actual "[(rationalize 1.1) (rationalize 1.5) (rationalize 1.0) (rationalize 0.0) (rationalize -1.0)]"}
|
||||
{:suite "numbers / rationalize" :label "bigdec and exacts pass through exactly" :expected "[3/2 1/3 7]" :actual "[(rationalize 1.5M) (rationalize 1/3) (rationalize 7)]"}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# clojure-test-suite known failures: <namespace> <fail> <error>
|
||||
# The gate fails on any per-namespace change, worse OR better; regenerate
|
||||
# with: JOLT_CTS_WRITE_BASELINE=1 host/chez/cts.sh
|
||||
clojure.core-test.abs 1 1
|
||||
clojure.core-test.abs 1 0
|
||||
clojure.core-test.add-watch 0 3
|
||||
clojure.core-test.ancestors 9 0
|
||||
clojure.core-test.atom 14 0
|
||||
|
|
@ -34,10 +34,8 @@ clojure.core-test.intern 2 0
|
|||
clojure.core-test.keys 0 4
|
||||
clojure.core-test.lazy-seq 3 0
|
||||
clojure.core-test.long 2 5
|
||||
clojure.core-test.max 3 1
|
||||
clojure.core-test.min 3 1
|
||||
clojure.core-test.minus 2 0
|
||||
clojure.core-test.mod 12 34
|
||||
clojure.core-test.mod 23 0
|
||||
clojure.core-test.neg-int-qmark 1 0
|
||||
clojure.core-test.not-eq 3 0
|
||||
clojure.core-test.nth 0 1
|
||||
|
|
@ -52,32 +50,29 @@ clojure.core-test.plus 11 0
|
|||
clojure.core-test.plus-squote 11 0
|
||||
clojure.core-test.pop 0 1
|
||||
clojure.core-test.pos-int-qmark 1 0
|
||||
clojure.core-test.quot 12 23
|
||||
clojure.core-test.quot 30 0
|
||||
clojure.core-test.rand-nth 0 1
|
||||
clojure.core-test.rational-qmark 3 0
|
||||
clojure.core-test.rationalize 10 0
|
||||
clojure.core-test.realized-qmark 3 0
|
||||
clojure.core-test.reduce 0 1
|
||||
clojure.core-test.rem 12 22
|
||||
clojure.core-test.rem 21 0
|
||||
clojure.core-test.remove-watch 0 1
|
||||
clojure.core-test.run-bang 1 0
|
||||
clojure.core-test.select-keys 2 0
|
||||
clojure.core-test.seqable-qmark 1 0
|
||||
clojure.core-test.short 7 5
|
||||
clojure.core-test.shuffle 1 0
|
||||
clojure.core-test.slash 2 22
|
||||
clojure.core-test.some-fn 3 0
|
||||
clojure.core-test.sort-by 2 0
|
||||
clojure.core-test.special-symbol-qmark 4 0
|
||||
clojure.core-test.star 22 0
|
||||
clojure.core-test.star-squote 19 0
|
||||
clojure.core-test.star 13 0
|
||||
clojure.core-test.star-squote 13 0
|
||||
clojure.core-test.transient 23 0
|
||||
clojure.core-test.underive 7 0
|
||||
clojure.core-test.update 1 0
|
||||
clojure.core-test.vals 0 3
|
||||
clojure.core-test.vec 1 0
|
||||
clojure.core-test.when-let 1 0
|
||||
clojure.core-test.with-precision 17 0
|
||||
clojure.edn-test.read-string 46 5
|
||||
clojure.string-test.capitalize 0 4
|
||||
clojure.string-test.ends-with-qmark 1 4
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
(ev "(def add1 (fn* ([x] (+ x 1))))")
|
||||
(let ((e (emitf "u" "(fn* ([y] (add1 y)))")))
|
||||
(ok "plain fn is inlined (call to add1 gone)" (not (has? e "add1")))
|
||||
(ok "inlined body present (+ ... 1)" (has? e "(+")))
|
||||
(ok "inlined body present (jolt-n+ ... 1)" (has? e "(jolt-n+")))
|
||||
(ok "inlined plain fn runtime: (add1 41) = 42" (= 42 (jnum->exact (ev "((fn* ([y] (add1 y))) 41)"))))
|
||||
|
||||
;; a ^double fn: body fl-ops fire after inlining, and the call is gone.
|
||||
|
|
|
|||
|
|
@ -127,10 +127,36 @@ arithmetic, `=`, and `hash` behave exactly as the JVM — but report `Long`, not
|
|||
`Byte`/`Short`/`Integer`, so `(class (byte 5))` and `(instance? Byte (byte 5))`
|
||||
diverge. This is substrate-inherent: a Chez fixnum is an immediate `identical?`
|
||||
to the plain integer (nothing to tag, and numbers carry no metadata), so the only
|
||||
faithful representation is a boxed type — which would crash raw compiled `(+ …)`
|
||||
(arithmetic emits a bare Chez `+`) or force every `+`/`-`/`*` through an
|
||||
unwrapping dispatcher, de-optimizing all arithmetic. Same shape as the accepted
|
||||
BigInt-vs-Long unification.
|
||||
faithful representation is a boxed type — which would crash the compiled
|
||||
arithmetic fast path (both operands Chez numbers → the raw Chez op) or force
|
||||
every `+`/`-`/`*` through an unwrapping dispatcher, de-optimizing all
|
||||
arithmetic. Same shape as the accepted BigInt-vs-Long unification.
|
||||
|
||||
## Number operations
|
||||
|
||||
Binary arithmetic and comparisons follow the JVM's `Numbers.ops(x, y)` category
|
||||
dispatch. Every position (call, value, higher-order) funnels a binary op through
|
||||
one seam (`host/chez/seq.ss`): operands inside Chez's tower take the native op
|
||||
with the JVM contagion rules patched in; an operand outside it (BigDecimal)
|
||||
falls to a slow hook the numeric shim extends (`host/chez/java/bigdec.ss`); a
|
||||
non-numeric operand throws `ClassCastException`. The rules the corpus pins
|
||||
(`numbers / ops dispatch`, `numbers / with-precision`, `numbers / rationalize`):
|
||||
|
||||
- A double operand wins: `(* 1.0 0)` is `0.0` (Chez's exact-zero shortcut must
|
||||
not leak), `(* ##Inf 0)` is `##NaN`, `(+ 1.5M 2.0)` is `3.5`.
|
||||
- Division: an exact zero divisor throws `ArithmeticException`; a double zero
|
||||
divisor yields `##Inf`/`##-Inf`/`##NaN`. `(/ 1M 3M)` with no bound
|
||||
`*math-context*` throws (non-terminating); under `with-precision` it rounds.
|
||||
- `quot`/`rem`/`mod` cover the full tower (ratios truncate; doubles keep double;
|
||||
`mod` takes the divisor's sign; zero divisor throws in both worlds).
|
||||
- `min`/`max` return the *original* operand (`(min 1 2.0)` is `1`, exact); a
|
||||
`##NaN` operand wins.
|
||||
- `with-precision` binds `*math-context*`; BigDecimal results round to the
|
||||
precision with the `java.math.RoundingMode` semantics (default `HALF_UP`,
|
||||
`UNNECESSARY` throws).
|
||||
- `rationalize` routes a double through its shortest decimal print
|
||||
(`BigDecimal.valueOf`), so `(rationalize 1.1)` is `11/10`, not the exact
|
||||
binary expansion.
|
||||
|
||||
## Hosting jolt on a new runtime
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"Rows of test/chez/corpus.edn whose :expected differs from reference JVM Clojure. The corpus is JVM-sourced (regen-corpus.clj), so this list is only the rows whose JVM value is an opaque host object that cannot round-trip to readable source — Java arrays, transients, atoms, beans, proxies, and chunks print as #object[..@addr] with a per-run identity — plus the (fn* foo) strictness case. For that the corpus keeps jolt's value. certify.clj gates on NEW (unlisted) divergences and STALE entries. Keyed by [suite label].",
|
||||
:legend
|
||||
{:numeric-model
|
||||
"jolt has a numeric tower (exact integer / Ratio / double); no BigDecimal",
|
||||
"jolt has the full numeric tower (exact integer / Ratio / double / BigDecimal); binary ops dispatch by operand category with JVM contagion rules",
|
||||
:host-model
|
||||
"no JVM host: classes->name strings, type->symbol, *in* is a map, inline-impl extenders, duck-typed with-open close",
|
||||
:reader-model
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue