From 939b168b89674a208d72939b5b2ffba251a82213 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Tue, 17 Mar 2026 09:20:10 +0100 Subject: [PATCH 01/15] core: support odd-bit primitive integers Allow declaring primitive integer types with non-byte logical widths while keeping their storage byte-rounded. Track the declared bit width in datatype layout metadata, use it in width-sensitive runtime and codegen paths, and add Core.bitsizeof for public introspection. Update bitstring and the manual to reflect logical bit widths, and add coverage for UInt63 and Int63 declarations, conversions, and LLVM lowering. Co-authored-by: Codex --- base/essentials.jl | 21 ++++++++++++++ base/intfuncs.jl | 8 ++++- doc/src/manual/types.md | 13 ++++++--- src/APInt-C.cpp | 23 ++++++++------- src/builtin_proto.h | 1 + src/builtins.c | 54 +++++++++++++++++++++++++++++++--- src/cgutils.cpp | 12 ++++---- src/common_symbols2.inc | 1 + src/datatype.c | 10 ++++--- src/intrinsics.cpp | 42 +++++++++++---------------- src/julia.h | 11 ++++++- src/runtime_intrinsics.c | 63 +++++++++++++++++++++++----------------- test/intfuncs.jl | 4 +++ test/intrinsics.jl | 29 ++++++++++++++++++ 14 files changed, 212 insertions(+), 80 deletions(-) diff --git a/base/essentials.jl b/base/essentials.jl index a834c57226439..b95eed2892557 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -828,6 +828,27 @@ Stacktrace: """ sizeof(x) = Core.sizeof(x) +""" + Core.bitsizeof(T::DataType) + Core.bitsizeof(obj) + +Logical size, in bits, of the canonical binary representation of the given `DataType` `T`, if any. +Or the logical size, in bits, of object `obj` if it is not a `DataType`. + +For primitive types, this may differ from `8*sizeof(T)` when the type uses byte-rounded storage +with unused bits in the last byte. + +# Examples +```jldoctest +julia> Core.bitsizeof(Float32) +32 + +julia> Core.bitsizeof(1.0) +64 +``` +""" +Core.bitsizeof + """ ifelse(condition::Bool, x, y) diff --git a/base/intfuncs.jl b/base/intfuncs.jl index 0a152f63f2604..f276a0c42f045 100644 --- a/base/intfuncs.jl +++ b/base/intfuncs.jl @@ -1133,7 +1133,7 @@ julia> bitstring(2.2) """ function bitstring(x::T) where {T} isprimitivetype(T) || throw(ArgumentError(LazyString(T, " not a primitive type"))) - sz = sizeof(T) * 8 + sz = Core.bitsizeof(T) str = _string_n(sz) GC.@preserve str begin p = pointer(str) @@ -1148,6 +1148,12 @@ function bitstring(x::T) where {T} x = lshr_int(x, 4) i -= 4 end + while i > 0 + b = UInt8(sizeof(T) == 1 ? bitcast(UInt8, x) : trunc_int(UInt8, x)) + unsafe_store!(p, 0x30 + (b & 0x01), i) + x = lshr_int(x, 1) + i -= 1 + end end return str end diff --git a/doc/src/manual/types.md b/doc/src/manual/types.md index b4d5a34bbb2fb..83dc31e4d7e27 100644 --- a/doc/src/manual/types.md +++ b/doc/src/manual/types.md @@ -300,10 +300,15 @@ The number of bits indicates how much storage the type requires and the name giv a name. A primitive type can optionally be declared to be a subtype of some supertype. If a supertype is omitted, then the type defaults to having `Any` as its immediate supertype. The declaration of [`Bool`](@ref) above therefore means that a boolean value takes eight bits to store, and has -[`Integer`](@ref) as its immediate supertype. Currently, only sizes that are multiples of -8 bits are supported and you are more likely to experience bugs with sizes other than those used above. -Therefore, boolean values, although they really need just a single bit, cannot be declared to be any -smaller than eight bits. +[`Integer`](@ref) as its immediate supertype. Primitive types continue to use byte-rounded storage, +so `sizeof(T)` rounds their storage size up to a whole number of bytes even when their logical width +is not a multiple of 8 bits. Use `Core.bitsizeof(T)` to query the declared logical width. + +Non-byte primitive widths are accepted, but remain an expert-only feature. They are more likely to +expose compiler, runtime, or ABI bugs than the standard built-in primitive widths, and arrays still +use byte-rounded element storage rather than packed bit layouts. Therefore, boolean values, although +they really need just a single bit, should still use the built-in eight-bit [`Bool`](@ref) unless +you are deliberately working with these expert-only semantics. The types [`Bool`](@ref), [`Int8`](@ref) and [`UInt8`](@ref) all have identical representations: they are eight-bit chunks of memory. Since Julia's type system is nominative, however, they diff --git a/src/APInt-C.cpp b/src/APInt-C.cpp index 52d82843be40e..1221aa7aff067 100644 --- a/src/APInt-C.cpp +++ b/src/APInt-C.cpp @@ -390,8 +390,7 @@ extern "C" JL_DLLEXPORT void LLVMSItoFP(jl_datatype_t *ty, integerPart *pa, jl_datatype_t *oty, integerPart *pr) { double val; { // end scope before jl_error call - unsigned numbytes = jl_datatype_size(ty); - unsigned numbits = numbytes * host_char_bit; + unsigned numbits = jl_datatype_nbits(ty); CREATE(a) val = a.roundToDouble(true); } @@ -411,8 +410,7 @@ extern "C" JL_DLLEXPORT void LLVMUItoFP(jl_datatype_t *ty, integerPart *pa, jl_datatype_t *oty, integerPart *pr) { double val; { // end scope before jl_error call - unsigned numbytes = jl_datatype_size(ty); - unsigned numbits = numbytes * host_char_bit; + unsigned numbits = jl_datatype_nbits(ty); CREATE(a) val = a.roundToDouble(false); } @@ -432,9 +430,10 @@ extern "C" JL_DLLEXPORT void LLVMSExt(jl_datatype_t *ty, integerPart *pa, jl_datatype_t *otys, integerPart *pr) { unsigned inumbytes = jl_datatype_size(ty); unsigned onumbytes = jl_datatype_size(otys); - if (!(onumbytes > inumbytes)) + unsigned inumbits = jl_datatype_nbits(ty); + unsigned onumbits = jl_datatype_nbits(otys); + if (!(onumbits > inumbits)) jl_error("SExt: output bitsize must be > input bitsize"); - unsigned inumbits = inumbytes * host_char_bit; int bits = (0 - inumbits) % host_char_bit; int signbit = (inumbits - 1) % host_char_bit; int sign = ((unsigned char*)pa)[inumbytes - 1] & (1 << signbit) ? -1 : 0; @@ -452,9 +451,10 @@ extern "C" JL_DLLEXPORT void LLVMZExt(jl_datatype_t *ty, integerPart *pa, jl_datatype_t *otys, integerPart *pr) { unsigned inumbytes = jl_datatype_size(ty); unsigned onumbytes = jl_datatype_size(otys); - if (!(onumbytes > inumbytes)) + unsigned inumbits = jl_datatype_nbits(ty); + unsigned onumbits = jl_datatype_nbits(otys); + if (!(onumbits > inumbits)) jl_error("ZExt: output bitsize must be > input bitsize"); - unsigned inumbits = inumbytes * host_char_bit; int bits = (0 - inumbits) % host_char_bit; // copy over the input bytes memcpy(pr, pa, inumbytes); @@ -468,11 +468,14 @@ void LLVMZExt(jl_datatype_t *ty, integerPart *pa, jl_datatype_t *otys, integerPa extern "C" JL_DLLEXPORT void LLVMTrunc(jl_datatype_t *ty, integerPart *pa, jl_datatype_t *otys, integerPart *pr) { - unsigned inumbytes = jl_datatype_size(ty); unsigned onumbytes = jl_datatype_size(otys); - if (!(onumbytes < inumbytes)) + unsigned inumbits = jl_datatype_nbits(ty); + unsigned onumbits = jl_datatype_nbits(otys); + if (!(onumbits < inumbits)) jl_error("Trunc: output bitsize must be < input bitsize"); memcpy(pr, pa, onumbytes); + if (onumbits % host_char_bit) + ((unsigned char*)pr)[onumbytes - 1] &= (1 << (onumbits % host_char_bit)) - 1; } extern "C" JL_DLLEXPORT diff --git a/src/builtin_proto.h b/src/builtin_proto.h index 1a06259ea6dca..db472bed3074b 100644 --- a/src/builtin_proto.h +++ b/src/builtin_proto.h @@ -66,6 +66,7 @@ extern "C" { XX(setfieldonce,"setfieldonce!") \ XX(setglobal,"setglobal!") \ XX(setglobalonce,"setglobalonce!") \ + XX(bitsizeof,"bitsizeof") \ XX(sizeof,"sizeof") \ XX(svec,"svec") \ XX(swapfield,"swapfield!") \ diff --git a/src/builtins.c b/src/builtins.c index 7a44ab86acce4..65a771ac70a73 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -98,6 +98,21 @@ static int NOINLINE compare_svec(jl_svec_t *a, jl_svec_t *b) JL_NOTSAFEPOINT return 1; } +static inline uint8_t last_byte_mask(jl_datatype_t *dt) JL_NOTSAFEPOINT +{ + uint32_t unused = jl_datatype_unusedbits(dt); + return unused ? (uint8_t)(0xff >> unused) : 0xff; +} + +static inline int primitive_bits_equal(const void *a, const void *b, jl_datatype_t *dt) JL_NOTSAFEPOINT +{ + size_t sz = jl_datatype_size(dt); + if (sz == 0) + return 1; + return (sz <= 1 || bits_equal(a, b, sz - 1)) && + ((((const uint8_t*)a)[sz - 1] ^ ((const uint8_t*)b)[sz - 1]) & last_byte_mask(dt)) == 0; +} + // See comment above for an explanation of NOINLINE. static int NOINLINE compare_fields(const jl_value_t *a, const jl_value_t *b, jl_datatype_t *dt) JL_NOTSAFEPOINT { @@ -143,8 +158,11 @@ static int NOINLINE compare_fields(const jl_value_t *a, const jl_value_t *b, jl_ return 0; } else { - assert(jl_datatype_nfields(ft) > 0); - if (!compare_fields((jl_value_t*)ao, (jl_value_t*)bo, ft)) + if (jl_datatype_nfields(ft) == 0) { + if (!primitive_bits_equal(ao, bo, ft)) + return 0; + } + else if (!compare_fields((jl_value_t*)ao, (jl_value_t*)bo, ft)) return 0; } } @@ -334,7 +352,9 @@ inline int jl_egal__bits(const jl_value_t *a JL_MAYBE_UNROOTED, const jl_value_t if (sz == 0) return 1; size_t nf = jl_datatype_nfields(dt); - if (nf == 0 || (!dt->layout->flags.haspadding && dt->layout->flags.isbitsegal)) + if (nf == 0) + return dt->layout->flags.haspadding ? primitive_bits_equal(a, b, dt) : bits_equal(a, b, sz); + if (!dt->layout->flags.haspadding && dt->layout->flags.isbitsegal) return bits_equal(a, b, sz); return compare_fields(a, b, dt); } @@ -450,6 +470,12 @@ static uintptr_t immut_id_(jl_datatype_t *dt, jl_value_t *v, uintptr_t h) JL_NOT // a few select pointers (notably symbol) also have special hash values // which may affect the stability of the objectid hash, even though // they don't affect egal comparison + if (nf == 0 && dt->layout->flags.haspadding) { + void *buf = alloca(sz); + memcpy(buf, v, sz); + ((uint8_t*)buf)[sz - 1] &= last_byte_mask(dt); + return bits_hash(buf, sz) ^ h; + } return bits_hash(v, sz) ^ h; } if (dt == jl_unionall_type) @@ -598,6 +624,26 @@ JL_CALLABLE(jl_f_sizeof) return jl_box_long(sz); } +JL_CALLABLE(jl_f_bitsizeof) +{ + JL_NARGS(bitsizeof, 1, 1); + jl_value_t *x = args[0]; + if (jl_is_unionall(x) || jl_is_uniontype(x)) + return jl_box_long(jl_unbox_long(jl_f_sizeof(F, args, 1)) * 8); + if (jl_is_datatype(x)) { + jl_datatype_t *dx = (jl_datatype_t*)x; + if (jl_is_primitivetype(dx)) + return jl_box_long(jl_datatype_nbits(dx)); + return jl_box_long(jl_unbox_long(jl_f_sizeof(F, args, 1)) * 8); + } + if (x == jl_bottom_type) + jl_error("The empty type does not have a definite size since it does not have instances."); + jl_datatype_t *dt = (jl_datatype_t*)jl_typeof(x); + if (jl_is_primitivetype(dt)) + return jl_box_long(jl_datatype_nbits(dt)); + return jl_box_long(jl_unbox_long(jl_f_sizeof(F, args, 1)) * 8); +} + JL_CALLABLE(jl_f_issubtype) { JL_NARGS(<:, 2, 2); @@ -2131,7 +2177,7 @@ JL_CALLABLE(jl_f__primitivetype) jl_errorf("invalid declaration of primitive type %s", jl_symbol_name((jl_sym_t*)name)); ssize_t nb = jl_unbox_long(vnb); - if (nb < 1 || nb >= (1 << 23) || (nb & 7) != 0) + if (nb < 1 || nb >= (1 << 23)) jl_errorf("invalid number of bits in primitive type %s", jl_symbol_name((jl_sym_t*)name)); jl_datatype_t *dt = jl_new_primitivetype(args[1], (jl_module_t*)args[0], NULL, (jl_svec_t*)args[2], nb); diff --git a/src/cgutils.cpp b/src/cgutils.cpp index 0e92e05338a90..4784392bac165 100644 --- a/src/cgutils.cpp +++ b/src/cgutils.cpp @@ -814,7 +814,7 @@ static Type *_julia_type_to_llvm(jl_codegen_output_t *ctx, LLVMContext &ctxt, jl if (jt == (jl_value_t*)jl_bottom_type || jt == (jl_value_t*)jl_typeofbottom_type || jt == (jl_value_t*)jl_typeofbottom_type->super) return getVoidTy(ctxt); if (jl_is_concrete_immutable(jt) || no_boxing) { - if (jl_datatype_nbits(jt) == 0) + if (jl_datatype_nbits((jl_datatype_t*)jt) == 0) return getVoidTy(ctxt); Type *t = _julia_struct_to_llvm(ctx, ctxt, jt, isboxed); assert(t != NULL); @@ -874,8 +874,8 @@ static Type *bitstype_to_llvm(jl_value_t *bt, LLVMContext &ctxt, bool llvmcall = jl_error("invalid pointer address space"); return PointerType::get(ctxt, as); } - int nb = jl_datatype_size(bt); - return Type::getIntNTy(ctxt, nb * 8); + int nb = jl_datatype_nbits((jl_datatype_t*)bt); + return Type::getIntNTy(ctxt, nb); } static bool jl_type_hasptr(jl_value_t* typ) @@ -2435,7 +2435,8 @@ static jl_cgval_t typed_load(jl_codectx_t &ctx, Value *ptr, Value *idx_0based, j } if (isa(elty)) { unsigned nb2 = PowerOf2Ceil(nb); - if (nb != nb2) + unsigned bitwidth = cast(elty)->getBitWidth(); + if (nb != nb2 || bitwidth != 8 * nb) elty = Type::getIntNTy(ctx.builder.getContext(), 8 * nb2); } } @@ -2611,7 +2612,8 @@ static jl_cgval_t typed_store(jl_codectx_t &ctx, realelty = elty; if (Order != AtomicOrdering::NotAtomic && isa(elty)) { unsigned nb2 = PowerOf2Ceil(nb); - if (nb != nb2) + unsigned bitwidth = cast(elty)->getBitWidth(); + if (nb != nb2 || bitwidth != 8 * nb) elty = Type::getIntNTy(ctx.builder.getContext(), 8 * nb2); } if (op != StoreKind::Modify) { diff --git a/src/common_symbols2.inc b/src/common_symbols2.inc index e9c070ee8da6a..f10f955e69004 100644 --- a/src/common_symbols2.inc +++ b/src/common_symbols2.inc @@ -9,6 +9,7 @@ jl_symbol("unchecked_oneto"), jl_symbol("structdiff"), jl_symbol("UnitRange"), jl_symbol("unitrange_last"), +jl_symbol("bitsizeof"), jl_symbol("sizeof"), jl_symbol("check_sign_bit"), jl_symbol("is_top_bit_set"), diff --git a/src/datatype.c b/src/datatype.c index 04fb25631d894..9f2d2bd4b4b8d 100644 --- a/src/datatype.c +++ b/src/datatype.c @@ -192,6 +192,7 @@ static jl_datatype_layout_t *jl_get_layout(uint32_t sz, int haspadding, int isbitsegal, int arrayelem, + uint8_t padding, jl_fielddesc32_t desc[], uint32_t pointers[]) JL_NOTSAFEPOINT { @@ -243,7 +244,7 @@ static jl_datatype_layout_t *jl_get_layout(uint32_t sz, flddesc->flags.arrayelem_isunion = (arrayelem & 2) != 0; flddesc->flags.arrayelem_isatomic = (arrayelem & 4) != 0; flddesc->flags.arrayelem_islocked = (arrayelem & 8) != 0; - flddesc->flags.padding = 0; + flddesc->flags.padding = padding; flddesc->npointers = npointers; flddesc->first_ptr = first_ptr; @@ -609,7 +610,7 @@ void jl_get_genericmemory_layout(jl_datatype_t *st) arrayelem |= 8; // arrayelem_islocked } assert(!st->layout); - st->layout = jl_get_layout(elsz, nfields, npointers, al, haspadding, isbitsegal, arrayelem, NULL, pointers); + st->layout = jl_get_layout(elsz, nfields, npointers, al, haspadding, isbitsegal, arrayelem, 0, NULL, pointers); st->zeroinit = zi; //st->has_concrete_subtype = 1; //st->isbitstype = 0; @@ -844,7 +845,7 @@ void jl_compute_field_offsets(jl_datatype_t *st) } } assert(ptr_i == npointers); - st->layout = jl_get_layout(sz, nfields, npointers, alignm, haspadding, isbitsegal, 0, desc, pointers); + st->layout = jl_get_layout(sz, nfields, npointers, alignm, haspadding, isbitsegal, 0, 0, desc, pointers); if (should_malloc) { free(desc); if (npointers) @@ -1002,6 +1003,7 @@ JL_DLLEXPORT jl_datatype_t *jl_new_primitivetype(jl_value_t *name, jl_module_t * jl_datatype_t *bt = jl_new_datatype((jl_sym_t*)name, module, super, parameters, jl_emptysvec, jl_emptysvec, jl_emptysvec, 0, 0, 0); uint32_t nbytes = (nbits + 7) / 8; + uint8_t unused_bits = (uint8_t)(nbytes * 8 - nbits); uint32_t alignm = next_power_of_two(nbytes); # if defined(_CPU_X86_) && !defined(_OS_WINDOWS_) // datalayout strings are often weird: on 64-bit they usually follow fairly simple rules, @@ -1024,7 +1026,7 @@ JL_DLLEXPORT jl_datatype_t *jl_new_primitivetype(jl_value_t *name, jl_module_t * bt->ismutationfree = 1; bt->isidentityfree = 1; bt->isbitstype = (parameters == jl_emptysvec); - bt->layout = jl_get_layout(nbytes, 0, 0, alignm, 0, 1, 0, NULL, NULL); + bt->layout = jl_get_layout(nbytes, 0, 0, alignm, unused_bits != 0, 1, 0, unused_bits, NULL, NULL); bt->instance = NULL; return bt; } diff --git a/src/intrinsics.cpp b/src/intrinsics.cpp index e9e02c4e922cb..728ab3886ad58 100644 --- a/src/intrinsics.cpp +++ b/src/intrinsics.cpp @@ -213,21 +213,23 @@ static Constant *julia_const_to_llvm(jl_codectx_t &ctx, const void *ptr, jl_data APFloat(lt->getFltSemantics(), APInt(64, data64))); } if (lt->isFloatingPointTy() || lt->isIntegerTy() || lt->isPointerTy()) { - int nb = jl_datatype_size(bt); - APInt val(8 * nb, 0); + int nbytes = jl_datatype_size(bt); + APInt val(jl_datatype_nbits(bt), 0); void *bits = const_cast(val.getRawData()); assert(sys::IsLittleEndianHost); - memcpy(bits, ptr, nb); + memcpy(bits, ptr, nbytes); + if (nbytes > 0) + ((uint8_t*)bits)[nbytes - 1] &= (uint8_t)(0xff >> jl_datatype_unusedbits(bt)); if (lt->isFloatingPointTy()) { return ConstantFP::get(ctx.builder.getContext(), APFloat(lt->getFltSemantics(), val)); } if (lt->isPointerTy()) { - Type *Ty = IntegerType::get(ctx.builder.getContext(), 8 * nb); + Type *Ty = IntegerType::get(ctx.builder.getContext(), nbytes * 8); Constant *addr = ConstantInt::get(Ty, val); return ConstantExpr::getIntToPtr(addr, lt); } - assert(cast(lt)->getBitWidth() == 8u * nb); + assert(cast(lt)->getBitWidth() == val.getBitWidth()); return ConstantInt::get(lt, val); } @@ -564,6 +566,7 @@ static jl_cgval_t generic_bitcast(jl_codectx_t &ctx, ArrayRef argv) Type *llvmt = bitstype_to_llvm((jl_value_t*)bt, ctx.builder.getContext(), true); uint32_t nb = jl_datatype_size(bt); + uint32_t nbits = jl_datatype_nbits(bt); Value *bt_value_rt = NULL; if (!jl_is_concrete_type((jl_value_t*)bt)) { @@ -574,30 +577,19 @@ static jl_cgval_t generic_bitcast(jl_codectx_t &ctx, ArrayRef argv) // Examine the second argument // bool isboxed; Type *vxt = julia_type_to_llvm(ctx, v.typ, &isboxed); - if (!jl_is_primitivetype(v.typ) || jl_datatype_size(v.typ) != nb) { + if (!jl_is_primitivetype(v.typ)) { Value *typ = emit_typeof(ctx, v, false, false); - if (!jl_is_primitivetype(v.typ)) { - if (jl_is_datatype(v.typ) && !jl_is_abstracttype(v.typ)) { - emit_error(ctx, "bitcast: value not a primitive type"); - return jl_cgval_t(); - } - else { - Value *isprimitive = emit_datatype_isprimitivetype(ctx, typ); - error_unless(ctx, isprimitive, "bitcast: value not a primitive type"); - } - } if (jl_is_datatype(v.typ) && !jl_is_abstracttype(v.typ)) { - emit_error(ctx, "bitcast: argument size does not match size of target type"); + emit_error(ctx, "bitcast: value not a primitive type"); return jl_cgval_t(); } - else { - Value *size = emit_datatype_size(ctx, typ); - auto sizecheck = ctx.builder.CreateICmpEQ(size, ConstantInt::get(size->getType(), nb)); - setName(ctx.emission_context, sizecheck, "sizecheck"); - error_unless(ctx, - sizecheck, - "bitcast: argument size does not match size of target type"); - } + Value *isprimitive = emit_datatype_isprimitivetype(ctx, typ); + error_unless(ctx, isprimitive, "bitcast: value not a primitive type"); + return emit_runtime_call(ctx, bitcast, argv, 2); + } + if (jl_datatype_nbits((jl_datatype_t*)v.typ) != nbits) { + emit_error(ctx, "bitcast: argument bitsize does not match bitsize of target type"); + return jl_cgval_t(); } assert(!v.isghost); diff --git a/src/julia.h b/src/julia.h index 7d113535169e0..8eb830f2ff63a 100644 --- a/src/julia.h +++ b/src/julia.h @@ -606,6 +606,7 @@ typedef struct { // If set, this type's egality can be determined entirely by comparing // the non-padding bits of this datatype. uint16_t isbitsegal : 1; + // Low 3 bits encode how many bits in the last storage byte are unused. uint16_t padding : 8; } flags; // union { @@ -1449,8 +1450,16 @@ STATIC_INLINE const jl_datatype_layout_t *jl_datatype_layout(jl_datatype_t *t) J } #define jl_datatype_size(t) (jl_datatype_layout((jl_datatype_t*)(t))->size) #define jl_datatype_align(t) (jl_datatype_layout((jl_datatype_t*)(t))->alignment) -#define jl_datatype_nbits(t) ((jl_datatype_layout((jl_datatype_t*)(t))->size)*8) #define jl_datatype_nfields(t) (jl_datatype_layout((jl_datatype_t*)(t))->nfields) +STATIC_INLINE uint32_t jl_datatype_unusedbits(jl_datatype_t *t) JL_NOTSAFEPOINT +{ + return jl_datatype_layout(t)->flags.padding & 0x7; +} +STATIC_INLINE uint32_t jl_datatype_nbits(jl_datatype_t *t) JL_NOTSAFEPOINT +{ + const jl_datatype_layout_t *layout = jl_datatype_layout(t); + return layout->size * 8 - (layout->flags.padding & 0x7); +} JL_DLLEXPORT void *jl_symbol_name(jl_sym_t *s); // inline version with strong type check to detect typos in a `->name` chain diff --git a/src/runtime_intrinsics.c b/src/runtime_intrinsics.c index d06a5ad1c3ebf..dd55f11b1d6aa 100644 --- a/src/runtime_intrinsics.c +++ b/src/runtime_intrinsics.c @@ -409,8 +409,8 @@ JL_DLLEXPORT jl_value_t *jl_bitcast(jl_value_t *ty, jl_value_t *v) jl_error("bitcast: target type not a leaf primitive type"); if (!jl_is_primitivetype(jl_typeof(v))) jl_error("bitcast: value not a primitive type"); - if (jl_datatype_size(jl_typeof(v)) != jl_datatype_size(ty)) - jl_error("bitcast: argument size does not match size of target type"); + if (jl_datatype_nbits((jl_datatype_t*)jl_typeof(v)) != jl_datatype_nbits((jl_datatype_t*)ty)) + jl_error("bitcast: argument bitsize does not match bitsize of target type"); if (ty == jl_typeof(v)) return v; if (ty == (jl_value_t*)jl_bool_type) @@ -682,14 +682,17 @@ JL_DLLEXPORT jl_value_t *jl_cglobal_auto(jl_value_t *v) { return jl_cglobal(v, (jl_value_t*)jl_nothing_type); } -static inline char signbitbyte(void *a, unsigned bytes) JL_NOTSAFEPOINT +static inline char signbitbyte(void *a, unsigned bytes, unsigned nbits) JL_NOTSAFEPOINT { - // sign bit of an signed number of n bytes, as a byte - return (((signed char*)a)[bytes - 1] < 0) ? ~0 : 0; + unsigned signbit = (nbits - 1) % host_char_bit; + return (((unsigned char*)a)[bytes - 1] & (1 << signbit)) ? ~0 : 0; } -static inline char usignbitbyte(void *a, unsigned bytes) JL_NOTSAFEPOINT +static inline char usignbitbyte(void *a, unsigned bytes, unsigned nbits) JL_NOTSAFEPOINT { + (void)a; + (void)bytes; + (void)nbits; // sign bit of an unsigned number return 0; } @@ -709,9 +712,9 @@ static inline unsigned select_by_size(unsigned sz) JL_NOTSAFEPOINT #define SELECTOR_FUNC(intrinsic) \ typedef intrinsic##_t select_##intrinsic##_t[6]; \ - static inline intrinsic##_t select_##intrinsic(unsigned sz, const select_##intrinsic##_t list) JL_NOTSAFEPOINT \ + static inline intrinsic##_t select_##intrinsic(unsigned sz, unsigned runtime_nbits, const select_##intrinsic##_t list) JL_NOTSAFEPOINT \ { \ - intrinsic##_t thunk = list[select_by_size(sz)]; \ + intrinsic##_t thunk = runtime_nbits == sz * host_char_bit ? list[select_by_size(sz)] : NULL; \ if (!thunk) thunk = list[0]; \ return thunk; \ } @@ -965,14 +968,15 @@ uu_iintrinsic(name, u) static inline jl_value_t *jl_iintrinsic_1(jl_value_t *a, const char *name, - char (*getsign)(void*, unsigned), + char (*getsign)(void*, unsigned, unsigned), jl_value_t *(*lambda1)(jl_value_t*, void*, unsigned, unsigned, const void*), const void *list) { jl_value_t *ty = jl_typeof(a); if (!jl_is_primitivetype(ty)) jl_errorf("%s: value is not a primitive type", name); void *pa = jl_data_ptr(a); - unsigned isize = jl_datatype_size(jl_typeof(a)); + unsigned runtime_nbits = jl_datatype_nbits((jl_datatype_t*)ty); + unsigned isize = jl_datatype_size((jl_datatype_t*)ty); unsigned isize2 = next_power_of_two(isize); unsigned osize = jl_datatype_size(ty); unsigned osize2 = next_power_of_two(osize); @@ -985,7 +989,7 @@ jl_value_t *jl_iintrinsic_1(jl_value_t *a, const char *name, /* TODO: this memcpy assumes little-endian, * for big-endian, need to align the copy to the other end */ \ memcpy(pa2, pa, isize); - memset((char*)pa2 + isize, getsign(pa, isize), osize2 - isize); + memset((char*)pa2 + isize, getsign(pa, isize, runtime_nbits), osize2 - isize); pa = pa2; } jl_value_t *newv = lambda1(ty, pa, osize, osize2, list); @@ -996,17 +1000,19 @@ jl_value_t *jl_iintrinsic_1(jl_value_t *a, const char *name, static inline jl_value_t *jl_intrinsiclambda_ty1(jl_value_t *ty, void *pa, unsigned osize, unsigned osize2, const void *voidlist) { - intrinsic_1_t op = select_intrinsic_1(osize2, (const intrinsic_1_t*)voidlist); + unsigned runtime_nbits = jl_datatype_nbits((jl_datatype_t*)ty); + intrinsic_1_t op = select_intrinsic_1(osize2, runtime_nbits, (const intrinsic_1_t*)voidlist); void *pr = alloca(osize2); - op(osize * host_char_bit, pa, pr); + op(runtime_nbits, pa, pr); return jl_new_bits(ty, pr); } static inline jl_value_t *jl_intrinsiclambda_u1(jl_value_t *ty, void *pa, unsigned osize, unsigned osize2, const void *voidlist) { jl_task_t *ct = jl_current_task; - intrinsic_u1_t op = select_intrinsic_u1(osize2, (const intrinsic_u1_t*)voidlist); - uint64_t cnt = op(osize * host_char_bit, pa); + unsigned runtime_nbits = jl_datatype_nbits((jl_datatype_t*)ty); + intrinsic_u1_t op = select_intrinsic_u1(osize2, runtime_nbits, (const intrinsic_u1_t*)voidlist); + uint64_t cnt = op(runtime_nbits, pa); // TODO: the following assume little-endian // for big-endian, need to copy from the other end of cnt if (osize <= sizeof(cnt)) { @@ -1172,7 +1178,7 @@ checked_iintrinsic(name, u, jl_intrinsiclambda_checkeddiv) static inline jl_value_t *jl_iintrinsic_2(jl_value_t *a, jl_value_t *b, const char *name, - char (*getsign)(void*, unsigned), + char (*getsign)(void*, unsigned, unsigned), jl_value_t *(*lambda2)(jl_value_t*, void*, void*, unsigned, unsigned, const void*), const void *list, int cvtb) { @@ -1187,6 +1193,7 @@ jl_value_t *jl_iintrinsic_2(jl_value_t *a, jl_value_t *b, const char *name, if (!jl_is_primitivetype(ty)) jl_errorf("%s: a is not a primitive type", name); void *pa = jl_data_ptr(a), *pb = jl_data_ptr(b); + unsigned runtime_nbits = jl_datatype_nbits((jl_datatype_t*)ty); unsigned sz = jl_datatype_size(ty); unsigned sz2 = next_power_of_two(sz); unsigned szb = cvtb ? jl_datatype_size(tyb) : sz; @@ -1194,7 +1201,7 @@ jl_value_t *jl_iintrinsic_2(jl_value_t *a, jl_value_t *b, const char *name, /* round type up to the appropriate c-type and set/clear the unused bits */ void *pa2 = alloca(sz2); memcpy(pa2, pa, sz); - memset((char*)pa2 + sz, getsign(pa, sz), sz2 - sz); + memset((char*)pa2 + sz, getsign(pa, sz, runtime_nbits), sz2 - sz); pa = pa2; } if (sz2 > szb) { @@ -1203,7 +1210,7 @@ jl_value_t *jl_iintrinsic_2(jl_value_t *a, jl_value_t *b, const char *name, */ void *pb2 = alloca(sz2); memcpy(pb2, pb, szb); - memset((char*)pb2 + szb, cvtb ? 0 : getsign(pb, szb), sz2 - szb); + memset((char*)pb2 + szb, cvtb ? 0 : getsign(pb, szb, runtime_nbits), sz2 - szb); pb = pb2; } jl_value_t *newv = lambda2(ty, pa, pb, sz, sz2, list); @@ -1213,15 +1220,17 @@ jl_value_t *jl_iintrinsic_2(jl_value_t *a, jl_value_t *b, const char *name, static inline jl_value_t *jl_intrinsiclambda_2(jl_value_t *ty, void *pa, void *pb, unsigned sz, unsigned sz2, const void *voidlist) { void *pr = alloca(sz2); - intrinsic_2_t op = select_intrinsic_2(sz2, (const intrinsic_2_t*)voidlist); - op(sz * host_char_bit, pa, pb, pr); + unsigned runtime_nbits = jl_datatype_nbits((jl_datatype_t*)ty); + intrinsic_2_t op = select_intrinsic_2(sz2, runtime_nbits, (const intrinsic_2_t*)voidlist); + op(runtime_nbits, pa, pb, pr); return jl_new_bits(ty, pr); } static inline jl_value_t *jl_intrinsiclambda_cmp(jl_value_t *ty, void *pa, void *pb, unsigned sz, unsigned sz2, const void *voidlist) { - intrinsic_cmp_t op = select_intrinsic_cmp(sz2, (const intrinsic_cmp_t*)voidlist); - int cmp = op(sz * host_char_bit, pa, pb); + unsigned runtime_nbits = jl_datatype_nbits((jl_datatype_t*)ty); + intrinsic_cmp_t op = select_intrinsic_cmp(sz2, runtime_nbits, (const intrinsic_cmp_t*)voidlist); + int cmp = op(runtime_nbits, pa, pb); return cmp ? jl_true : jl_false; } @@ -1235,8 +1244,9 @@ static inline jl_value_t *jl_intrinsiclambda_checked(jl_value_t *ty, void *pa, v jl_task_t *ct = jl_current_task; jl_value_t *newv = jl_gc_alloc(ct->ptls, jl_datatype_size(tuptyp), tuptyp); - intrinsic_checked_t op = select_intrinsic_checked(sz2, (const intrinsic_checked_t*)voidlist); - int ovflw = op(sz * host_char_bit, pa, pb, jl_data_ptr(newv)); + unsigned runtime_nbits = jl_datatype_nbits((jl_datatype_t*)ty); + intrinsic_checked_t op = select_intrinsic_checked(sz2, runtime_nbits, (const intrinsic_checked_t*)voidlist); + int ovflw = op(runtime_nbits, pa, pb, jl_data_ptr(newv)); char *ao = (char*)jl_data_ptr(newv) + sz; *ao = (char)ovflw; @@ -1245,8 +1255,9 @@ static inline jl_value_t *jl_intrinsiclambda_checked(jl_value_t *ty, void *pa, v static inline jl_value_t *jl_intrinsiclambda_checkeddiv(jl_value_t *ty, void *pa, void *pb, unsigned sz, unsigned sz2, const void *voidlist) { void *pr = alloca(sz2); - intrinsic_checked_t op = select_intrinsic_checked(sz2, (const intrinsic_checked_t*)voidlist); - int ovflw = op(sz * host_char_bit, pa, pb, pr); + unsigned runtime_nbits = jl_datatype_nbits((jl_datatype_t*)ty); + intrinsic_checked_t op = select_intrinsic_checked(sz2, runtime_nbits, (const intrinsic_checked_t*)voidlist); + int ovflw = op(runtime_nbits, pa, pb, pr); if (ovflw) jl_throw(jl_diverror_exception); return jl_new_bits(ty, pr); diff --git a/test/intfuncs.jl b/test/intfuncs.jl index 9774903c23f0a..4bea95f05f30e 100644 --- a/test/intfuncs.jl +++ b/test/intfuncs.jl @@ -514,6 +514,10 @@ primitive type BitString128 128 end "0000000000000000000000000000000000000000000000000000010000001011") @test bitstring(Int128(3)) == "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011" @test bitstring(reinterpret(BitString128, Int128(3))) == "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011" + primitive type BitString63 <: Unsigned 63 end + bit63(x::UInt64) = Core.Intrinsics.trunc_int(BitString63, x) + @test bitstring(bit63(0x7fff_ffff_ffff_ffff)) == repeat("1", 63) + @test bitstring(bit63(0x4000_0000_0000_0001)) == "1" * repeat("0", 61) * "1" end @testset "digits/base" begin diff --git a/test/intrinsics.jl b/test/intrinsics.jl index f0c662f179eaa..70d3acf2078a8 100644 --- a/test/intrinsics.jl +++ b/test/intrinsics.jl @@ -4,6 +4,7 @@ # For curmod_* include("testenv.jl") +using InteractiveUtils: code_llvm # bits types @test isa((() -> Core.Intrinsics.bitcast(Ptr{Int8}, 0))(), Ptr{Int8}) @@ -79,6 +80,30 @@ let x, y, f @test string(y) == "$(curmod_prefix)Int24(0x468ace)" end +# odd-bit primitive integers keep byte-rounded storage but logical bit widths +primitive type Int63 <: Signed 63 end +primitive type UInt63 <: Unsigned 63 end +Int63(x::Int64) = Core.Intrinsics.trunc_int(Int63, x) +UInt63(x::UInt64) = Core.Intrinsics.trunc_int(UInt63, x) +Base.Int64(x::Int63) = Core.Intrinsics.sext_int(Int64, x) +Base.UInt64(x::UInt63) = Core.Intrinsics.zext_int(UInt64, x) +let x = UInt63(0xc000_ba98_8765_4321), y = Int63(-1) + @test sizeof(UInt63) == 8 + @test Core.bitsizeof(UInt63) == 63 + @test Core.bitsizeof(x) == 63 + @test Core.bitsizeof(Int64) == 64 + @test Core.bitsizeof(1.0) == 64 + @test UInt64(x) === 0x4000_ba98_8765_4321 + @test Int64(y) === -1 + # Under Revise`, this `code_llvm` query can fail in InteractiveUtils' + # reflective inference path before it reaches the actual odd-bit lowering. + if !isdefined(Main, :Revise) + id_u63(x::UInt63) = x + ir = sprint(io -> code_llvm(io, id_u63, Tuple{UInt63}; debuginfo=:none)) + @test occursin(r"\bi63\b", ir) + end +end + # test nonsensical valid conversions and errors compiled_addi(x, y) = Core.Intrinsics.add_int(x, y) @@ -110,6 +135,10 @@ end (0x80000000, Int64(0x80000000), -Int64(0x80000000)) @test compiled_conv(UInt32, UInt64(0xC000_BA98_8765_4321)) == (0x87654321, 0x0000000087654321, 0xffffffff87654321) +@test compiled_conv(UInt63, UInt64(0xC000_BA98_8765_4321)) == + (UInt63(0x4000_BA98_8765_4321), 0x4000_BA98_8765_4321, 0xC000_BA98_8765_4321) +@test compiled_conv(Int63, Int64(-1)) == + (Int63(-1), typemax(Int64), -Int64(1)) @test_throws ErrorException compiled_conv(Bool, im) function compiled_fptrunc(::Type{T}, x) where T From e0c385f43f316b0db9071b8f21a974876ef2c557 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Thu, 19 Mar 2026 23:18:56 +0100 Subject: [PATCH 02/15] base: fix bitstring for sub-byte primitives Handle byte extraction in bitstring by logical bit width rather than rounded storage size. Sub-byte primitives need zero-extension to UInt8, while wider odd-bit primitives still truncate to their low byte. Add regression coverage for a 6-bit primitive to keep bitstring working below one byte. Co-authored-by: Codex --- base/intfuncs.jl | 6 ++++-- test/intfuncs.jl | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/base/intfuncs.jl b/base/intfuncs.jl index f276a0c42f045..889212bbd99ff 100644 --- a/base/intfuncs.jl +++ b/base/intfuncs.jl @@ -1134,12 +1134,14 @@ julia> bitstring(2.2) function bitstring(x::T) where {T} isprimitivetype(T) || throw(ArgumentError(LazyString(T, " not a primitive type"))) sz = Core.bitsizeof(T) + onebyte = sz == 8 + subbyte = sz < 8 str = _string_n(sz) GC.@preserve str begin p = pointer(str) i = sz while i >= 4 - b = UInt32(sizeof(T) == 1 ? bitcast(UInt8, x) : trunc_int(UInt8, x)) + b = UInt32(onebyte ? bitcast(UInt8, x) : subbyte ? zext_int(UInt8, x) : trunc_int(UInt8, x)) d = 0x30303030 + ((b * 0x08040201) >> 0x3) & 0x01010101 unsafe_store!(p, (d >> 0x00) % UInt8, i-3) unsafe_store!(p, (d >> 0x08) % UInt8, i-2) @@ -1149,7 +1151,7 @@ function bitstring(x::T) where {T} i -= 4 end while i > 0 - b = UInt8(sizeof(T) == 1 ? bitcast(UInt8, x) : trunc_int(UInt8, x)) + b = UInt8(onebyte ? bitcast(UInt8, x) : subbyte ? zext_int(UInt8, x) : trunc_int(UInt8, x)) unsafe_store!(p, 0x30 + (b & 0x01), i) x = lshr_int(x, 1) i -= 1 diff --git a/test/intfuncs.jl b/test/intfuncs.jl index 4bea95f05f30e..c52c6b4d343a3 100644 --- a/test/intfuncs.jl +++ b/test/intfuncs.jl @@ -514,6 +514,9 @@ primitive type BitString128 128 end "0000000000000000000000000000000000000000000000000000010000001011") @test bitstring(Int128(3)) == "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011" @test bitstring(reinterpret(BitString128, Int128(3))) == "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011" + primitive type BitString6 <: Unsigned 6 end + bit6(x::UInt64) = Core.Intrinsics.trunc_int(BitString6, x) + @test bitstring(bit6(UInt64(0x11))) == "010001" primitive type BitString63 <: Unsigned 63 end bit63(x::UInt64) = Core.Intrinsics.trunc_int(BitString63, x) @test bitstring(bit63(0x7fff_ffff_ffff_ffff)) == repeat("1", 63) From 3b7930f1e5f52dfe50fe780479271f9ebc628b23 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Thu, 19 Mar 2026 23:30:02 +0100 Subject: [PATCH 03/15] src: split layout unused_bits from padding Store odd-bit primitive width metadata in an explicit unused_bits field instead of overloading the layout struct padding bits. This keeps the layout description self-documenting while preserving the same packed footprint. Adjust the layout builder and runtime metadata accessors to use the new field name, and update the corresponding runtime_internals bitfield notes. Co-authored-by: Codex --- base/runtime_internals.jl | 3 ++- src/datatype.c | 6 ++++-- src/julia.h | 8 ++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/base/runtime_internals.jl b/base/runtime_internals.jl index e17f3f2f084be..c9d25ecb6de01 100644 --- a/base/runtime_internals.jl +++ b/base/runtime_internals.jl @@ -574,7 +574,8 @@ struct DataTypeLayout # arrayelem_isatomic : 1; # arrayelem_islocked : 1; # isbitsegal : 1; - # padding : 8; + # unused_bits : 3; + # padding : 5; end """ diff --git a/src/datatype.c b/src/datatype.c index 9f2d2bd4b4b8d..fd45407637dc5 100644 --- a/src/datatype.c +++ b/src/datatype.c @@ -192,7 +192,7 @@ static jl_datatype_layout_t *jl_get_layout(uint32_t sz, int haspadding, int isbitsegal, int arrayelem, - uint8_t padding, + uint8_t unused_bits, jl_fielddesc32_t desc[], uint32_t pointers[]) JL_NOTSAFEPOINT { @@ -244,7 +244,8 @@ static jl_datatype_layout_t *jl_get_layout(uint32_t sz, flddesc->flags.arrayelem_isunion = (arrayelem & 2) != 0; flddesc->flags.arrayelem_isatomic = (arrayelem & 4) != 0; flddesc->flags.arrayelem_islocked = (arrayelem & 8) != 0; - flddesc->flags.padding = padding; + flddesc->flags.unused_bits = unused_bits; + flddesc->flags.padding = 0; flddesc->npointers = npointers; flddesc->first_ptr = first_ptr; @@ -1051,6 +1052,7 @@ JL_DLLEXPORT jl_datatype_t * jl_new_foreign_type(jl_sym_t *name, layout->flags.haspadding = 1; layout->flags.isbitsegal = 0; layout->flags.fielddesc_type = 3; + layout->flags.unused_bits = 0; layout->flags.padding = 0; layout->flags.arrayelem_isboxed = 0; layout->flags.arrayelem_isunion = 0; diff --git a/src/julia.h b/src/julia.h index 8eb830f2ff63a..1cc7578f14a40 100644 --- a/src/julia.h +++ b/src/julia.h @@ -606,8 +606,8 @@ typedef struct { // If set, this type's egality can be determined entirely by comparing // the non-padding bits of this datatype. uint16_t isbitsegal : 1; - // Low 3 bits encode how many bits in the last storage byte are unused. - uint16_t padding : 8; + uint16_t unused_bits : 3; + uint16_t padding : 5; } flags; // union { // jl_fielddesc8_t field8[nfields]; @@ -1453,12 +1453,12 @@ STATIC_INLINE const jl_datatype_layout_t *jl_datatype_layout(jl_datatype_t *t) J #define jl_datatype_nfields(t) (jl_datatype_layout((jl_datatype_t*)(t))->nfields) STATIC_INLINE uint32_t jl_datatype_unusedbits(jl_datatype_t *t) JL_NOTSAFEPOINT { - return jl_datatype_layout(t)->flags.padding & 0x7; + return jl_datatype_layout(t)->flags.unused_bits; } STATIC_INLINE uint32_t jl_datatype_nbits(jl_datatype_t *t) JL_NOTSAFEPOINT { const jl_datatype_layout_t *layout = jl_datatype_layout(t); - return layout->size * 8 - (layout->flags.padding & 0x7); + return layout->size * 8 - layout->flags.unused_bits; } JL_DLLEXPORT void *jl_symbol_name(jl_sym_t *s); From 119a0c6698ad3e770b8d312bf1c4e9379d56090d Mon Sep 17 00:00:00 2001 From: Max Horn Date: Fri, 20 Mar 2026 09:54:35 +0100 Subject: [PATCH 04/15] core: tighten odd-bit primitive integer support Reject byte-oriented reinterpret helpers for non-byte-aligned primitive types, fix APInt sign, zero-extension, and truncation for sub-byte widths, and use storage width for ABI integer rewrites. Extend the odd-width intrinsics and reinterpret tests. Co-authored-by: Codex --- base/reinterpretarray.jl | 29 +++++++++++++++++++- base/show.jl | 14 ++++++++-- src/APInt-C.cpp | 32 +++++++++++++--------- src/abi_win32.cpp | 2 +- src/abi_win64.cpp | 2 +- src/abi_x86_64.cpp | 2 +- test/intrinsics.jl | 59 ++++++++++++++++++++++++++++++++++++++++ test/reinterpretarray.jl | 46 +++++++++++++++++++++++++++++++ 8 files changed, 166 insertions(+), 20 deletions(-) diff --git a/base/reinterpretarray.jl b/base/reinterpretarray.jl index 6ca3f4273f8d1..c8c4a52cb1b1d 100644 --- a/base/reinterpretarray.jl +++ b/base/reinterpretarray.jl @@ -770,6 +770,11 @@ end """ @assume_effects :foldable function padding(T::DataType, baseoffset::Int = 0) pads = Padding[] + if isprimitivetype(T) + Core.bitsizeof(T) % 8 == 0 || throw(ArgumentError(LazyString( + "padding cannot be computed for non-byte-aligned primitive type ", T))) + return Core.svec() + end last_end::Int = baseoffset for i = 1:fieldcount(T) offset = baseoffset + Int(fieldoffset(T, i)) @@ -829,11 +834,29 @@ end end @assume_effects :foldable function packedsize(::Type{T}) where T + if isprimitivetype(T) + Core.bitsizeof(T) % 8 == 0 || throw(ArgumentError(LazyString( + "packed size cannot be computed for non-byte-aligned primitive type ", T))) + return Core.bitsizeof(T) >> 3 + end pads = padding(T) return sizeof(T) - sum((p.size for p ∈ pads), init = 0) end -@assume_effects :foldable ispacked(::Type{T}) where T = isempty(padding(T)) +@assume_effects :foldable function ispacked(::Type{T}) where T + isprimitivetype(T) && return Core.bitsizeof(T) == sizeof(T) * 8 + return isempty(padding(T)) +end + +@assume_effects :foldable function has_bit_padding(::Type{T}) where T + if isprimitivetype(T) + return Core.bitsizeof(T) % 8 != 0 + end + for i in 1:fieldcount(T) + has_bit_padding(fieldtype(T, i)) && return true + end + return false +end function _copytopacked!(ptr_out::Ptr{Out}, ptr_in::Ptr{In}) where {Out, In} writeoffset = 0 @@ -871,6 +894,10 @@ end # handle non-primitive types isbitstype(Out) || throw(ArgumentError("Target type for `reinterpret` must be isbits")) isbitstype(In) || throw(ArgumentError("Source type for `reinterpret` must be isbits")) + has_bit_padding(Out) && throw(ArgumentError(LazyString( + "cannot `reinterpret` type ", Out, " containing non-byte-aligned primitive fields"))) + has_bit_padding(In) && throw(ArgumentError(LazyString( + "cannot `reinterpret` type ", In, " containing non-byte-aligned primitive fields"))) inpackedsize = packedsize(In) outpackedsize = packedsize(Out) inpackedsize == outpackedsize || diff --git a/base/show.jl b/base/show.jl index 3a08e05219bfe..6872618e0ad1c 100644 --- a/base/show.jl +++ b/base/show.jl @@ -525,10 +525,18 @@ function _show_default(io::IO, @nospecialize(x)) else print(io, "0x") r = Ref{Any}(x) + nbits = Core.bitsizeof(t) + nbytes = cld(nbits, 8) GC.@preserve r begin p = unsafe_convert(Ptr{Cvoid}, r) - for i in (nb - 1):-1:0 - print(io, string(unsafe_load(convert(Ptr{UInt8}, p + i)), base = 16, pad = 2)) + for i in (nbytes - 1):-1:0 + byte = unsafe_load(convert(Ptr{UInt8}, p + i)) + if i == nbytes - 1 && nbits % 8 != 0 + byte &= (UInt8(1) << (nbits % 8)) - UInt8(1) + print(io, string(byte, base = 16)) + else + print(io, string(byte, base = 16, pad = 2)) + end end end end @@ -1299,7 +1307,7 @@ show(io::IO, ::Nothing) = print(io, "nothing") show(io::IO, n::Signed) = (write(io, string(n)); nothing) function show(io::IO, n::Unsigned) if get(io, :hexunsigned, true)::Bool - print(io, "0x", string(n, pad = sizeof(n)<<1, base = 16)) + print(io, "0x", string(n, pad = cld(Core.bitsizeof(n), 4), base = 16)) else if get(io, :typeinfo, Nothing)::Type == typeof(n) print(io, n) diff --git a/src/APInt-C.cpp b/src/APInt-C.cpp index 1221aa7aff067..29bed1fe7c341 100644 --- a/src/APInt-C.cpp +++ b/src/APInt-C.cpp @@ -428,20 +428,23 @@ void LLVMUItoFP(jl_datatype_t *ty, integerPart *pa, jl_datatype_t *oty, integerP extern "C" JL_DLLEXPORT void LLVMSExt(jl_datatype_t *ty, integerPart *pa, jl_datatype_t *otys, integerPart *pr) { - unsigned inumbytes = jl_datatype_size(ty); - unsigned onumbytes = jl_datatype_size(otys); unsigned inumbits = jl_datatype_nbits(ty); unsigned onumbits = jl_datatype_nbits(otys); + unsigned inumbytes = (inumbits + host_char_bit - 1) / host_char_bit; + unsigned onumbytes = jl_datatype_size(otys); if (!(onumbits > inumbits)) jl_error("SExt: output bitsize must be > input bitsize"); - int bits = (0 - inumbits) % host_char_bit; + unsigned bits = (host_char_bit - (inumbits % host_char_bit)) % host_char_bit; int signbit = (inumbits - 1) % host_char_bit; int sign = ((unsigned char*)pa)[inumbytes - 1] & (1 << signbit) ? -1 : 0; - // copy over the input bytes + // copy over the meaningful input bytes memcpy(pr, pa, inumbytes); if (bits) { - // sign-extend the partial byte - ((signed char*)pr)[inumbytes - 1] = ((signed char*)pa)[inumbytes - 1] << bits >> bits; + // sign-extend the partial byte. + unsigned char byte = ((unsigned char*)pa)[inumbytes - 1]; + ((unsigned char*)pr)[inumbytes - 1] = sign + ? (byte | (unsigned char)(0xFF << (host_char_bit - bits))) + : (byte & (unsigned char)(0xFF >> bits)); } // sign-extend the rest of the bytes memset((char*)pr + inumbytes, sign, onumbytes - inumbytes); @@ -449,14 +452,14 @@ void LLVMSExt(jl_datatype_t *ty, integerPart *pa, jl_datatype_t *otys, integerPa extern "C" JL_DLLEXPORT void LLVMZExt(jl_datatype_t *ty, integerPart *pa, jl_datatype_t *otys, integerPart *pr) { - unsigned inumbytes = jl_datatype_size(ty); - unsigned onumbytes = jl_datatype_size(otys); unsigned inumbits = jl_datatype_nbits(ty); unsigned onumbits = jl_datatype_nbits(otys); + unsigned inumbytes = (inumbits + host_char_bit - 1) / host_char_bit; + unsigned onumbytes = jl_datatype_size(otys); if (!(onumbits > inumbits)) jl_error("ZExt: output bitsize must be > input bitsize"); - int bits = (0 - inumbits) % host_char_bit; - // copy over the input bytes + unsigned bits = (host_char_bit - (inumbits % host_char_bit)) % host_char_bit; + // copy over the meaningful input bytes memcpy(pr, pa, inumbytes); if (bits) { // zero the remaining bits of the partial byte @@ -468,14 +471,17 @@ void LLVMZExt(jl_datatype_t *ty, integerPart *pa, jl_datatype_t *otys, integerPa extern "C" JL_DLLEXPORT void LLVMTrunc(jl_datatype_t *ty, integerPart *pa, jl_datatype_t *otys, integerPart *pr) { - unsigned onumbytes = jl_datatype_size(otys); unsigned inumbits = jl_datatype_nbits(ty); unsigned onumbits = jl_datatype_nbits(otys); + unsigned onumbytes_storage = jl_datatype_size(otys); + unsigned onumbytes_value = (onumbits + host_char_bit - 1) / host_char_bit; if (!(onumbits < inumbits)) jl_error("Trunc: output bitsize must be < input bitsize"); - memcpy(pr, pa, onumbytes); + memcpy(pr, pa, onumbytes_value); if (onumbits % host_char_bit) - ((unsigned char*)pr)[onumbytes - 1] &= (1 << (onumbits % host_char_bit)) - 1; + ((unsigned char*)pr)[onumbytes_value - 1] &= (1 << (onumbits % host_char_bit)) - 1; + if (onumbytes_value < onumbytes_storage) + memset((char*)pr + onumbytes_value, 0, onumbytes_storage - onumbytes_value); } extern "C" JL_DLLEXPORT diff --git a/src/abi_win32.cpp b/src/abi_win32.cpp index ccfc6a16ebee3..c1f150c4dcecd 100644 --- a/src/abi_win32.cpp +++ b/src/abi_win32.cpp @@ -65,7 +65,7 @@ Type *preferred_llvm_type(jl_datatype_t *dt, bool isret, LLVMContext &ctx) const // rewrite integer sized (non-sret) struct to the corresponding integer if (!dt->layout->nfields && !dt->layout->npointers) return NULL; - return Type::getIntNTy(ctx, jl_datatype_nbits(dt)); + return Type::getIntNTy(ctx, jl_datatype_size(dt) * 8); } }; diff --git a/src/abi_win64.cpp b/src/abi_win64.cpp index ec97203eee5ff..536a8ca91e46f 100644 --- a/src/abi_win64.cpp +++ b/src/abi_win64.cpp @@ -72,7 +72,7 @@ Type *preferred_llvm_type(jl_datatype_t *dt, bool isret, LLVMContext &ctx) const { size_t size = jl_datatype_size(dt); if (size > 0 && win64_reg_size(size) && !jl_is_primitivetype(dt)) - return Type::getIntNTy(ctx, jl_datatype_nbits(dt)); + return Type::getIntNTy(ctx, jl_datatype_size(dt) * 8); return NULL; } diff --git a/src/abi_x86_64.cpp b/src/abi_x86_64.cpp index 6a853421dbccd..64c0899c9f613 100644 --- a/src/abi_x86_64.cpp +++ b/src/abi_x86_64.cpp @@ -223,7 +223,7 @@ Type *preferred_llvm_type(jl_datatype_t *dt, bool isret, LLVMContext &ctx) const return NULL; size_t size = jl_datatype_size(dt); - size_t nbits = jl_datatype_nbits(dt); + size_t nbits = size * 8; if (size > 16 || size == 0) return NULL; diff --git a/test/intrinsics.jl b/test/intrinsics.jl index 70d3acf2078a8..37c8a42434b7e 100644 --- a/test/intrinsics.jl +++ b/test/intrinsics.jl @@ -80,6 +80,65 @@ let x, y, f @test string(y) == "$(curmod_prefix)Int24(0x468ace)" end +@testset "non-standard integer widths" begin + primitive type TestUInt24 24 end + primitive type TestUInt40 40 end + primitive type TestUInt48 48 end + primitive type TestUInt5 5 end + primitive type TestUInt17 17 end + primitive type TestUInt63 63 end + primitive type TestInt17 <: Signed 17 end + primitive type TestInt63 <: Signed 63 end + + @test Core.bitsizeof(TestUInt24) == 24 + @test Core.bitsizeof(TestUInt40) == 40 + @test Core.bitsizeof(TestUInt48) == 48 + @test Core.bitsizeof(TestUInt5) == 5 + @test Core.bitsizeof(TestUInt17) == 17 + @test Core.bitsizeof(TestUInt63) == 63 + + x24 = Core.Intrinsics.trunc_int(TestUInt24, UInt64(0xffee_ddcc_bbaa_9988)) + @test Core.Intrinsics.zext_int(UInt64, x24) === 0x0000_0000_00aa_9988 + + x40 = Core.Intrinsics.trunc_int(TestUInt40, UInt64(0xffee_ddcc_bbaa_9988)) + @test Core.Intrinsics.zext_int(UInt64, x40) === 0x0000_00cc_bbaa_9988 + + x48 = Core.Intrinsics.trunc_int(TestUInt48, UInt64(0xffee_ddcc_bbaa_9988)) + @test Core.Intrinsics.zext_int(UInt64, x48) === 0x0000_ddcc_bbaa_9988 + + x5 = Core.Intrinsics.trunc_int(TestUInt5, UInt16(0xffff)) + @test Core.Intrinsics.zext_int(UInt16, x5) === 0x001f + + x17 = Core.Intrinsics.trunc_int(TestUInt17, UInt32(0xffff_ffff)) + @test Core.Intrinsics.zext_int(UInt32, x17) === 0x0001_ffff + + x63 = Core.Intrinsics.trunc_int(TestUInt63, UInt64(0xffff_ffff_ffff_ffff)) + @test Core.Intrinsics.zext_int(UInt64, x63) === 0x7fff_ffff_ffff_ffff + + i17 = Core.Intrinsics.trunc_int(TestInt17, Int32(-1)) + @test Core.Intrinsics.sext_int(Int32, i17) === Int32(-1) + i17min = Core.Intrinsics.trunc_int(TestInt17, Int32(-65536)) + @test Core.Intrinsics.sext_int(Int32, i17min) === Int32(-65536) + + i63 = Core.Intrinsics.trunc_int(TestInt63, Int64(-1)) + @test Core.Intrinsics.sext_int(Int64, i63) === Int64(-1) + + @test Core.Intrinsics.bitcast(TestUInt24, x24) === x24 + @test Core.Intrinsics.bitcast(TestUInt63, i63) === Core.Intrinsics.trunc_int(TestUInt63, typemax(UInt64)) + @test_throws ErrorException Core.Intrinsics.bitcast(TestUInt24, x40) + @test_throws ErrorException Core.Intrinsics.bitcast(TestUInt63, x24) + + chain40 = Core.Intrinsics.trunc_int(TestUInt40, UInt64(0x0000_00aa_bbcc_ddee)) + chain24 = Core.Intrinsics.trunc_int(TestUInt24, chain40) + chain17 = Core.Intrinsics.trunc_int(TestUInt17, chain24) + @test Core.Intrinsics.zext_int(UInt64, chain24) === 0x0000_0000_00cc_ddee + @test Core.Intrinsics.zext_int(UInt32, chain17) === 0x0000_ddee + + primitive type TestBits63 63 end + bits63 = Core.Intrinsics.trunc_int(TestBits63, UInt64(0xffff_ffff_ffff_ffff)) + @test repr(bits63) == "$(curmod_prefix)TestBits63(0x7fffffffffffffff)" +end + # odd-bit primitive integers keep byte-rounded storage but logical bit widths primitive type Int63 <: Signed 63 end primitive type UInt63 <: Unsigned 63 end diff --git a/test/reinterpretarray.jl b/test/reinterpretarray.jl index 71afe1e0a3696..c35aefaf6b16a 100644 --- a/test/reinterpretarray.jl +++ b/test/reinterpretarray.jl @@ -692,3 +692,49 @@ end @test reinterpret(reshape, UInt8, fill(x)) == [0x67, 0x45, 0x23, 0x01, 0xef, 0xcd, 0xab, 0x00] @test reinterpret(reshape, UInt8, [x]) == [0x67; 0x45; 0x23; 0x01; 0xef; 0xcd; 0xab; 0x00;;] end + +@testset "primitive reinterpret alignment" begin + primitive type RUInt24 24 end + primitive type RUInt40 40 end + primitive type RUInt48 48 end + primitive type RUInt17 17 end + primitive type RUInt23 23 end + primitive type RUInt63 63 end + + @test Base.ispacked(RUInt24) + @test Base.ispacked(RUInt40) + @test Base.ispacked(RUInt48) + @test Base.packedsize(RUInt24) == 3 + @test Base.packedsize(RUInt40) == 5 + @test Base.packedsize(RUInt48) == 6 + + r24 = reinterpret(RUInt24, (0xaa, 0xbb, 0xcc)) + @test reinterpret(NTuple{3, UInt8}, r24) === (0xaa, 0xbb, 0xcc) + + r40 = reinterpret(RUInt40, (0x01, 0x02, 0x03, 0x04, 0x05)) + @test reinterpret(NTuple{5, UInt8}, r40) === (0x01, 0x02, 0x03, 0x04, 0x05) + + r48 = reinterpret(RUInt48, (0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f)) + @test reinterpret(NTuple{6, UInt8}, r48) === (0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f) + + @test_throws ArgumentError Base.padding(RUInt17) + @test_throws ArgumentError Base.packedsize(RUInt17) + @test_throws ArgumentError Base.padding(RUInt23) + @test_throws ArgumentError Base.packedsize(RUInt23) + @test_throws ArgumentError Base.padding(RUInt63) + @test_throws ArgumentError Base.packedsize(RUInt63) + + @test_throws ArgumentError reinterpret(RUInt17, (0x01, 0x02, 0x03)) + @test_throws ArgumentError reinterpret(RUInt23, (0x01, 0x02, 0x03)) + @test_throws ArgumentError reinterpret(RUInt63, ntuple(i -> UInt8(i), 8)) + + struct RHasUInt17 + x::RUInt17 + y::UInt8 + end + @test_throws ArgumentError reinterpret(RHasUInt17, (0x01, 0x02, 0x03, 0x04)) + @test_throws ArgumentError reinterpret( + NTuple{4, UInt8}, + RHasUInt17(Core.Intrinsics.trunc_int(RUInt17, UInt32(1)), 0x02), + ) +end From d061c20e05111e2987f67bfede70f9935158f6e8 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Fri, 20 Mar 2026 11:08:15 +0100 Subject: [PATCH 05/15] docs: clarify primitive bit-width semantics Rewrite the primitive type description to say that the declared bit count is the logical width, while storage remains byte-rounded. This keeps the manual aligned with odd-bit primitive support and Core.bitsizeof. Co-authored-by: Codex --- doc/src/manual/types.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/src/manual/types.md b/doc/src/manual/types.md index 83dc31e4d7e27..2e5a9aa205258 100644 --- a/doc/src/manual/types.md +++ b/doc/src/manual/types.md @@ -296,13 +296,14 @@ primitive type «name» «bits» end primitive type «name» <: «supertype» «bits» end ``` -The number of bits indicates how much storage the type requires and the name gives the new type -a name. A primitive type can optionally be declared to be a subtype of some supertype. If a supertype -is omitted, then the type defaults to having `Any` as its immediate supertype. The declaration -of [`Bool`](@ref) above therefore means that a boolean value takes eight bits to store, and has -[`Integer`](@ref) as its immediate supertype. Primitive types continue to use byte-rounded storage, -so `sizeof(T)` rounds their storage size up to a whole number of bytes even when their logical width -is not a multiple of 8 bits. Use `Core.bitsizeof(T)` to query the declared logical width. +The number of bits gives the declared logical width of the primitive type, and the name gives the +new type a name. A primitive type can optionally be declared to be a subtype of some supertype. If a +supertype is omitted, then the type defaults to having `Any` as its immediate supertype. The +declaration of [`Bool`](@ref) above therefore means that a boolean value has a logical width of eight +bits, and has [`Integer`](@ref) as its immediate supertype. Primitive types continue to use +byte-rounded storage, so `sizeof(T)` rounds their storage size up to a whole number of bytes even +when their logical width is not a multiple of 8 bits. Use `Core.bitsizeof(T)` to query the declared +logical width. Non-byte primitive widths are accepted, but remain an expert-only feature. They are more likely to expose compiler, runtime, or ABI bugs than the standard built-in primitive widths, and arrays still From 553f547e5a7fd1dac8d8c2f30924de2f34e50415 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Fri, 20 Mar 2026 11:12:10 +0100 Subject: [PATCH 06/15] test: expand odd-width primitive coverage Add regressions for odd-width primitive equality and object identity masking, integer-float conversion round trips, Ref and field loads, and storage-metadata behavior. Extend reinterpret coverage for odd-width primitive layout metadata as well. Co-authored-by: Codex --- test/intrinsics.jl | 55 ++++++++++++++++++++++++++++++++++++++++ test/reinterpretarray.jl | 9 +++++++ 2 files changed, 64 insertions(+) diff --git a/test/intrinsics.jl b/test/intrinsics.jl index 37c8a42434b7e..ab575dee8dbb8 100644 --- a/test/intrinsics.jl +++ b/test/intrinsics.jl @@ -128,12 +128,67 @@ end @test_throws ErrorException Core.Intrinsics.bitcast(TestUInt24, x40) @test_throws ErrorException Core.Intrinsics.bitcast(TestUInt63, x24) + function compiled_sitofp(::Type{T}, x) where T + Core.Intrinsics.sitofp(Float64, x)::Float64 + end + function compiled_uitofp(::Type{T}, x) where T + Core.Intrinsics.uitofp(Float64, x)::Float64 + end + function compiled_fptosi(::Type{T}, x) where T + Core.Intrinsics.fptosi(T, x) + end + function compiled_fptoui(::Type{T}, x) where T + Core.Intrinsics.fptoui(T, x) + end + + si17 = Core.Intrinsics.trunc_int(TestInt17, Int32(-12345)) + @test compiled_sitofp(TestInt17, si17) === -12345.0 + @test compiled_fptosi(TestInt17, -12345.0) === si17 + + ui17 = Core.Intrinsics.trunc_int(TestUInt17, UInt32(54321)) + @test compiled_uitofp(TestUInt17, ui17) === 54321.0 + @test compiled_fptoui(TestUInt17, 54321.0) === ui17 + + si63 = Core.Intrinsics.trunc_int(TestInt63, Int64(-(Int64(1) << 52) + 3)) + @test compiled_sitofp(TestInt63, si63) === -4.503599627370493e15 + @test compiled_fptosi(TestInt63, -4.503599627370493e15) === si63 + + ui63 = Core.Intrinsics.trunc_int(TestUInt63, UInt64((UInt64(1) << 52) + 3)) + @test compiled_uitofp(TestUInt63, ui63) === 4.503599627370499e15 + @test compiled_fptoui(TestUInt63, 4.503599627370499e15) === ui63 + chain40 = Core.Intrinsics.trunc_int(TestUInt40, UInt64(0x0000_00aa_bbcc_ddee)) chain24 = Core.Intrinsics.trunc_int(TestUInt24, chain40) chain17 = Core.Intrinsics.trunc_int(TestUInt17, chain24) @test Core.Intrinsics.zext_int(UInt64, chain24) === 0x0000_0000_00cc_ddee @test Core.Intrinsics.zext_int(UInt32, chain17) === 0x0000_ddee + ref63 = Ref(ui63) + @test ref63[] === ui63 + ref63[] = x63 + @test ref63[] === x63 + + struct FieldWrap63 + x::TestUInt63 + y::UInt8 + end + field_wrap = FieldWrap63(ui63, 0x12) + @test field_wrap.x === ui63 + @test field_wrap.y === 0x12 + + struct HashWrap63 + x::TestUInt63 + end + hw1 = Ref(HashWrap63(Core.Intrinsics.trunc_int(TestUInt63, UInt64(1)))) + hw2 = Ref(HashWrap63(Core.Intrinsics.trunc_int(TestUInt63, UInt64(1)))) + GC.@preserve hw1 hw2 begin + p2 = Ptr{UInt8}(Base.unsafe_convert(Ptr{HashWrap63}, hw2)) + unsafe_store!(p2 + 7, unsafe_load(p2 + 7) | 0x80) + end + @test hw1[] === hw2[] + @test hash(hw1[]) == hash(hw2[]) + @test objectid(hw1[]) == objectid(hw2[]) + primitive type TestBits63 63 end bits63 = Core.Intrinsics.trunc_int(TestBits63, UInt64(0xffff_ffff_ffff_ffff)) @test repr(bits63) == "$(curmod_prefix)TestBits63(0x7fffffffffffffff)" diff --git a/test/reinterpretarray.jl b/test/reinterpretarray.jl index c35aefaf6b16a..f9e98e7b7d025 100644 --- a/test/reinterpretarray.jl +++ b/test/reinterpretarray.jl @@ -704,9 +704,18 @@ end @test Base.ispacked(RUInt24) @test Base.ispacked(RUInt40) @test Base.ispacked(RUInt48) + @test !Base.datatype_haspadding(RUInt24) + @test !Base.datatype_haspadding(RUInt40) + @test !Base.datatype_haspadding(RUInt48) @test Base.packedsize(RUInt24) == 3 @test Base.packedsize(RUInt40) == 5 @test Base.packedsize(RUInt48) == 6 + @test !Base.ispacked(RUInt17) + @test !Base.ispacked(RUInt23) + @test !Base.ispacked(RUInt63) + @test Base.datatype_haspadding(RUInt17) + @test Base.datatype_haspadding(RUInt23) + @test Base.datatype_haspadding(RUInt63) r24 = reinterpret(RUInt24, (0xaa, 0xbb, 0xcc)) @test reinterpret(NTuple{3, UInt8}, r24) === (0xaa, 0xbb, 0xcc) From 2d4c60fa8b07a782b857a4fb7cd12c414ee880d8 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Thu, 9 Jul 2026 22:01:38 +0200 Subject: [PATCH 07/15] test: fix Int63 intrinsics on 32-bit hosts Allow the local Int63 test constructor to accept signed integers through Int64 so default Int32 literals on 32-bit runners exercise the same truncation path. Co-authored-by: Codex --- test/intrinsics.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/intrinsics.jl b/test/intrinsics.jl index dcc2c2dd66767..0d4e6ba867d8d 100644 --- a/test/intrinsics.jl +++ b/test/intrinsics.jl @@ -198,6 +198,7 @@ end primitive type Int63 <: Signed 63 end primitive type UInt63 <: Unsigned 63 end Int63(x::Int64) = Core.Intrinsics.trunc_int(Int63, x) +Int63(x::Signed) = Core.Intrinsics.trunc_int(Int63, Int64(x)) UInt63(x::UInt64) = Core.Intrinsics.trunc_int(UInt63, x) Base.Int64(x::Int63) = Core.Intrinsics.sext_int(Int64, x) Base.UInt64(x::UInt63) = Core.Intrinsics.zext_int(UInt64, x) @@ -209,6 +210,7 @@ let x = UInt63(0xc000_ba98_8765_4321), y = Int63(-1) @test Core.bitsizeof(1.0) == 64 @test UInt64(x) === 0x4000_ba98_8765_4321 @test Int64(y) === -1 + @test Int64(Int63(Int32(-1))) === -1 # Under Revise`, this `code_llvm` query can fail in InteractiveUtils' # reflective inference path before it reaches the actual odd-bit lowering. if !isdefined(Main, :Revise) From 7dccab8e3d4e0bf7bbf16d10e81241f5096a3407 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Thu, 9 Jul 2026 22:39:28 +0200 Subject: [PATCH 08/15] simplify code --- src/builtins.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builtins.c b/src/builtins.c index 52bb21e17df76..e4bd21e7d21f7 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -101,7 +101,7 @@ static int NOINLINE compare_svec(jl_svec_t *a, jl_svec_t *b) JL_NOTSAFEPOINT static inline uint8_t last_byte_mask(jl_datatype_t *dt) JL_NOTSAFEPOINT { uint32_t unused = jl_datatype_unusedbits(dt); - return unused ? (uint8_t)(0xff >> unused) : 0xff; + return (uint8_t)(0xff >> unused); } static inline int primitive_bits_equal(const void *a, const void *b, jl_datatype_t *dt) JL_NOTSAFEPOINT From 95a06b2db6afffcc91b696e1fed74dec63cee46d Mon Sep 17 00:00:00 2001 From: Max Horn Date: Thu, 9 Jul 2026 23:18:07 +0200 Subject: [PATCH 09/15] core: fix odd-bit primitive edge cases Mask the partial storage byte when zero-extending odd-bit APInt values, and make reinterpret padding checks inspect isbits union members so odd-width primitives cannot bypass the guard. Add focused coverage for runtime odd-width zero extension and reinterpret rejection through an isbits union field. Co-authored-by: Codex --- base/reinterpretarray.jl | 1 + src/APInt.c | 2 +- src/builtins.c | 2 ++ test/intrinsics.jl | 1 + test/reinterpretarray.jl | 4 ++++ 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/base/reinterpretarray.jl b/base/reinterpretarray.jl index bf1dd9b4f4686..1696a3bbf8b5a 100644 --- a/base/reinterpretarray.jl +++ b/base/reinterpretarray.jl @@ -850,6 +850,7 @@ end if isprimitivetype(T) return Core.bitsizeof(T) % 8 != 0 end + T isa Union && return any(has_bit_padding, uniontypes(T)) for i in 1:fieldcount(T) has_bit_padding(fieldtype(T, i)) && return true end diff --git a/src/APInt.c b/src/APInt.c index f6603bbc94d1c..21cb3a1beca70 100644 --- a/src/APInt.c +++ b/src/APInt.c @@ -832,7 +832,7 @@ JL_DLLEXPORT void APInt_zext(jl_datatype_t *ty, integerPart *pa, memcpy(pr, pa, inumbytes); if (bits) { ((unsigned char *)pr)[inumbytes - 1] = - ((unsigned char *)pa)[inumbytes - 1] << bits >> bits; + ((unsigned char *)pa)[inumbytes - 1] & (unsigned char)(0xFF >> bits); } memset((char *)pr + inumbytes, 0, onumbytes - inumbytes); } diff --git a/src/builtins.c b/src/builtins.c index e4bd21e7d21f7..8dc86df8c4b99 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -159,6 +159,8 @@ static int NOINLINE compare_fields(const jl_value_t *a, const jl_value_t *b, jl_ } else { if (jl_datatype_nfields(ft) == 0) { + // Odd-bit primitives have trailing unused bits, which are + // represented as padding even though they have no fields. if (!primitive_bits_equal(ao, bo, ft)) return 0; } diff --git a/test/intrinsics.jl b/test/intrinsics.jl index 0d4e6ba867d8d..2193a112e2a4f 100644 --- a/test/intrinsics.jl +++ b/test/intrinsics.jl @@ -162,6 +162,7 @@ end chain17 = Core.Intrinsics.trunc_int(TestUInt17, chain24) @test Core.Intrinsics.zext_int(UInt64, chain24) === 0x0000_0000_00cc_ddee @test Core.Intrinsics.zext_int(UInt32, chain17) === 0x0000_ddee + @test invokelatest(Core.Intrinsics.zext_int, UInt64, x63) === 0x7fff_ffff_ffff_ffff ref63 = Ref(ui63) @test ref63[] === ui63 diff --git a/test/reinterpretarray.jl b/test/reinterpretarray.jl index 8ed463dc4c1cf..078cecb7f243c 100644 --- a/test/reinterpretarray.jl +++ b/test/reinterpretarray.jl @@ -718,7 +718,11 @@ end x::RUInt17 y::UInt8 end + struct RHasUnionUInt17 + x::Union{UInt8, RUInt17} + end @test_throws ArgumentError reinterpret(RHasUInt17, (0x01, 0x02, 0x03, 0x04)) + @test_throws ArgumentError reinterpret(RHasUnionUInt17, (0x01, 0x02, 0x03)) @test_throws ArgumentError reinterpret( NTuple{4, UInt8}, RHasUInt17(Core.Intrinsics.trunc_int(RUInt17, UInt32(1)), 0x02), From fc3b47a1c1541d1c20b3e01316aeea99fbaf9cc1 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Fri, 10 Jul 2026 00:24:12 +0200 Subject: [PATCH 10/15] Add Core.bitsizeof(MyUInt63) example to docstring --- base/essentials.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/base/essentials.jl b/base/essentials.jl index 2c8d0b85d3715..a8eae32cee5f4 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -851,6 +851,11 @@ julia> Core.bitsizeof(Float32) julia> Core.bitsizeof(1.0) 64 + +julia> primitive type MyUInt63 <: Unsigned 63 end + +julia> Core.bitsizeof(MyUInt63) +63 ``` """ Core.bitsizeof From 57f50c9388fdf6380a79042fcf67883ebafb5e76 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Fri, 10 Jul 2026 13:15:08 +0200 Subject: [PATCH 11/15] compiler: Model intrinsic logical widths Use declared primitive bit widths when modeling bitcast and integer conversion exceptions, so invalid casts remain observable and valid odd-width conversions are recognized. Co-authored-by: Codex --- Compiler/src/tfuncs.jl | 6 +++--- Compiler/test/effects.jl | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Compiler/src/tfuncs.jl b/Compiler/src/tfuncs.jl index e314430e0bedf..452f62102c535 100644 --- a/Compiler/src/tfuncs.jl +++ b/Compiler/src/tfuncs.jl @@ -3252,7 +3252,7 @@ function intrinsic_exct(𝕃::AbstractLattice, f::IntrinsicFunction, argtypes::V if !isconcrete return Union{ErrorException, TypeError} end - if !(isprimitivetype(ty) && isprimitivetype(xty) && Core.sizeof(ty) === Core.sizeof(xty)) + if !(isprimitivetype(ty) && isprimitivetype(xty) && Core.bitsizeof(ty) === Core.bitsizeof(xty)) return ErrorException end return Union{} @@ -3278,14 +3278,14 @@ function intrinsic_exct(𝕃::AbstractLattice, f::IntrinsicFunction, argtypes::V !(ty <: CORE_FLOAT_TYPES && xty <: CORE_FLOAT_TYPES && Core.sizeof(ty) > Core.sizeof(xty)) return ErrorException end - if (f === Intrinsics.sext_int || f === Intrinsics.zext_int) && !(Core.sizeof(ty) > Core.sizeof(xty)) + if (f === Intrinsics.sext_int || f === Intrinsics.zext_int) && !(Core.bitsizeof(ty) > Core.bitsizeof(xty)) return ErrorException end if f === Intrinsics.fptrunc && !(ty <: CORE_FLOAT_TYPES && xty <: CORE_FLOAT_TYPES && Core.sizeof(ty) < Core.sizeof(xty)) return ErrorException end - if f === Intrinsics.trunc_int && !(Core.sizeof(ty) < Core.sizeof(xty)) + if f === Intrinsics.trunc_int && !(Core.bitsizeof(ty) < Core.bitsizeof(xty)) return ErrorException end if (f === Intrinsics.fptoui || f === Intrinsics.fptosi) && !(xty <: CORE_FLOAT_TYPES) diff --git a/Compiler/test/effects.jl b/Compiler/test/effects.jl index 805c514d18e5d..20f2041a2dc62 100644 --- a/Compiler/test/effects.jl +++ b/Compiler/test/effects.jl @@ -1553,6 +1553,27 @@ let f = (x) -> Core.Intrinsics.trunc_int(Int16, x) @test catch_error_61435(f, Int16(0)) === :caught end +# Intrinsic width checks use logical primitive widths rather than storage sizes. +primitive type EffectsUInt17 17 end +primitive type EffectsUInt23 23 end +let f = (x) -> Core.Intrinsics.bitcast(EffectsUInt17, x) + @test !Compiler.is_nothrow(Base.infer_effects(f, (EffectsUInt23,))) + @test Base.infer_exception_type(f, (EffectsUInt23,)) === ErrorException + @test !fully_eliminated((EffectsUInt23,)) do x + f(x) + return nothing + end +end +let f = (x) -> Core.Intrinsics.zext_int(EffectsUInt23, x) + @test Compiler.is_nothrow(Base.infer_effects(f, (EffectsUInt17,))) +end +let f = (x) -> Core.Intrinsics.sext_int(EffectsUInt23, x) + @test Compiler.is_nothrow(Base.infer_effects(f, (EffectsUInt17,))) +end +let f = (x) -> Core.Intrinsics.trunc_int(EffectsUInt17, x) + @test Compiler.is_nothrow(Base.infer_effects(f, (EffectsUInt23,))) +end + # issue #57324 module Issue57324 struct T <: AbstractVector{Float64} From bd9c68e1a0e8fb3a7330aafa9b3248a5c7fc7dd7 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Fri, 10 Jul 2026 13:26:21 +0200 Subject: [PATCH 12/15] compiler: Model Core.bitsizeof Share size transfer logic with Core.sizeof and register bitsizeof for constant folding, return inference, and builtin effect analysis. Co-authored-by: Codex --- Compiler/src/tfuncs.jl | 35 +++++++++++++++++++++-------------- Compiler/test/effects.jl | 6 ++++++ Compiler/test/inference.jl | 14 ++++++++++++++ 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/Compiler/src/tfuncs.jl b/Compiler/src/tfuncs.jl index 452f62102c535..30be9d62c2906 100644 --- a/Compiler/src/tfuncs.jl +++ b/Compiler/src/tfuncs.jl @@ -524,11 +524,11 @@ function sizeof_nothrow(@nospecialize(x)) return true end -function _const_sizeof(@nospecialize(x)) +function _const_sizeof(@nospecialize(f), @nospecialize(x)) # Constant GenericMemory does not have constant size isa(x, GenericMemory) && return Int size = try - Core.sizeof(x) + f(x) catch ex # Might return # "argument is an abstract type; size is indeterminate" or @@ -538,17 +538,17 @@ function _const_sizeof(@nospecialize(x)) end return Const(size) end -@nospecs function sizeof_tfunc(𝕃::AbstractLattice, x) +@nospecs function size_tfunc(𝕃::AbstractLattice, x, f) x = widenmustalias(x) - isa(x, Const) && return _const_sizeof(x.val) - isa(x, Conditional) && return _const_sizeof(Bool) - isconstType(x) && return _const_sizeof(type_parameter(x)) + isa(x, Const) && return _const_sizeof(f, x.val) + isa(x, Conditional) && return _const_sizeof(f, Bool) + isconstType(x) && return _const_sizeof(f, type_parameter(x)) xu = unwrap_unionall(x) if isa(xu, Union) - return tmerge(sizeof_tfunc(𝕃, rewrap_unionall(xu.a, x)), - sizeof_tfunc(𝕃, rewrap_unionall(xu.b, x))) + return tmerge(size_tfunc(𝕃, rewrap_unionall(xu.a, x), f), + size_tfunc(𝕃, rewrap_unionall(xu.b, x), f)) end - # Core.sizeof operates on either a type or a value. First check which + # The size builtins operate on either a type or a value. First check which # case we're in. t, exact = instanceof_tfunc(x, false) if t !== Bottom @@ -557,18 +557,21 @@ end x = unwrap_unionall(t) if exact && isa(x, Union) isinline = uniontype_layout(x)[1] - return isinline ? Const(Int(Core.sizeof(x))) : Bottom + return isinline ? Const(Int(f(x))) : Bottom end isa(x, DataType) || return Int - (isconcretetype(x) || isprimitivetype(x)) && return _const_sizeof(x) + (isconcretetype(x) || isprimitivetype(x)) && return _const_sizeof(f, x) else x = widenconst(x) - x !== DataType && isconcretetype(x) && return _const_sizeof(x) - isprimitivetype(x) && return _const_sizeof(x) + x !== DataType && isconcretetype(x) && return _const_sizeof(f, x) + isprimitivetype(x) && return _const_sizeof(f, x) end return Int end +@nospecs sizeof_tfunc(𝕃::AbstractLattice, x) = size_tfunc(𝕃, x, Core.sizeof) +@nospecs bitsizeof_tfunc(𝕃::AbstractLattice, x) = size_tfunc(𝕃, x, Core.bitsizeof) add_tfunc(Core.sizeof, 1, 1, sizeof_tfunc, 1) +add_tfunc(Core.bitsizeof, 1, 1, bitsizeof_tfunc, 1) @nospecs function nfields_tfunc(𝕃::AbstractLattice, x) isa(x, Const) && return Const(nfields(x.val)) isa(x, Conditional) && return Const(0) @@ -2580,7 +2583,7 @@ function _builtin_nothrow(𝕃::AbstractLattice, @nospecialize(f::Builtin), argt return subtype_nothrow(𝕃, argtypes[1], argtypes[2]) elseif f === isdefined return isdefined_nothrow(𝕃, argtypes) - elseif f === Core.sizeof + elseif f === Core.sizeof || f === Core.bitsizeof na == 1 || return false return sizeof_nothrow(argtypes[1]) elseif f === Core.ifelse @@ -2632,6 +2635,7 @@ const _CONSISTENT_BUILTINS = Any[ apply_type, isa, UnionAll, + Core.bitsizeof, Core.sizeof, Core.ifelse, (<:), @@ -2659,6 +2663,7 @@ const _EFFECT_FREE_BUILTINS = [ memoryrefget, memoryref_isassigned, isdefined, + Core.bitsizeof, Core.sizeof, Core.ifelse, Core._typevar, @@ -2676,6 +2681,7 @@ const _INACCESSIBLEMEM_BUILTINS = Any[ (<:), (===), apply_type, + Core.bitsizeof, Core.ifelse, Core.sizeof, svec, @@ -2890,6 +2896,7 @@ const _EFFECTS_KNOWN_BUILTINS = Any[ # Core.memoryrefsetonce!, # Core.memoryrefswap!, memoryrefunset!, + Core.bitsizeof, Core.sizeof, svec, Core.throw_methoderror, diff --git a/Compiler/test/effects.jl b/Compiler/test/effects.jl index 20f2041a2dc62..c5d7deb6ab490 100644 --- a/Compiler/test/effects.jl +++ b/Compiler/test/effects.jl @@ -1507,6 +1507,12 @@ end @test Base.infer_effects(invokelatest, Tuple{Vararg{Any}}) == Compiler.Effects() @test Base.infer_effects(invoke, Tuple{Vararg{Any}}) == Compiler.Effects() +bitsizeof_int() = Core.bitsizeof(Int) +let effects = Base.infer_effects(bitsizeof_int) + @test Compiler.is_foldable_nothrow(effects) + @test Compiler.is_inaccessiblememonly(effects) +end + # Core._svec_ref effects modeling (required for external abstract interpreter that doesn't run optimization) let effects = Base.infer_effects((Core.SimpleVector,Int); optimize=false) do svec, i Core._svec_ref(svec, i) diff --git a/Compiler/test/inference.jl b/Compiler/test/inference.jl index 1c611df933020..15b64328966c7 100644 --- a/Compiler/test/inference.jl +++ b/Compiler/test/inference.jl @@ -1449,6 +1449,7 @@ test_const_return(()->sizeof(1 < 2), Tuple{}, 1) test_const_return(()->fieldtype(Dict{Int64,Nothing}, :age), Tuple{}, UInt) test_const_return(@eval(()->Core.sizeof($(Array{Int,0}(undef)))), Tuple{}, 2 * sizeof(Int)) test_const_return(@eval(()->Core.sizeof($(Matrix{Float32}(undef, 2, 2)))), Tuple{}, 4 * sizeof(Int)) +primitive type BitsizeofUInt17 17 end # TODO: do we want to implement these? # test_const_return(@eval(()->sizeof($(Array{Int,0}(undef)))), Tuple{}, sizeof(Int)) # test_const_return(@eval(()->sizeof($(Matrix{Float32}(undef, 2, 2)))), Tuple{}, 4 * 2 * 2) @@ -1460,6 +1461,15 @@ function sizeof_typeref(typeref) end @test @inferred(sizeof_typeref(Ref{DataType}(Int))) == sizeof(Int) @test find_call(only(code_typed(sizeof_typeref, (Ref{DataType},)))[1], Core.sizeof, 2) +# Make sure Core.bitsizeof with a ::DataType as inferred input type is inferred but not constant. +function bitsizeof_typeref(typeref) + return Core.bitsizeof(typeref[]) +end +@test bitsizeof_typeref(Ref{DataType}(BitsizeofUInt17)) == 17 +let (src, rt) = only(code_typed(bitsizeof_typeref, (Ref{DataType},))) + @test rt === Int + @test find_call(src, Core.bitsizeof, 2) +end # Constant `Vector` can be resized and shouldn't be optimized to a constant. const constvec = [1, 2, 3] @eval function sizeof_constvec() @@ -1642,10 +1652,14 @@ let nfields_tfunc(@nospecialize xs...) = Compiler.nfields_tfunc(Compiler.fallback_lattice, xs...) sizeof_tfunc(@nospecialize xs...) = Compiler.sizeof_tfunc(Compiler.fallback_lattice, xs...) + bitsizeof_tfunc(@nospecialize xs...) = + Compiler.bitsizeof_tfunc(Compiler.fallback_lattice, xs...) sizeof_nothrow(@nospecialize xs...) = Compiler.sizeof_nothrow(xs...) @test sizeof_tfunc(Const(Ptr)) === sizeof_tfunc(Union{Ptr, Int, Type{Ptr{Int8}}, Type{Int}}) === Const(Sys.WORD_SIZE ÷ 8) @test sizeof_tfunc(Type{Ptr}) === Const(sizeof(Ptr)) + @test bitsizeof_tfunc(Type{BitsizeofUInt17}) === Const(17) + @test bitsizeof_tfunc(DataType) === Int @test !sizeof_nothrow(Union{Ptr, Int, Type{Ptr{Int8}}, Type{Int}}) @test sizeof_nothrow(Union{Ptr, Int, Core.TypeEgal{Ptr{Int8}}, Core.TypeEgal{Int}}) @test sizeof_nothrow(Const(Ptr)) From 8bebe5cac6db16fe5a2741740db060486ea8e741 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Sat, 11 Jul 2026 09:47:21 +0200 Subject: [PATCH 13/15] show: Pad partial primitive bytes Pad the leading partial byte to its logical hexadecimal width so low-valued 5-, 6-, and 7-bit primitives keep a stable representation. Co-authored-by: Codex --- base/show.jl | 2 +- test/intrinsics.jl | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/base/show.jl b/base/show.jl index 94b7dee2f9c02..6d4adc1154c2c 100644 --- a/base/show.jl +++ b/base/show.jl @@ -533,7 +533,7 @@ function _show_default(io::IO, @nospecialize(x)) byte = unsafe_load(convert(Ptr{UInt8}, p + i)) if i == nbytes - 1 && nbits % 8 != 0 byte &= (UInt8(1) << (nbits % 8)) - UInt8(1) - print(io, string(byte, base = 16)) + print(io, string(byte, base = 16, pad = cld(nbits % 8, 4))) else print(io, string(byte, base = 16, pad = 2)) end diff --git a/test/intrinsics.jl b/test/intrinsics.jl index 2193a112e2a4f..3b27cac81b9da 100644 --- a/test/intrinsics.jl +++ b/test/intrinsics.jl @@ -193,6 +193,16 @@ end primitive type TestBits63 63 end bits63 = Core.Intrinsics.trunc_int(TestBits63, UInt64(0xffff_ffff_ffff_ffff)) @test repr(bits63) == "$(curmod_prefix)TestBits63(0x7fffffffffffffff)" + + primitive type TestBits5 5 end + primitive type TestBits6 6 end + primitive type TestBits7 7 end + @test repr(Core.Intrinsics.trunc_int(TestBits5, UInt8(1))) == + "$(curmod_prefix)TestBits5(0x01)" + @test repr(Core.Intrinsics.trunc_int(TestBits6, UInt8(1))) == + "$(curmod_prefix)TestBits6(0x01)" + @test repr(Core.Intrinsics.trunc_int(TestBits7, UInt8(1))) == + "$(curmod_prefix)TestBits7(0x01)" end # odd-bit primitive integers keep byte-rounded storage but logical bit widths From 263277ac9772df2ad7f575bd5ef63919cbca1dff Mon Sep 17 00:00:00 2001 From: Max Horn Date: Sat, 11 Jul 2026 10:07:35 +0200 Subject: [PATCH 14/15] comments --- Compiler/src/tfuncs.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Compiler/src/tfuncs.jl b/Compiler/src/tfuncs.jl index 30be9d62c2906..f12a75555bfd8 100644 --- a/Compiler/src/tfuncs.jl +++ b/Compiler/src/tfuncs.jl @@ -524,6 +524,7 @@ function sizeof_nothrow(@nospecialize(x)) return true end +# f shall be Core.sizeof or Core.bitsizeof function _const_sizeof(@nospecialize(f), @nospecialize(x)) # Constant GenericMemory does not have constant size isa(x, GenericMemory) && return Int @@ -548,8 +549,8 @@ end return tmerge(size_tfunc(𝕃, rewrap_unionall(xu.a, x), f), size_tfunc(𝕃, rewrap_unionall(xu.b, x), f)) end - # The size builtins operate on either a type or a value. First check which - # case we're in. + # Core.sizeof or Core.bitsizeof operate on either a type or a value. + # First check which case we're in. t, exact = instanceof_tfunc(x, false) if t !== Bottom # The value corresponding to `x` at runtime could be a type. From 4d43c94a379baddf240ab6caa6808e3a253785f8 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Sat, 11 Jul 2026 17:36:22 +0200 Subject: [PATCH 15/15] test: Fix Int63 expectations on 32-bit hosts Make the expected values explicitly Int64 so identity comparisons do not depend on the host-sized Int literal type. CI: https://buildkite.com/julialang/julia-pr/builds/333#019f5038-9b13-473b-9287-582d1b7113b4 Co-authored-by: Codex --- test/intrinsics.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/intrinsics.jl b/test/intrinsics.jl index 3b27cac81b9da..1f2f27eaf82fa 100644 --- a/test/intrinsics.jl +++ b/test/intrinsics.jl @@ -220,8 +220,8 @@ let x = UInt63(0xc000_ba98_8765_4321), y = Int63(-1) @test Core.bitsizeof(Int64) == 64 @test Core.bitsizeof(1.0) == 64 @test UInt64(x) === 0x4000_ba98_8765_4321 - @test Int64(y) === -1 - @test Int64(Int63(Int32(-1))) === -1 + @test Int64(y) === Int64(-1) + @test Int64(Int63(Int32(-1))) === Int64(-1) # Under Revise`, this `code_llvm` query can fail in InteractiveUtils' # reflective inference path before it reaches the actual odd-bit lowering. if !isdefined(Main, :Revise)