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:
Yogthos 2026-07-02 06:41:45 -04:00
parent 38abc1be84
commit e66a91750e
13 changed files with 1106 additions and 729 deletions

View file

@ -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

View file

@ -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