From 754239b077e0f18fa5a82b10c32404cc0aa482b8 Mon Sep 17 00:00:00 2001 From: Modestas Valauskas Date: Tue, 12 May 2026 05:49:31 -0700 Subject: [PATCH] [core] Add trailingZeroBitCount and oneBitCount to int Adds two new getters to int for 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. Implementations: - VM: unified C++ natives Integer_trailingZeroBitCount / Integer_oneBitCount on _IntegerImplementation, using Utils::CountTrailingZeros64 and Utils::CountOneBits64. The receiver may be _Smi or _Mint at runtime. - dart2js / DDC: clz32-based ctz and a SWAR popcount. - dart2wasm: inlined i64.ctz and i64.popcnt intrinsics. leadingZeroBitCount (clz) is intentionally excluded from this CL: its result depends on the platform integer width (e.g. 1.leadingZeroBitCount is 31 on web, 63 on native), and the same value can be derived from the existing bitLength getter when needed. Asm intrinsification on native architectures is intentionally left for a separate follow-up CL. Work towards https://github.com/dart-lang/sdk/issues/6486 (this CL covers popcount and ctz from the bit-twiddling list; clz, rotate, reverse, and others remain). Work towards https://github.com/dart-lang/sdk/issues/1053 (efficient BitSet implementation). Bug: https://github.com/dart-lang/sdk/issues/52673 Bug: https://github.com/dart-lang/sdk/issues/38346 TEST=tests/corelib/int_bit_count_test Change-Id: I8a5cdb5c91360478f47bbd6b9c84ca1c477aa8c7 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/498041 Reviewed-by: Slava Egorov Commit-Queue: Slava Egorov Reviewed-by: Stephen Adams Reviewed-by: Martin Kustermann Auto-Submit: Modestas Valauskas Reviewed-by: Lasse Nielsen --- CHANGELOG.md | 8 + pkg/dart2wasm/lib/intrinsics.dart | 15 ++ runtime/lib/integers.cc | 20 ++ runtime/vm/bootstrap_natives.h | 2 + .../js_dev_runtime/private/js_number.dart | 20 ++ .../_internal/js_runtime/lib/js_number.dart | 18 ++ sdk/lib/_internal/vm/lib/integers.dart | 9 + sdk/lib/_internal/wasm/lib/boxed_int.dart | 2 + sdk/lib/core/int.dart | 40 +++ tests/corelib/int_bit_count_test.dart | 239 ++++++++++++++++++ 10 files changed, 373 insertions(+) create mode 100644 tests/corelib/int_bit_count_test.dart 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(); +}