Skip to content

Commit a918dae

Browse files
leifericfclaude
andcommitted
bc: route bitwise BC fast paths through mino_int_wrap
Surfaced by mino-tests's gen.clj while building a seeded xorshift64* RNG: a few iterations of (bit-xor x (bit-shift-left x1 25)) inside a defn body raised MTY001 "bit-xor expects integers". Same code at top level worked. binop_int_fast in src/eval/bc/vm.c routed BAND / BOR / BXOR / SHL / SHR / USHR results through tag_or_box_int, which falls back to mino_int that promotes to MINO_BIGINT when bignum capability is installed. The corresponding prims use mino_int_wrap which always boxes as MINO_INT and never promotes. The two paths produced different result types; a downstream bit-xor's fast-path check refused the bigint -- hence the misleading MTY001. Fix routes all bitwise BC fast paths through mino_int_wrap. ADD / SUB / MUL keep tag_or_box_int since Clojure-correct overflow there DOES promote to bigint. Regression test in tests/bc_bitwise_test.clj: five bitwise ops at i64 range plus the xorshift64* chain that surfaced the issue. Suite: 1272 / 4549 green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ee801ef commit a918dae

5 files changed

Lines changed: 79 additions & 7 deletions

File tree

CHANGELOG.md

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

3+
## v0.255.5 — Fix: BC Bitwise Fast Path No Longer Promotes to Bigint
4+
5+
Surfaced by mino-tests's `gen.clj` while building a seeded RNG: an
6+
xorshift64* step inside a `defn` body raised `MTY001 "bit-xor
7+
expects integers"` after a few iterations. The same code at the
8+
top level worked.
9+
10+
Root cause: `binop_int_fast` in `src/eval/bc/vm.c` routed BAND /
11+
BOR / BXOR / SHL / SHR / USHR results through `tag_or_box_int`,
12+
which falls back to `mino_int` for values outside the inline-tag
13+
range. `mino_int` promotes to `MINO_BIGINT` when the bignum
14+
capability is installed. The corresponding prims
15+
(`prim_bit_shift_left`, `prim_bit_and`, ...) all use
16+
`mino_int_wrap`, which always boxes as a plain `MINO_INT` and never
17+
promotes. The two paths produced different result types for the
18+
same operation, and a downstream `bit-xor`'s fast-path check
19+
refused the bigint with `MTY001 "bit-xor expects integers"`.
20+
21+
Fix: bitwise BC fast-path results now go through `mino_int_wrap`,
22+
matching the prim semantics exactly. Arithmetic (ADD / SUB / MUL)
23+
keeps `tag_or_box_int` since Clojure-correct overflow there
24+
DOES promote to bigint.
25+
26+
Regression test in `tests/bc_bitwise_test.clj` covers all five
27+
bitwise ops at i64 range plus the xorshift64* chain that surfaced
28+
the original issue. Suite: 1272 / 4549 green.
29+
330
## v0.255.4 — Hygiene: I/O Buffer Overflow + Safepoint Comment
431

532
Two small cleanups closing the rest of the runtime-audit BUGS.md

