Add bigdec min/max (review follow-up)

Review found (< 1M 2M) worked but (min 1M 2M) threw — incoherent. Wire min/max
the same way as the other ops: value-position jolt-min/jolt-max shims (new in
seq.ss, added to core-value-procs) and call-position via bd-spec/bd-ops ->
jbd-min/jbd-max.

min/max return the original operand by value, not a coerced copy, matching
Clojure: (min 1M 2.0) -> 1M, (max 1M 2.0) -> 2.0, (min 1.50M 2M) -> 1.50M; a tie
keeps the second operand ((max 1.5M 1.50M) -> 1.50M). bigdec mixed with a flonum
in call position stays in the documented :any/contagion gap (value position
handles it). Re-mint; 6 more JVM-certified rows.
This commit is contained in:
Yogthos 2026-06-25 20:22:26 -04:00
parent 6fcc9fa8e6
commit 09a9ad8c75
7 changed files with 207 additions and 178 deletions

View file

@ -9,9 +9,9 @@
;; larger scale; multiply adds scales; divide gives the exact quotient at minimal
;; scale or throws ArithmeticException on a non-terminating expansion. Clojure
;; contagion: a bigdec mixed with an integer stays a bigdec; a flonum operand wins
;; (the result is a double). jbd-add/-sub/-mul/-div, the jbd-lt?/…/zero? helpers,
;; and jbd-quot/-rem are the shared engine. Two paths reach it, both leaving the
;; inlined native hot path untouched:
;; (the result is a double). jbd-add/-sub/-mul/-div, jbd-min/-max, the jbd-lt?/…
;; /zero? helpers, and jbd-quot/-rem are the shared engine. Two paths reach it, both
;; leaving the inlined native hot path untouched:
;; - value position ((reduce + bigs)/(apply * bigs)): the jolt-add/-sub/-mul/-div
;; and compare shims dispatch here when a bigdec operand is present.
;; - call position ((+ 1.5M 2.5M), (< a b), (zero? b)): jolt.passes.numeric tags
@ -185,6 +185,21 @@
(define (jbd-quot a b) (jbd-int-quot (jbd-coerce a) (jbd-coerce b)))
(define (jbd-rem a b) (jbd-int-rem (jbd-coerce a) (jbd-coerce b)))
;; min/max compare by value but return the ORIGINAL operand (its type and scale
;; unchanged), matching java/Clojure: (min 1M 2.0) -> 1M, (max 1M 2.0) -> 2.0,
;; (min 1.50M 2M) -> 1.50M. Comparison handles a bigdec mixed with an int / flonum.
(define (jbd-value-compare a b)
(if (or (flonum? a) (flonum? b))
(let ((fa (if (jbigdec? a) (jbigdec->flonum a) a)) (fb (if (jbigdec? b) (jbigdec->flonum b) b)))
(cond ((< fa fb) -1) ((> fa fb) 1) (else 0)))
(jbd-compare2 (jbd-coerce a) (jbd-coerce b))))
;; strict comparison so a tie keeps the second operand, like Clojure's
;; (if (< x y) x y) / (if (> x y) x y): (max 1.5M 1.50M) -> 1.50M.
(define (jbd-min2 a b) (if (< (jbd-value-compare a b) 0) a b))
(define (jbd-max2 a b) (if (> (jbd-value-compare a b) 0) a b))
(define (jbd-min x . xs) (fold-left jbd-min2 x xs))
(define (jbd-max x . xs) (fold-left jbd-max2 x xs))
;; --- wire into the value model ----------------------------------------------
(def-var! "clojure.core" "bigdec" jolt-bigdec)
@ -196,11 +211,15 @@
(define jbd-prev-sub jolt-sub)
(define jbd-prev-mul jolt-mul)
(define jbd-prev-div jolt-div)
(define jbd-prev-min jolt-min)
(define jbd-prev-max jolt-max)
(define (jbd-any? xs) (and (pair? xs) (or (jbigdec? (car xs)) (jbd-any? (cdr xs)))))
(set! jolt-add (lambda xs (if (jbd-any? xs) (apply jbd-add xs) (apply jbd-prev-add xs))))
(set! jolt-sub (lambda xs (if (jbd-any? xs) (apply jbd-sub xs) (apply jbd-prev-sub xs))))
(set! jolt-mul (lambda xs (if (jbd-any? xs) (apply jbd-mul xs) (apply jbd-prev-mul xs))))
(set! jolt-div (lambda xs (if (jbd-any? xs) (apply jbd-div xs) (apply jbd-prev-div xs))))
(set! jolt-min (lambda xs (if (jbd-any? xs) (apply jbd-min xs) (apply jbd-prev-min xs))))
(set! jolt-max (lambda xs (if (jbd-any? xs) (apply jbd-max xs) (apply jbd-prev-max xs))))
;; compare: add a bigdec arm (enables compare / sort / sorted collections). A
;; bigdec vs a plain number compares by value; bigdec vs bigdec is scale-independent.

