Skip to content

Commit ff1e5a8

Browse files
authored
Add LSB parsing support to bitmask (#29)
## Goal This PR adds LSB parsing support to bitmask. The bitmask parsing is defaulted to MSB (BE). To opt in LSB (LE) parsing, one needs to specify in the `@ParseStrucut`, `@ParseEnum`, and `@ParseBitmask` macro with `bitEndian: .little`. ## Design This PR replaces the old RawBitsInteger-based approach with a new RawBitsSpan type and uses it to enable endianness-aware bit-level parsing. ### RawBitsSpan RawBitsSpan is a ~Escapable, ~Copyable span type that provides a zero-copy view into a contiguous byte buffer with bit-level granularity. It tracks a _bitOffset and _bitCount into an underlying Span<UInt8>. Key operations: - `load(as:bitCount:)` — extracts bits as a right-aligned fixed-width integer - `extracting(first:)` / `extracting(last:)` — non-mutating sub-span extraction - `slicing(first:)` / `slicing(last:)` — mutating extraction that consumes bits from the span This replaces the previous design where all bits were pre-extracted into a FixedWidthInteger (RawBitsInteger associated type). The span-based approach removes the 64/128-bit size limit, avoids copying bits into an integer container, and decouples conforming types from a specific integer width. ### Endianness flow The bitEndian parameter flows through the system as follows: 1. Declaration — user specifies bitEndian: .little on the macro attribute 2. Macro expansion — MacroConfigurationVisitor extracts the value and stores it in AccessorInfo 3. Code generation — the macro generates RawBitsSpan initialization and field slicing calls: - Big endian (default): span starts at bit offset 0, fields are sliced with `__slicing(unchecked: (), first:)`, consuming from MSB toward LSB - Little endian: span starts at bit offset totalBytes * 8 - totalBits, fields are sliced with `__slicing(unchecked: (), last:)`, consuming from LSB toward MSB 4. Runtime — `__createFromBits` adjusts the span when `fieldRequestedBitCount > typeBitCount`, extracting from first (big) or last (little) to select the significant bits ### ExpressibleByRawBits protocol change The protocol no longer has an associated `RawBitsInteger` type. The initializer now takes borrowing RawBitsSpan: ```swift public protocol ExpressibleByRawBits { init(bits: borrowing RawBitsSpan) throws } ``` Conformances for `Bool`, `UInt8`, `Int8`, and other integer types extract only the bits they need via RawBitsSpan.load(). ### Other changes - CI updated to use Swiftly - Benchmarks disabled by default - Removed `__maskParsing` and `ExtractBitsAsIntegerTests` / `MaskParsingTests` (superseded by `RawBitsSpan` and new tests) - Refactored macro utility naming (`MacroConfigurationVisitor`, `extractMacroConfiguration`) - Added comprehensive test coverage for both endianness modes across bitmask, enum, and struct parsing
1 parent 17e5440 commit ff1e5a8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3379
-2574
lines changed

.github/workflows/benchmark.yml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,31 @@ jobs:
1616
name: Run Benchmarks
1717
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
1818
runs-on: macOS-26
19-
env:
20-
DEVELOPER_DIR: "/Applications/Xcode_26.1.app/Contents/Developer"
2119
steps:
2220
- uses: actions/checkout@v6
2321
with:
2422
fetch-depth: 0
23+
- name: Install Swiftly And Swift
24+
run: |
25+
curl -L https://download.swift.org/swiftly/darwin/swiftly.pkg > swiftly.pkg
26+
installer -pkg swiftly.pkg -target CurrentUserHomeDirectory
27+
28+
~/.swiftly/bin/swiftly init
29+
30+
. "$HOME/.swiftly/env.sh"
31+
hash -r
32+
33+
swiftly install --use --assume-yes
34+
swiftly link --assume-yes
35+
36+
echo "$HOME/.swiftly/bin" >> $GITHUB_PATH
2537
- name: Swift Version
26-
run: xcrun swift --version
38+
run: which swift && swift --version
2739
- name: Install jemalloc
2840
run: |
2941
echo "/opt/homebrew/bin:/usr/local/bin" >> $GITHUB_PATH
3042
brew install jemalloc
3143
- name: Run Parsing Benchmarks
3244
run: |
33-
xcrun swift package benchmark --no-progress --format markdown >> $GITHUB_STEP_SUMMARY
45+
export ENABLE_BENCHMARK=1
46+
swift package benchmark --no-progress --format markdown >> $GITHUB_STEP_SUMMARY

.github/workflows/ci.yml

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,46 +23,55 @@ jobs:
2323
lint:
2424
name: Lint & Format Check
2525
runs-on: macOS-26
26-
env:
27-
DEVELOPER_DIR: "/Applications/Xcode_26.1.app/Contents/Developer"
2826
steps:
2927
- name: Checkout
3028
uses: actions/checkout@v6
31-
- name: Swift Version
32-
run: xcrun swift --version
3329
- name: Install SwiftLint
3430
run: brew install swiftlint --quiet
3531
- name: Install swift-format
3632
run: brew install swiftformat --quiet
3733
- name: SwiftLint
38-
run: swiftlint --strict --config .swiftlint.yml .
34+
run: swiftlint --strict --config .swiftlint.yml . --reporter github-actions-logging
3935
- name: Swift Format Check
40-
run: swiftformat --lint-only --config .swiftformat.conf . --strict
36+
run: swiftformat --lint-only --config .swiftformat.conf . --strict --reporter github-actions-log --verbose
4137
test:
4238
name: ${{ matrix.name }}
4339
runs-on: ${{ matrix.runsOn }}
4440
needs: lint
45-
env:
46-
DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer"
4741
strategy:
4842
matrix:
4943
include:
50-
- xcode: "Xcode_26.0"
51-
runsOn: macOS-26
44+
- runsOn: macOS-26
5245
name: "macOS 26, SPM 6.2.0 Test"
5346
steps:
5447
- uses: actions/checkout@v4
48+
- name: Install Swiftly And Swift
49+
run: |
50+
curl -L https://download.swift.org/swiftly/darwin/swiftly.pkg > swiftly.pkg
51+
installer -pkg swiftly.pkg -target CurrentUserHomeDirectory
52+
53+
~/.swiftly/bin/swiftly init
54+
55+
. "$HOME/.swiftly/env.sh"
56+
hash -r
57+
58+
swiftly install --use --assume-yes
59+
swiftly link --assume-yes
60+
61+
echo "$HOME/.swiftly/bin" >> $GITHUB_PATH
5562
- name: Swift Version
56-
run: xcrun swift --version
57-
- name: Install xcbeautify
63+
run: which swift && swift --version
64+
- name: Brew install tools
5865
run: brew install xcbeautify --quiet
5966
- name: Run Tests
6067
run: |
61-
set -o pipefail && \
62-
xcodebuild test \
63-
-scheme BinaryParseKit \
64-
-destination 'platform=macOS' \
65-
-skipMacroValidation | xcbeautify --renderer github-actions
68+
defaults write com.apple.dt.Xcode IDEPackageEnablePrebuilts YES
69+
70+
# set -o pipefail && \
71+
# xcodebuild test \
72+
# -scheme BinaryParseKit \
73+
# -destination 'platform=macOS' \
74+
# -skipMacroValidation | xcbeautify --renderer github-actions
6675
6776
# https://github.com/swiftlang/swift-package-manager/issues/9163
68-
# set -o pipefail && xcrun swift test -c debug --enable-code-coverage --xunit-output $TEST_RESULTS_DIR/xunit.xml 2>&1 | xcbeautify --renderer github-actions
77+
set -o pipefail && swift test | xcbeautify --renderer github-actions

.github/workflows/deploy-docc.yml

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,29 @@ jobs:
1414
build:
1515
name: Build DocC Documentation
1616
runs-on: macOS-26
17-
env:
18-
DEVELOPER_DIR: "/Applications/Xcode_26.1.app/Contents/Developer"
1917
steps:
2018
- name: Checkout
2119
uses: actions/checkout@v6
20+
- name: Install Swiftly And Swift
21+
run: |
22+
curl -L https://download.swift.org/swiftly/darwin/swiftly.pkg > swiftly.pkg
23+
installer -pkg swiftly.pkg -target CurrentUserHomeDirectory
24+
25+
~/.swiftly/bin/swiftly init
26+
27+
. "$HOME/.swiftly/env.sh"
28+
hash -r
29+
30+
swiftly install --use --assume-yes
31+
swiftly link --assume-yes
32+
33+
echo "$HOME/.swiftly/bin" >> $GITHUB_PATH
2234
- name: Swift Version
23-
run: xcrun swift --version
35+
run: which swift && swift --version
2436
- name: Build DocC
2537
run: |
26-
xcrun swift build
27-
xcrun swift package --allow-writing-to-directory ./docs \
38+
swift build
39+
swift package --allow-writing-to-directory ./docs \
2840
generate-documentation \
2941
--enable-experimental-combined-documentation \
3042
--target BinaryParseKit \

.swift-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
6.2
1+
6.2.3

.swiftlint.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function_body_length:
2020
warning: 300
2121
error: 500
2222
file_length:
23-
warning: 800
23+
warning: 1000
2424
error: 1200
2525
ignore_comment_only_lines: true
2626
large_tuple:
@@ -33,6 +33,7 @@ identifier_name:
3333
- j
3434
- x
3535
- y
36+
- "b[0-9]"
3637
allowed_symbols: ["_", " "]
3738
cyclomatic_complexity:
3839
warning: 20

Benchmarks/BenchmarkTypes/BenchmarkBitmaskComplex.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import Foundation
1111

1212
@ParseBitmask
1313
public struct BenchmarkBitmaskComplex: Equatable, Sendable, BaselineParsable {
14-
public typealias RawBitsInteger = UInt32
15-
1614
@mask(bitCount: 1)
1715
public var flag1: UInt8
1816

Benchmarks/BenchmarkTypes/BenchmarkBitmaskSimple.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import Foundation
1111

1212
@ParseBitmask
1313
public struct BenchmarkBitmaskSimple: Equatable, Sendable, BaselineParsable {
14-
public typealias RawBitsInteger = UInt8
15-
1614
@mask(bitCount: 1)
1715
public var flag: UInt8
1816

Benchmarks/BenchmarkTypes/NonByteAlignedBitmask.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import Foundation
1111

1212
@ParseBitmask
1313
public struct NonByteAlignedBitmask: Equatable, Sendable, BaselineParsable {
14-
public typealias RawBitsInteger = UInt16
15-
1614
@mask(bitCount: 3)
1715
public var first: UInt8
1816

Benchmarks/BenchmarkTypes/UInt16+.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ import BinaryParseKit
88
import Foundation
99

1010
extension UInt16: ExpressibleByRawBits {
11-
public typealias RawBitsInteger = UInt16
12-
13-
public init(bits: RawBitsInteger) throws {
14-
self = bits
11+
public init(bits: borrowing RawBitsSpan) throws {
12+
self = try bits.load(as: Self.self)
1513
}
1614
}
1715

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Benchmark
2+
3+
public extension Benchmark {
4+
@inlinable
5+
func context(_ body: () -> Void) {
6+
startMeasurement()
7+
body()
8+
stopMeasurement()
9+
}
10+
}

0 commit comments

Comments
 (0)