src/eval/bc/vm.c

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -549,21 +549,29 @@ mino_val_t *binop_int_fast(mino_state_t *S, mino_val_t *lhs,
549549
r = a % b;
550550
if (subop == BINOP_MOD && r != 0 && ((r < 0) != (b < 0))) r += b;
551551
return tag_or_box_int(S, r);
552-
case BINOP_BAND: return tag_or_box_int(S, a & b);
553-
case BINOP_BOR: return tag_or_box_int(S, a | b);
554-
case BINOP_BXOR: return tag_or_box_int(S, a ^ b);
552+
/* Bitwise ops are i64 operations -- the prims call mino_int_wrap
553+
* which always boxes as MINO_INT, never promotes to bigint. Route
554+
* the fast-path results through mino_int_wrap as well so a BC-
555+
* compiled call has the same result type as the prim path. Using
556+
* tag_or_box_int here promoted to bigint via mino_int's overflow
557+
* branch when the bignum capability was installed, and the
558+
* surface bug was a downstream bit-xor refusing the promoted
559+
* bigint (MTY001 "bit-xor expects integers"). */
560+
case BINOP_BAND: return mino_int_wrap(S, a & b);
561+
case BINOP_BOR: return mino_int_wrap(S, a | b);
562+
case BINOP_BXOR: return mino_int_wrap(S, a ^ b);
555563
case BINOP_SHL:
556564
/* Shift amount must be in [0, 63]; route through unsigned so
557565
* that bit-shift-left of negative values matches the prim's
558566
* wrap-around result (and stays clear of signed-overflow UB). */
559567
if (b < 0 || b >= 64) return NULL;
560-
return tag_or_box_int(S, (long long)((unsigned long long)a << b));
568+
return mino_int_wrap(S, (long long)((unsigned long long)a << b));
561569
case BINOP_SHR:
562570
if (b < 0 || b >= 64) return NULL;
563-
return tag_or_box_int(S, a >> b);
571+
return mino_int_wrap(S, a >> b);
564572
case BINOP_USHR:
565573
if (b < 0 || b >= 64) return NULL;
566-
return tag_or_box_int(S, (long long)((unsigned long long)a >> b));
574+
return mino_int_wrap(S, (long long)((unsigned long long)a >> b));
567575
default: return NULL;
568576
}
569577
}

src/mino.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
*/
2929
#define MINO_VERSION_MAJOR 0
3030
#define MINO_VERSION_MINOR 255
31-
#define MINO_VERSION_PATCH 4
31+
#define MINO_VERSION_PATCH 5
3232

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

tests/bc_bitwise_test.clj

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
(require "tests/test")
2+
3+
;; Regression: the BC compiler's BINOP_SHL / BAND / BOR / BXOR /
4+
;; SHR / USHR fast paths used to route their results through
5+
;; tag_or_box_int, which calls mino_int that promotes overflowing
6+
;; values to MINO_BIGINT when the bignum capability is installed.
7+
;; The corresponding prims used mino_int_wrap which always boxes as
8+
;; MINO_INT. The surface symptom: a downstream bit-xor against the
9+
;; promoted bigint raised MTY001 "bit-xor expects integers" --
10+
;; misleading, since the bigint IS an integer, just not the type the
11+
;; bit-xor fast path was looking for. Fix routes the bitwise BC fast
12+
;; paths through mino_int_wrap to match the prim's semantics.
13+
14+
(deftest bc-bit-shift-left-stays-int
15+
(testing "bit-shift-left of a large i64 stays :int in a BC-compiled fn"
16+
(defn step [x] (bit-shift-left x 25))
17+
(let [r (step 142452317671654462)]
18+
(is (= :int (type r)))
19+
(is (= 4728920382667489280 r))))
20+
(testing "chained bit-shift-left + bit-xor in a fn body"
21+
(defn xorshift [x]
22+
(let [a (bit-xor x (unsigned-bit-shift-right x 12))
23+
b (bit-xor a (bit-shift-left a 25))
24+
c (bit-xor b (unsigned-bit-shift-right b 27))]
25+
c))
26+
(is (= :int (type (xorshift 142435135655313518))))))
27+
28+
(deftest bc-bitwise-stays-int
29+
(testing "bit-and, bit-or, bit-xor in BC-compiled fn stay :int"
30+
(defn bw [a b]
31+
[(bit-and a b) (bit-or a b) (bit-xor a b)])
32+
(let [r (bw 142452317671654462 9223372036854775000)]
33+
(is (every? #(= :int (type %)) r))))
34+
(testing "unsigned-bit-shift-right in BC fn"
35+
(defn ushr [x] (unsigned-bit-shift-right x 1))
36+
(is (= :int (type (ushr 142452317671654462))))))

tests/run.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
(require "tests/bc_closure_test")
6262
(require "tests/reduce_perf_test")
6363
(require "tests/bc_let_fold_test")
64+
(require "tests/bc_bitwise_test")
6465
(require "tests/ifn_test")
6566
(require "tests/stack_test")
6667
(require "tests/sorted_test")

0 commit comments

Comments
 (0)