File diff suppressed because one or more lines are too long

View file

@ -1517,6 +1517,6 @@
(guard (e (#t #f))
(def-var! "clojure.pprint" "add-padding" (letrec ((add-padding (lambda (width s) (let fnrec2063 ((width width) (s s)) (let* ((padding (max 0 (- width (jolt-count s))))) (jolt-apply (var-deref "clojure.core" "str") (jolt-invoke (var-deref "clojure.string" "join") (jolt-invoke (var-deref "clojure.core" "repeat") padding (integer->char 32))) s)))))) add-padding)))
(guard (e (#t #f))
(def-var-with-meta! "clojure.pprint" "print-table" (letrec ((print-table (case-lambda ((ks rows) (let fnrec2064 ((ks ks) (rows rows)) (if (jolt-truthy? (jolt-seq rows)) (let* ((widths (jolt-map (lambda (k) (let fnrec2065 ((k k)) (let* ((_a$2067 max) (_a$2068 (jolt-count (jolt-invoke (var-deref "clojure.core" "str") k))) (_a$2069 (jolt-map (lambda (p__39_) (let fnrec2066 ((p__39_ p__39_)) (jolt-count (jolt-invoke (var-deref "clojure.core" "str") (jolt-get p__39_ k))))) rows))) (jolt-apply _a$2067 _a$2068 _a$2069)))) ks)) (spacers (jolt-map (lambda (p__40_) (let fnrec2070 ((p__40_ p__40_)) (jolt-apply (var-deref "clojure.core" "str") (jolt-invoke (var-deref "clojure.core" "repeat") p__40_ "-")))) widths)) (fmt-row (lambda (leader divider trailer row) (let fnrec2071 ((leader leader) (divider divider) (trailer trailer) (row row)) (jolt-invoke (var-deref "clojure.core" "str") leader (jolt-apply (var-deref "clojure.core" "str") (jolt-invoke (var-deref "clojure.core" "interpose") divider (let* ((_a$2074 (lambda (G__96) (let fnrec2072 ((G__96 G__96)) (let* ((G__97 G__96) (col (jolt-nth G__97 0 jolt-nil)) (width (jolt-nth G__97 1 jolt-nil))) (jolt-invoke (var-deref "clojure.pprint" "add-padding") width (jolt-invoke (var-deref "clojure.core" "str") col)))))) (_a$2075 (jolt-map jolt-vector (jolt-map (lambda (p__41_) (let fnrec2073 ((p__41_ p__41_)) (jolt-get row p__41_))) ks) widths))) (jolt-map _a$2074 _a$2075)))) trailer))))) (begin (jolt-invoke (var-deref "clojure.core" "println")) (jolt-invoke (var-deref "clojure.core" "println") (jolt-invoke fmt-row "| " " | " " |" (jolt-invoke (var-deref "clojure.core" "zipmap") ks ks))) (jolt-invoke (var-deref "clojure.core" "println") (jolt-invoke fmt-row "|-" "-+-" "-|" (jolt-invoke (var-deref "clojure.core" "zipmap") ks spacers))) (begin (jolt-count (jolt-map (lambda (row) (let fnrec2076 ((row row)) (begin (jolt-invoke (var-deref "clojure.core" "println") (jolt-invoke fmt-row "| " " | " " |" row)) jolt-nil))) rows)) jolt-nil))) jolt-nil))) ((rows) (let fnrec2077 ((rows rows)) (jolt-invoke print-table (jolt-keys (jolt-first rows)) rows)))))) print-table) (let* ((_o$2078 (keyword #f "doc")) (_o$2079 "Prints a collection of maps in a textual table.")) (jolt-hash-map _o$2078 _o$2079))))
(def-var-with-meta! "clojure.pprint" "print-table" (letrec ((print-table (case-lambda ((ks rows) (let fnrec2064 ((ks ks) (rows rows)) (if (jolt-truthy? (jolt-seq rows)) (let* ((widths (jolt-map (lambda (k) (let fnrec2065 ((k k)) (let* ((_a$2067 jolt-max) (_a$2068 (jolt-count (jolt-invoke (var-deref "clojure.core" "str") k))) (_a$2069 (jolt-map (lambda (p__39_) (let fnrec2066 ((p__39_ p__39_)) (jolt-count (jolt-invoke (var-deref "clojure.core" "str") (jolt-get p__39_ k))))) rows))) (jolt-apply _a$2067 _a$2068 _a$2069)))) ks)) (spacers (jolt-map (lambda (p__40_) (let fnrec2070 ((p__40_ p__40_)) (jolt-apply (var-deref "clojure.core" "str") (jolt-invoke (var-deref "clojure.core" "repeat") p__40_ "-")))) widths)) (fmt-row (lambda (leader divider trailer row) (let fnrec2071 ((leader leader) (divider divider) (trailer trailer) (row row)) (jolt-invoke (var-deref "clojure.core" "str") leader (jolt-apply (var-deref "clojure.core" "str") (jolt-invoke (var-deref "clojure.core" "interpose") divider (let* ((_a$2074 (lambda (G__96) (let fnrec2072 ((G__96 G__96)) (let* ((G__97 G__96) (col (jolt-nth G__97 0 jolt-nil)) (width (jolt-nth G__97 1 jolt-nil))) (jolt-invoke (var-deref "clojure.pprint" "add-padding") width (jolt-invoke (var-deref "clojure.core" "str") col)))))) (_a$2075 (jolt-map jolt-vector (jolt-map (lambda (p__41_) (let fnrec2073 ((p__41_ p__41_)) (jolt-get row p__41_))) ks) widths))) (jolt-map _a$2074 _a$2075)))) trailer))))) (begin (jolt-invoke (var-deref "clojure.core" "println")) (jolt-invoke (var-deref "clojure.core" "println") (jolt-invoke fmt-row "| " " | " " |" (jolt-invoke (var-deref "clojure.core" "zipmap") ks ks))) (jolt-invoke (var-deref "clojure.core" "println") (jolt-invoke fmt-row "|-" "-+-" "-|" (jolt-invoke (var-deref "clojure.core" "zipmap") ks spacers))) (begin (jolt-count (jolt-map (lambda (row) (let fnrec2076 ((row row)) (begin (jolt-invoke (var-deref "clojure.core" "println") (jolt-invoke fmt-row "| " " | " " |" row)) jolt-nil))) rows)) jolt-nil))) jolt-nil))) ((rows) (let fnrec2077 ((rows rows)) (jolt-invoke print-table (jolt-keys (jolt-first rows)) rows)))))) print-table) (let* ((_o$2078 (keyword #f "doc")) (_o$2079 "Prints a collection of maps in a textual table.")) (jolt-hash-map _o$2078 _o$2079))))
(guard (e (#t #f))
(jolt-invoke (var-deref "clojure.core" "__set-pprint-write-hook!") (lambda (s) (let fnrec2080 ((s s)) (let* ((o (var-deref "clojure.core" "*out*"))) (if (jolt-truthy? (jolt-invoke (var-deref "clojure.core" "instance-check") (jolt-symbol #f "PrettyWriter") o)) (begin (jolt-invoke (var-deref "clojure.pprint" "-write") o s) #t) jolt-nil))))))

View file

@ -134,6 +134,8 @@
(define (jolt-sub . xs) (apply - xs))
(define (jolt-mul . xs) (apply * xs))
(define (jolt-div . xs) (apply / xs))
(define (jolt-min . xs) (apply min xs))
(define (jolt-max . xs) (apply max xs))
;; ============================================================================
;; IFn dispatch — the dynamic "value as fn" fallback. A callee that the emitter

View file

@ -42,7 +42,8 @@
;; EXACT results for exact/zero-arg inputs, breaking the all-double model in
;; higher-order use, so value-position arithmetic routes to the flonum wrappers.
(def ^:private core-value-procs
(merge native-ops {"+" "jolt-add" "-" "jolt-sub" "*" "jolt-mul" "/" "jolt-div"}))
(merge native-ops {"+" "jolt-add" "-" "jolt-sub" "*" "jolt-mul" "/" "jolt-div"
"min" "jolt-min" "max" "jolt-max"}))
;; Per-op arity gate: only lower when the Scheme prim and the jolt fn agree at
;; this arity. Ops absent from the table are variadic (legal at any arity).
@ -101,6 +102,7 @@
;; flonum path no literal rewrite is needed.
(def ^:private bd-ops
{"+" "jbd-add" "-" "jbd-sub" "*" "jbd-mul" "/" "jbd-div"
"min" "jbd-min" "max" "jbd-max"
"quot" "jbd-quot" "rem" "jbd-rem"
"<" "jbd-lt?" ">" "jbd-gt?" "<=" "jbd-le?" ">=" "jbd-ge?"
"zero?" "jbd-zero?" "pos?" "jbd-pos?" "neg?" "jbd-neg?"})

View file

@ -55,7 +55,7 @@
;; still a bigdec op. Each non-nil name must have an entry in backend bd-ops.
(defn- bd-spec [nm n]
(cond
(and (>= n 1) (contains? #{"+" "-" "*" "/"} nm)) :bigdec
(and (>= n 1) (contains? #{"+" "-" "*" "/" "min" "max"} nm)) :bigdec
(and (= n 2) (contains? #{"quot" "rem"} nm)) :bigdec
(and (= n 1) (contains? #{"zero?" "pos?" "neg?"} nm)) :bool
(and (>= n 2) (contains? #{"<" ">" "<=" ">="} nm)) :bool

View file

@ -1341,6 +1341,12 @@
{:suite "numbers / bigdec call position" :label "let-bound operands" :expected "4.0M" :actual "(let [a 1.5M b 2.5M] (+ a b))"}
{:suite "numbers / bigdec call position" :label "nested arithmetic propagates" :expected "6.0M" :actual "(+ (* 1.5M 2M) 3M)"}
{:suite "numbers / bigdec call position" :label "non-terminating divide throws" :expected ":nonterm" :actual "(try (/ 1M 3M) (catch ArithmeticException _ :nonterm))"}
{:suite "numbers / bigdec call position" :label "min" :expected "1M" :actual "(min 1M 2M)"}
{:suite "numbers / bigdec call position" :label "max of three" :expected "3M" :actual "(max 1M 2M 3M)"}
{:suite "numbers / bigdec call position" :label "min returns the operand, scale intact" :expected "\"1.50\"" :actual "(str (min 1.50M 2M))"}
{:suite "numbers / bigdec call position" :label "max tie keeps second operand" :expected "\"1.50\"" :actual "(str (max 1.5M 1.50M))"}
{:suite "numbers / bigdec arithmetic" :label "max (value position)" :expected "3M" :actual "(reduce max [1M 3M 2M])"}
{:suite "numbers / bigdec arithmetic" :label "min (apply)" :expected "1M" :actual "(apply min [3M 1M 2M])"}
{:suite "numbers / literal syntax" :label "ratio -> double" :expected "1/2" :actual "1/2"}
{:suite "numbers / literal syntax" :label "ratio 3/4" :expected "3/4" :actual "3/4"}
{:suite "numbers / literal syntax" :label "neg ratio" :expected "-1/2" :actual "-1/2"}