diff --git a/CHANGELOG.md b/CHANGELOG.md index 2437bc0a772..729b697cfe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,14 @@ To learn more about the feature, check out the - Added `List.unmodifiableOf` with better typing than `List.unmodifiable`. - Added `Map.unmodifiableOf` with better typing than `Map.unmodifiable`. +#### `dart:core` + +- Added two getters on `int` for efficient bit-counting: + `trailingZeroBitCount` (ctz) and `oneBitCount` (popcount). On native + platforms they operate on the full 64-bit two's-complement + representation; on the web they operate on the least-significant 32 + bits. See [#52673](https://github.com/dart-lang/sdk/issues/52673). + #### `dart:io` - The cookie-date parser now uses the correct algorithm again. diff --git a/pkg/dart2wasm/lib/intrinsics.dart b/pkg/dart2wasm/lib/intrinsics.dart index fe20cac65f1..bbdfbcdc2a4 100644 --- a/pkg/dart2wasm/lib/intrinsics.dart +++ b/pkg/dart2wasm/lib/intrinsics.dart @@ -629,6 +629,21 @@ class Intrinsifier { return w.NumType.i64; } + // int.trailingZeroBitCount + if (cls == translator.coreTypes.intClass && + name == 'trailingZeroBitCount') { + codeGen.translateExpression(receiver, w.NumType.i64); + b.i64_ctz(); + return w.NumType.i64; + } + + // int.oneBitCount + if (cls == translator.coreTypes.intClass && name == 'oneBitCount') { + codeGen.translateExpression(receiver, w.NumType.i64); + b.i64_popcnt(); + return w.NumType.i64; + } + return null; } diff --git a/runtime/lib/integers.cc b/runtime/lib/integers.cc index 8df26e4fa94..603b2957d5f 100644 --- a/runtime/lib/integers.cc +++ b/runtime/lib/integers.cc @@ -272,6 +272,26 @@ DEFINE_NATIVE_ENTRY(Smi_bitLength, 0, 1) { return Smi::New(result); } +// Unified bit-count natives. Receiver is _IntegerImplementation, so the +// operand can be either Smi or Mint at runtime. +DEFINE_NATIVE_ENTRY(Integer_trailingZeroBitCount, 0, 1) { + const Integer& operand = + Integer::CheckedHandle(zone, arguments->NativeArgAt(0)); + intptr_t result = + Utils::CountTrailingZeros64(static_cast(operand.Value())); + ASSERT(Smi::IsValid(result)); + return Smi::New(result); +} + +DEFINE_NATIVE_ENTRY(Integer_oneBitCount, 0, 1) { + const Integer& operand = + Integer::CheckedHandle(zone, arguments->NativeArgAt(0)); + intptr_t result = + Utils::CountOneBits64(static_cast(operand.Value())); + ASSERT(Smi::IsValid(result)); + return Smi::New(result); +} + // Should be kept in sync with il_*.cc EmitHashIntegerCodeSequence uint32_t Multiply64Hash(int64_t ivalue) { const uint64_t magic_constant = /*0x1b873593cc9e*/ 0x2d51; diff --git a/runtime/vm/bootstrap_natives.h b/runtime/vm/bootstrap_natives.h index 2ca92bc364d..2b6a201e309 100644 --- a/runtime/vm/bootstrap_natives.h +++ b/runtime/vm/bootstrap_natives.h @@ -66,6 +66,8 @@ namespace dart { V(SendPort_sendInternal_, 2) \ V(Smi_bitNegate, 1) \ V(Smi_bitLength, 1) \ + V(Integer_trailingZeroBitCount, 1) \ + V(Integer_oneBitCount, 1) \ V(SuspendState_instantiateClosureWithFutureTypeArgument, 2) \ V(Mint_bitNegate, 1) \ V(Mint_bitLength, 1) \ diff --git a/sdk/lib/_internal/js_dev_runtime/private/js_number.dart b/sdk/lib/_internal/js_dev_runtime/private/js_number.dart index 0a1db6541dc..5278910eee6 100644 --- a/sdk/lib/_internal/js_dev_runtime/private/js_number.dart +++ b/sdk/lib/_internal/js_dev_runtime/private/js_number.dart @@ -458,6 +458,26 @@ final class JSNumber extends Interceptor return JS('!', 'Math.clz32(#)', uint32); } + @notNull + int get trailingZeroBitCount { + int v = JS('!', '# | 0', this); + if (v == 0) return 32; + return JS('!', '31 - Math.clz32(# & -#)', v, v); + } + + @notNull + int get oneBitCount { + // The number of bits set in the least significant 32 bits of `this`, + // also known as the "Hamming weight". See: + // https://en.wikipedia.org/wiki/Hamming_weight for an explanation of the + // following algorithm. + int v = JS('!', '# | 0', this); + v -= (v >>> 1) & 0x5555_5555; + v = (v & 0x3333_3333) + ((v >>> 2) & 0x3333_3333); + v = (v + (v >>> 4)) & 0x0f0f_0f0f; + return JS('!', '(Math.imul(#, 0x01010101) >>> 24)', v); + } + // Returns pow(this, e) % m. @notNull int modPow(@nullCheck int e, @nullCheck int m) { diff --git a/sdk/lib/_internal/js_runtime/lib/js_number.dart b/sdk/lib/_internal/js_runtime/lib/js_number.dart index 33572a33827..3895fe1a259 100644 --- a/sdk/lib/_internal/js_runtime/lib/js_number.dart +++ b/sdk/lib/_internal/js_runtime/lib/js_number.dart @@ -547,6 +547,24 @@ final class JSInt extends JSNumber implements int, TrustedGetRuntimeType { return JS('JSUInt31', 'Math.clz32(#)', uint32); } + int get trailingZeroBitCount { + int v = JS('int', '# | 0', this); + if (v == 0) return 32; + return JS('JSUInt31', '31 - Math.clz32(# & -#)', v, v); + } + + int get oneBitCount { + // The number of bits set in the least significant 32 bits of `this`, + // also known as the "Hamming weight". See: + // https://en.wikipedia.org/wiki/Hamming_weight for an explanation of the + // following algorithm. + int v = JS('int', '# | 0', this); + v -= (v >>> 1) & 0x5555_5555; + v = (v & 0x3333_3333) + ((v >>> 2) & 0x3333_3333); + v = (v + (v >>> 4)) & 0x0f0f_0f0f; + return JS('JSUInt31', '(Math.imul(#, 0x01010101) >>> 24)', v); + } + // Returns pow(this, e) % m. int modPow(int e, int m) { if (e is! int) { diff --git a/sdk/lib/_internal/vm/lib/integers.dart b/sdk/lib/_internal/vm/lib/integers.dart index ad9a38f8646..80dcd34ad4b 100644 --- a/sdk/lib/_internal/vm/lib/integers.dart +++ b/sdk/lib/_internal/vm/lib/integers.dart @@ -162,6 +162,15 @@ abstract final class _IntegerImplementation implements int { @pragma("vm:exact-result-type", bool) @pragma("vm:external-name", "Integer_equalToInteger") external bool _equalToInteger(int other); + + @pragma("vm:exact-result-type", "dart:core#_Smi") + @pragma("vm:external-name", "Integer_trailingZeroBitCount") + external int get trailingZeroBitCount; + + @pragma("vm:exact-result-type", "dart:core#_Smi") + @pragma("vm:external-name", "Integer_oneBitCount") + external int get oneBitCount; + int abs() { return this < 0 ? -this : this; } diff --git a/sdk/lib/_internal/wasm/lib/boxed_int.dart b/sdk/lib/_internal/wasm/lib/boxed_int.dart index e7526e40b51..be8ac453e5a 100644 --- a/sdk/lib/_internal/wasm/lib/boxed_int.dart +++ b/sdk/lib/_internal/wasm/lib/boxed_int.dart @@ -432,6 +432,8 @@ final class BoxedInt implements int { @pragma("wasm:intrinsic") external int operator ~(); external int get bitLength; + external int get trailingZeroBitCount; + external int get oneBitCount; @override external String toString(); diff --git a/sdk/lib/core/int.dart b/sdk/lib/core/int.dart index a1a2b686376..7de0a251710 100644 --- a/sdk/lib/core/int.dart +++ b/sdk/lib/core/int.dart @@ -256,6 +256,46 @@ abstract final class int extends num { /// ``` int get bitLength; + /// The number of trailing (least significant) zero bits in the binary + /// representation of this integer. + /// + /// On JavaScript platforms, only the least significant 32 bits are used. + /// On native platforms, the 64-bit signed integer is used directly. + /// + /// The trailing zero-bit count is the position of the least significant + /// 1-bit in the binary representation of the integer. If the integer is + /// zero, the value is the size of integer that the platform uses for bit + /// operations (64-bit on native, 32-bit on the web). + /// ```dart + /// 1.trailingZeroBitCount; // 0 + /// 2.trailingZeroBitCount; // 1 + /// 8.trailingZeroBitCount; // 3 + /// 0.trailingZeroBitCount; // 64 on native, 32 on the web + /// ``` + @Since("3.13") + int get trailingZeroBitCount; + + /// The number of `1` bits in the binary representation of this integer. + /// + /// On JavaScript platforms, only the least significant 32 bits are used. + /// On native platforms, the 64-bit signed integer is used directly. + /// + /// The one-bit count is the number of `1` digits in the binary + /// representation of that integer. A negative integer has one-digits up to + /// the size of integer that the platform uses for bit operations (64-bit + /// on native, 32-bit on the web). + /// The value of `n.oneBitCount + (~n).oneBitCount` is always the size the + /// platform uses for bit operations (on the web, at least if the value + /// starts out as a 32-bit integer). + /// ```dart + /// 0.oneBitCount; // 0 + /// 1.oneBitCount; // 1 + /// 7.oneBitCount; // 3 + /// (-1).oneBitCount; // 64 on native, 32 on the web + /// ``` + @Since("3.13") + int get oneBitCount; + /// Returns the least significant [width] bits of this integer as a /// non-negative number (i.e. unsigned representation). The returned value has /// zeros in all bit positions higher than [width]. diff --git a/tests/corelib/int_bit_count_test.dart b/tests/corelib/int_bit_count_test.dart new file mode 100644 index 00000000000..6d0322d6732 --- /dev/null +++ b/tests/corelib/int_bit_count_test.dart @@ -0,0 +1,239 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// +// Testing int.trailingZeroBitCount and int.oneBitCount. + +import "package:expect/expect.dart"; +import "package:expect/variations.dart" show jsNumbers; + +// Platform width for bit operations: 64 with native integer semantics +// (VM, dart2wasm), 32 with JS-number semantics (dart2js, DDC). +const int width = jsNumbers ? 32 : 64; + +// `js == null` means the case is only meaningful on a native (64-bit) +// implementation and the JS assertion is skipped. +void checkTrailing(int i, int native, int? js) { + final expected = jsNumbers ? js : native; + if (expected == null) return; + Expect.equals(expected, i.trailingZeroBitCount, '$i.trailingZeroBitCount'); +} + +void checkOne(int i, int native, int? js) { + final expected = jsNumbers ? js : native; + if (expected == null) return; + Expect.equals(expected, i.oneBitCount, '$i.oneBitCount'); +} + +void testTrailingZeroBitCount() { + // Zero: trailing count equals full platform width. + checkTrailing(0, 64, 32); + + // Positive values. + checkTrailing(1, 0, 0); + checkTrailing(2, 1, 1); + checkTrailing(3, 0, 0); + checkTrailing(4, 2, 2); + checkTrailing(8, 3, 3); + checkTrailing(0x10, 4, 4); + checkTrailing(4096, 12, 12); + checkTrailing(0x8000_0000, 31, 31); + checkTrailing(0x7fff_ffff, 0, 0); + + // Negative values: two's complement preserves low bits. + checkTrailing(-1, 0, 0); + checkTrailing(-2, 1, 1); + checkTrailing(-4, 2, 2); + checkTrailing(-1024, 10, 10); + checkTrailing(-0x4000_0000, 30, 30); + checkTrailing(-0x8000_0000, 31, 31); + + // Values above the 32-bit range. On JS the operation is defined as + // counting trailing zeros of the low 32 bits, so any value whose low + // 32 bits are zero yields the JS platform width (32) regardless of + // what's above. + checkTrailing(0x1_0000_0000, 32, 32); + checkTrailing(0x2_0000_0000, 33, 32); + checkTrailing(0x4_0000_0000, 34, 32); + checkTrailing(0x100_0000_0000, 40, 32); + checkTrailing(0x8000_0000_0000_0000, 63, 32); + + // 64-bit-range values whose low 32 bits are non-zero: the JS result + // is determined entirely by the low half. + checkTrailing(0x1_0000_0001, 0, 0); + checkTrailing(0x2_0000_0080, 7, 7); + + // Near 2^63, JS doubles only have enough precision for values that + // differ by 2048 (= 2^11), so consecutive integers can't all be + // represented. + // `2^63 + 4096` is exactly representable. + // `2^63 + 4095` rounds up to the same value. + checkTrailing(0x8000_0000_0000_0000 + 4096, 12, 12); + checkTrailing(0x8000_0000_0000_0000 + 4095, 0, 12); +} + +void testOneBitCount() { + checkOne(0, 0, 0); + checkOne(1, 1, 1); + checkOne(2, 1, 1); + checkOne(3, 2, 2); + checkOne(7, 3, 3); + checkOne(0x55, 4, 4); + checkOne(0xff, 8, 8); + checkOne(0xffff_ffff, 32, 32); + + // Negative values: sign-extend to platform width. + checkOne(-1, 64, 32); + checkOne(-2, 63, 31); + checkOne(-3, 63, 31); + checkOne(~0x55, 60, 28); + checkOne(-0x5555_5555, 49, 17); + checkOne(-0x7fff_ffff, 34, 2); + checkOne(-0x8000_0000, 33, 1); + + // Values above the 32-bit range. JS counts only the low 32 bits, so + // a single bit above bit 31 is invisible to JS oneBitCount. + checkOne(0x1_0000_0000, 1, 0); + checkOne(0x8000_0000_0000_0000, 1, 0); + checkOne(0x2_0000_0001, 2, 1); + checkOne(0x1_0000_FFFF, 17, 16); + + // `0x5555_5555_0000_0000 + 0x5555_5555` constructs + // 0x5555_5555_5555_5555 on native, but the runtime addition overflows + // JS double precision so the result on JS is unpredictable. Only the + // native expectation is asserted. + if (!jsNumbers) { + final pattern = 0x5555_5555_0000_0000 + 0x5555_5555; + checkOne(pattern, 32, null); + checkOne(~0x8000_0000_0000_0000, 63, null); + // Setting any odd-position bit on the alternating pattern should + // raise the count from 32 to 33, exercising the 64-bit popcount path + // across all positions. + for (int i = 1; i < 64; i += 2) { + Expect.equals( + 33, + (pattern | (1 << i)).oneBitCount, + '(pattern | (1<<$i)).oneBitCount', + ); + } + } +} + +// Exhaustive single-bit coverage across the full platform width. +void testSingleBitCoverage() { + for (int b = 0; b < width; b++) { + final n = 1 << b; + Expect.equals(b, n.trailingZeroBitCount, '(1<<$b).trailingZeroBitCount'); + Expect.equals(1, n.oneBitCount, '(1<<$b).oneBitCount'); + } +} + +// Dart-on-JS guarantees that bit operations performed on unsigned 32-bit +// values produce the same answer as on a native 64-bit Dart implementation. +// Verify the new getters honor that for a representative set of inputs that +// span the full 32-bit unsigned range. +void testUnsigned32BitConsistency() { + const cases = <(int, int, int)>[ + (0x0000_0001, 0, 1), + (0x0000_0002, 1, 1), + (0x0000_0080, 7, 1), + (0x0000_FFFF, 0, 16), + (0x5555_5555, 0, 16), + (0xAAAA_AAAA, 1, 16), + (0xCCCC_CCCC, 2, 16), + (0xF0F0_F0F0, 4, 16), + (0x4000_0000, 30, 1), + (0x4000_0001, 0, 2), + (0x8000_0000, 31, 1), + (0x8000_0001, 0, 2), + (0xC000_0000, 30, 2), + (0xFFFF_FFFE, 1, 31), + (0xFFFF_FFFF, 0, 32), + ]; + for (final (n, tzc, obc) in cases) { + Expect.equals(tzc, n.trailingZeroBitCount, '$n.trailingZeroBitCount'); + Expect.equals(obc, n.oneBitCount, '$n.oneBitCount'); + } +} + +void testIdentities() { + // n.oneBitCount + (~n).oneBitCount == platform width. + for (final n in const [ + 0, + 1, + 2, + 7, + 42, + 0x7fff_ffff, + 0x8000_0000, + 0xffff_ffff, + -1, + -2, + -42, + ]) { + Expect.equals( + width, + n.oneBitCount + (~n).oneBitCount, + '$n.oneBitCount + ~$n.oneBitCount', + ); + } + + // Cross-check: for any nonzero n, `(n & -n) - 1` is a mask of exactly + // `trailingZeroBitCount(n)` ones, so counting them recovers that count. + // Exercises both getters against each other. + void checkIdentity(int n) { + Expect.equals( + ((n & -n) - 1).oneBitCount, + n.trailingZeroBitCount, + '(($n & -$n) - 1).oneBitCount == $n.trailingZeroBitCount', + ); + } + + // Small values + unsigned 32-bit boundaries + sign-extended negatives. + // Identity holds on every backend. + for (final n in const [ + 1, + 2, + 3, + 7, + 42, + 0x4000_0000, + 0x8000_0000, + 0xffff_ffff, + -1, + -2, + -42, + ]) { + checkIdentity(n); + } + + // Values above the 32-bit range but within JS double mantissa precision + // (≤ 2^52). Under Dart's "operate on the low 32 bits" web semantics + // both sides of the identity collapse to 32, so the identity still + // holds on dart2js / DDC. + for (final n in const [ + 0x1_0000_0000, // 2^32 + 0x100_0000_0000, // 2^40 + 0x10_0000_0000_0000, // 2^52 + ]) { + checkIdentity(n); + } + + // Native-only: values whose source bit pattern is not preserved by JS + // doubles. Exercised only on backends with native 64-bit ints. The + // `+ 1` form is used to keep `0x20_0000_0000_0000` as the literal, + // since the dart2js analyzer rejects literals that can't be + // represented exactly as a JS Number. + if (!jsNumbers) { + checkIdentity(0x20_0000_0000_0000 + 1); // 2^53 + 1 + checkIdentity(0x8000_0000_0000_0000); // 2^63 + } +} + +void main() { + testTrailingZeroBitCount(); + testOneBitCount(); + testSingleBitCoverage(); + testUnsigned32BitConsistency(); + testIdentities(); +}