Skip to content

Commit 8194175

Browse files
committed
long: stay fixnum across the inline-tag boundary
prim_long was calling mino_int(), which auto-promotes to MINO_BIGINT for values outside the inline-tagged 61-bit range. That silently disables checked-arithmetic overflow on the result: (* (long (/ Long/MIN_VALUE 2)) 3) returned a wider bigint instead of throwing. Switch to mino_int_wrap(), the fixnum-preserving constructor already used by unchecked-multiply, inc, dec, and the bit primitives. Caught by the external clojure-test-suite driver: star.cljc was the only file failing assertions in the sweep.
1 parent a0c2d2d commit 8194175

4 files changed

Lines changed: 44 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Changelog
22

3+
## v0.422.2 — `long` Coercion Stays Fixnum
4+
5+
Fixes `(long x)` silently promoting to `:bigint` when the coerced
6+
value fell outside the inline-tagged 61-bit range (roughly
7+
`|x| > 2^60`). The primitive was calling the tower-aware
8+
`mino_int` constructor, which auto-promotes large fixnums to
9+
`MINO_BIGINT` so plain literals can keep using the numeric tower
10+
transparently. For `long` that promotion is wrong: a coerced long
11+
should observe checked-arithmetic overflow downstream, and a
12+
`MINO_BIGINT` silently disables that check. `prim_long` now uses
13+
`mino_int_wrap`, the same fixnum-preserving constructor that
14+
backs `unchecked-multiply`, `inc`, `dec`, and the bit
15+
primitives.
16+
17+
Caught by the external `clojure-test-suite` driver
18+
(`tests/clojure_test_suite.clj`): `star.cljc` was failing two
19+
overflow assertions of the form
20+
`(thrown? (* (long (/ Long/MIN_VALUE 2)) 3))` because the inner
21+
`long` was returning a bigint, so the outer `*` happily produced
22+
a wider bigint instead of throwing.
23+
324
## v0.422.1 — Amalgamate Symbol Collision Fix
425

526
Renames the static `kw_match` helper in `src/prim/bits.c` to

src/mino.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
*/
4848
#define MINO_VERSION_MAJOR 0
4949
#define MINO_VERSION_MINOR 422
50-
#define MINO_VERSION_PATCH 1
50+
#define MINO_VERSION_PATCH 2
5151

5252
/*
5353
* Human-readable version string of the *linked* runtime, e.g. "0.48.0".

src/prim/numeric_coerce.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,11 @@ mino_val *prim_long(mino_state *S, mino_val *args, mino_env *env)
150150
snprintf(buf, sizeof(buf), "long: %s", err ? err : "expected a number");
151151
return prim_throw_classified(S, "eval/type", "MTY001", buf);
152152
}
153-
/* mino's MINO_INT is int64 already, so the range check is the same
154-
* one extract_integer_for_cast already did against long range. */
155-
return mino_int(S, ll);
153+
/* Keep the MINO_INT type even when the value falls outside the
154+
* inline-tagged 61-bit range. mino_int would auto-promote to
155+
* MINO_BIGINT in that band, which would then silently disable
156+
* checked-arithmetic overflow on the coerced value. */
157+
return mino_int_wrap(S, ll);
156158
}
157159

158160
mino_val *prim_short(mino_state *S, mino_val *args, mino_env *env)

tests/numeric_edges_test.clj

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@
4141
(is (= 9223372036854775807 (unchecked-long 9223372036854775807N)))
4242
(is (= -9223372036854775808 (unchecked-long -9223372036854775808N))))
4343

44+
(deftest long-stays-fixnum-across-tag-boundary
45+
;; long must always produce a fixnum (:int), even when the value
46+
;; falls outside the inline-tagged 61-bit range. Promoting to bigint
47+
;; here would silently disable checked-arithmetic overflow on the
48+
;; result.
49+
(is (= :int (type (long 5))))
50+
(is (= :int (type (long 4611686018427387904))))
51+
(is (= :int (type (long -4611686018427387904))))
52+
(is (= :int (type (long 9223372036854775807))))
53+
(is (= :int (type (long -9223372036854775808))))
54+
;; Bigint-shaped input that fits the long range coerces back to :int.
55+
(is (= :int (type (long 4611686018427387904N))))
56+
;; Once coerced, downstream checked arithmetic must observe overflow
57+
;; rather than auto-promote.
58+
(is (thrown? (* (long (/ -9223372036854775808 2)) 3)))
59+
(is (thrown? (* 3 (long (/ -9223372036854775808 2))))))
60+
4461
(deftest with-precision-basic
4562
(is (= 0.33333M (with-precision 5 (/ 1M 3M))))
4663
(is (= 0.667M (with-precision 3 (/ 2M 3M))))

0 commit comments

Comments
 (0)