Skip to content

Commit 38b34e1

Browse files
committed
Prepare v1.0.0 release
1 parent 23f2d8a commit 38b34e1

9 files changed

Lines changed: 112 additions & 51 deletions

File tree

.github/dependabot.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
version: 2
22
updates:
3+
- package-ecosystem: github-actions
4+
directory: "/"
5+
schedule:
6+
interval: weekly
37
- package-ecosystem: mix
48
directory: "/"
59
schedule:

.github/workflows/test.yml

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,72 @@ name: test
33
on:
44
pull_request:
55
push:
6-
branches:
6+
branches:
77
- master
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
812

913
jobs:
1014
mix_test:
11-
name: mix test (OTP ${{matrix.otp}} | Elixir ${{matrix.elixir}})
12-
13-
env:
14-
MIX_ENV: test
15+
name: mix test (OTP ${{ matrix.otp }} | Elixir ${{ matrix.elixir }})
1516

1617
strategy:
18+
fail-fast: false
1719
matrix:
1820
include:
19-
- elixir: 1.14.3
20-
otp: 25.1
21-
lint: true
22-
installer: true
21+
- elixir: "1.14"
22+
otp: "25"
23+
- elixir: "1.20"
24+
otp: "29"
25+
26+
runs-on: ubuntu-latest
27+
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@v7.0.1
31+
32+
- name: Set up Elixir
33+
uses: erlef/setup-beam@v1.24.1
34+
with:
35+
elixir-version: ${{ matrix.elixir }}
36+
otp-version: ${{ matrix.otp }}
37+
38+
- name: Install test dependencies
39+
run: MIX_ENV=test mix deps.get
2340

41+
- name: Compile with warnings as errors
42+
run: MIX_ENV=test mix compile --warnings-as-errors
2443

44+
- name: Run tests
45+
run: MIX_ENV=test mix test
46+
47+
release_checks:
48+
name: release checks
2549
runs-on: ubuntu-latest
2650

2751
steps:
28-
- name: Checkout
29-
uses: actions/checkout@v3
30-
- name: Set up Elixir
31-
uses: erlef/setup-beam@v1
32-
with:
33-
elixir-version: ${{matrix.elixir}}
34-
otp-version: ${{matrix.otp}}
35-
36-
- name: Install Dependencies
37-
run: mix deps.get --only test
38-
39-
- name: Run Tests
40-
run: mix test
52+
- name: Checkout
53+
uses: actions/checkout@v7.0.1
54+
55+
- name: Set up Elixir
56+
uses: erlef/setup-beam@v1.24.1
57+
with:
58+
elixir-version: "1.20"
59+
otp-version: "29"
60+
61+
- name: Install development dependencies
62+
run: mix deps.get
63+
64+
- name: Check formatting
65+
run: mix format --check-formatted
66+
67+
- name: Compile with warnings as errors
68+
run: mix compile --warnings-as-errors
69+
70+
- name: Build documentation
71+
run: mix docs
72+
73+
- name: Build Hex package
74+
run: mix hex.build --output /tmp/abit-1.0.0.tar

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# Changelog for Abit
22

3-
## Unreleased
3+
## v1.0.0 - 2026-08-20
44

5+
* Stability - Declared the existing public API stable for the first major release.
6+
* Breaking - Raised the minimum supported versions to Elixir 1.14 and OTP 25.
57
* Performance - Reduced overhead in bit-array set operations, atomics membership checks, and packed-counter access.
68
* Fix - Added explicit validation and errors for malformed atomics serialization payloads.
79
* Tests - Added property-based coverage for bitmasks, atomics operations and serialization, and packed counters.
8-
* Documentation - Updated installation instructions, package metadata, and Erlang documentation links.
10+
* Documentation - Updated installation instructions, package metadata, Erlang documentation links, and the 64-bit bitmask contract.
911

1012
## v0.4.0
1113
* Feature - Added fast atomics resetting via `Abit.clear/1` and `Abit.Counter.clear/1`.

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ See the [API documentation](https://hexdocs.pm/abit) on HexDocs.
88

99
## Installation
1010

11-
Abit requires Elixir 1.7 or later and OTP 21.2.1 or later.
11+
Abit requires Elixir 1.14 or later and OTP 25 or later.
1212

1313
Add `abit` to your list of dependencies in `mix.exs`:
1414

1515
```elixir
1616
def deps do
1717
[
18-
{:abit, "~> 0.4"}
18+
{:abit, "~> 1.0"}
1919
]
2020
end
2121
```
@@ -60,6 +60,11 @@ Operations that write to an `:atomics` reference mutate it in place. See the
6060
* `Abit.Counter.get_all_at_atomic/2` - Returns all counters packed into the atomics element at the given index.
6161

6262
### Abit.Bitmask - helper functions for bitmasks
63+
64+
Population counts and Hamming distances cover only the lowest 64 bits. Indexed
65+
operations can address higher bit positions, while `to_list/2` uses its explicit
66+
size argument.
67+
6368
* `Abit.Bitmask.set_bits_count/1` - Returns the number of bits set to 1 in the given integer.
6469
* `Abit.Bitmask.bit_at/2` - Returns the bit at a given position in the given integer.
6570
* `Abit.Bitmask.set_bit_at/3` - Sets a bit in the given integer at the given position to a given bit (0 or 1).

lib/abit/bitmask.ex

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
defmodule Abit.Bitmask do
22
@moduledoc """
33
Functions for working with bits and integer bitmasks.
4+
5+
`set_bits_count/1` and `hamming_distance/2` treat integers as 64-bit
6+
bitmasks: they inspect bits 0 through 63 and ignore higher bits. Negative
7+
integers are interpreted using their lowest 64 two's-complement bits.
8+
9+
The indexed operations are not capped at 64 bits, and `to_list/2` uses the
10+
explicit size supplied by the caller.
411
"""
512

613
import Bitwise
714

815
@doc """
916
Returns the count of bits set to 1 in the given integer `int`.
1017
18+
Only the lowest 64 bits are counted. Bits above bit 63 are ignored, and
19+
negative integers are interpreted using their lowest 64 two's-complement
20+
bits.
21+
1122
## Examples
1223
1324
iex> Abit.Bitmask.set_bits_count(3)
@@ -19,21 +30,22 @@ defmodule Abit.Bitmask do
1930
iex> Abit.Bitmask.set_bits_count(1023)
2031
10
2132
"""
22-
@popcount_table (for i <- 0..255,
23-
do:
24-
for(<<b::1 <- <<i::8>> >>, b == 1, reduce: 0, do: (acc -> acc + 1)))
33+
@popcount_table for(
34+
i <- 0..255,
35+
do: for(<<(b::1 <- <<i::8>>)>>, b == 1, reduce: 0, do: (acc -> acc + 1))
36+
)
2537
|> List.to_tuple()
2638

2739
@spec set_bits_count(integer) :: non_neg_integer
2840
def set_bits_count(int) when is_integer(int) do
2941
elem(@popcount_table, int &&& 255) +
30-
elem(@popcount_table, (int >>> 8) &&& 255) +
31-
elem(@popcount_table, (int >>> 16) &&& 255) +
32-
elem(@popcount_table, (int >>> 24) &&& 255) +
33-
elem(@popcount_table, (int >>> 32) &&& 255) +
34-
elem(@popcount_table, (int >>> 40) &&& 255) +
35-
elem(@popcount_table, (int >>> 48) &&& 255) +
36-
elem(@popcount_table, (int >>> 56) &&& 255)
42+
elem(@popcount_table, int >>> 8 &&& 255) +
43+
elem(@popcount_table, int >>> 16 &&& 255) +
44+
elem(@popcount_table, int >>> 24 &&& 255) +
45+
elem(@popcount_table, int >>> 32 &&& 255) +
46+
elem(@popcount_table, int >>> 40 &&& 255) +
47+
elem(@popcount_table, int >>> 48 &&& 255) +
48+
elem(@popcount_table, int >>> 56 &&& 255)
3749
end
3850

3951
@doc """
@@ -101,6 +113,10 @@ defmodule Abit.Bitmask do
101113
Returns the bitwise Hamming distance between the
102114
given integers `int_l` and `int_r`.
103115
116+
The distance covers only the lowest 64 bits. Bits above bit 63 are ignored,
117+
and negative integers are interpreted using their lowest 64
118+
two's-complement bits.
119+
104120
## Examples
105121
106122
iex> Abit.Bitmask.hamming_distance(1, 1)

mix.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
defmodule Abit.MixProject do
22
use Mix.Project
33

4-
@version "0.4.0"
4+
@version "1.0.0"
55
@github "https://github.com/preciz/abit"
66

77
def project do
88
[
99
app: :abit,
1010
name: "Abit",
1111
version: @version,
12-
elixir: "~> 1.7",
12+
elixir: "~> 1.14",
1313
start_permanent: Mix.env() == :prod,
1414
deps: deps(),
1515
docs: docs(),

test/abit/bitmask_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ defmodule Abit.BitmaskTest do
9292
assert Bitmask.set_bit_at(10, -1, 1) == 10
9393
assert Bitmask.set_bit_at(10, -1, 0) == 10
9494
end
95-
95+
9696
test "hamming_distance with integers larger than 64 bits" do
9797
# Since it uses set_bits_count, it also ignores bits above 64.
9898
assert Bitmask.hamming_distance(1 <<< 65, 0) == 0

test/abit/counter_test.exs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ defmodule Abit.CounterTest do
141141
counter = Counter.new(10, 8)
142142
tasks = for i <- 1..8, do: Task.async(fn -> Counter.add(counter, i, 1) end)
143143
Enum.each(tasks, &Task.await/1)
144-
for i <- 1..8, do: assert Counter.get(counter, i) == 1
144+
for i <- 1..8, do: assert(Counter.get(counter, i) == 1)
145145
end
146146

147147
test "put/3 preserves concurrent writes to counters in the same atomics integer" do
@@ -170,17 +170,17 @@ defmodule Abit.CounterTest do
170170
refute Enum.member?(counter, 101)
171171

172172
assert Enum.max(counter) == 100
173-
173+
174174
# This covers `do_reduce` with `{:halt, acc}` and `fun` invocation
175175
assert Enum.take(counter, 4) == [0, 42, 0, 100]
176-
176+
177177
# This covers `do_reduce` reaching the `done` state
178178
assert Enum.sum(counter) == 142
179179
assert Enum.to_list(counter) |> length() == 16
180180

181181
# This covers `do_slice` function via `Enum.slice`
182182
assert Enum.slice(counter, 1..3) == [42, 0, 100]
183-
183+
184184
# This covers `do_reduce` with `{:suspend, acc}`
185185
assert Enum.zip(counter, [1, 2, 3]) |> Enum.to_list() == [{0, 1}, {42, 2}, {0, 3}]
186186
end
@@ -192,14 +192,14 @@ defmodule Abit.CounterTest do
192192
Counter.new(0, 8)
193193
end
194194
end
195-
195+
196196
test "put with negative value when wrap_around is true" do
197197
c = Counter.new(10, 8, wrap_around: true, signed: false)
198198
# -1 wrapping around in 8-bit unsigned becomes 255
199199
assert {:ok, {0, 255}} = Counter.put(c, 0, -1)
200200
assert Counter.get(c, 0) == 255
201201
end
202-
202+
203203
test "add with very large increment wrapping around multiple times" do
204204
c = Counter.new(10, 8, wrap_around: true, signed: false)
205205
# 256 + 10 = 266 -> wraps around 256 to 10

test/abit_test.exs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ defmodule AbitTest do
2727
ref_a |> :atomics.put(1, 321)
2828
ref_b |> :atomics.put(2, 123)
2929

30-
merged_ref = Abit.merge(ref_a, ref_b)
30+
merged_ref = apply(Abit, :merge, [ref_a, ref_b])
3131
assert merged_ref == ref_a
3232
assert 321 = :atomics.get(merged_ref, 1)
3333
assert 123 = :atomics.get(merged_ref, 2)
@@ -307,14 +307,14 @@ defmodule AbitTest do
307307

308308
test "set_bit_at/3 concurrently for different bits" do
309309
ref = :atomics.new(1, signed: false)
310-
310+
311311
# Spawn tasks that toggle bit 0 concurrently to create contention on the atomics integer
312312
toggle_tasks = for _ <- 1..500, do: Task.async(fn -> Abit.toggle_bit_at(ref, 63) end)
313-
313+
314314
tasks = for i <- 0..50, do: Task.async(fn -> Abit.set_bit_at(ref, i, 1) end)
315-
315+
316316
Enum.each(toggle_tasks ++ tasks, &Task.await/1)
317-
317+
318318
# All bits 0..50 should be set
319319
for i <- 0..50 do
320320
assert Abit.bit_at(ref, i) == 1
@@ -327,7 +327,7 @@ defmodule AbitTest do
327327
tasks = for _ <- 1..500, do: Task.async(fn -> Abit.toggle_bit_at(ref, 0) end)
328328
Enum.each(tasks, &Task.await/1)
329329
assert :atomics.get(ref, 1) == 0
330-
330+
331331
# Toggling 501 times should result in the bit being set to 1.
332332
tasks = for _ <- 1..501, do: Task.async(fn -> Abit.toggle_bit_at(ref, 0) end)
333333
Enum.each(tasks, &Task.await/1)

0 commit comments

Comments
 (0)