Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function_body_length:
warning: 300
error: 500
file_length:
warning: 800
warning: 1000
error: 1200
ignore_comment_only_lines: true
large_tuple:
Expand All @@ -33,6 +33,7 @@ identifier_name:
- j
- x
- y
- "b[0-9]"
allowed_symbols: ["_", " "]
cyclomatic_complexity:
warning: 20
Expand Down
2 changes: 0 additions & 2 deletions Benchmarks/BenchmarkTypes/BenchmarkBitmaskComplex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import Foundation

@ParseBitmask
public struct BenchmarkBitmaskComplex: Equatable, Sendable, BaselineParsable {
public typealias RawBitsInteger = UInt32

@mask(bitCount: 1)
public var flag1: UInt8

Expand Down
2 changes: 0 additions & 2 deletions Benchmarks/BenchmarkTypes/BenchmarkBitmaskSimple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import Foundation

@ParseBitmask
public struct BenchmarkBitmaskSimple: Equatable, Sendable, BaselineParsable {
public typealias RawBitsInteger = UInt8

@mask(bitCount: 1)
public var flag: UInt8

Expand Down
2 changes: 0 additions & 2 deletions Benchmarks/BenchmarkTypes/NonByteAlignedBitmask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import Foundation

@ParseBitmask
public struct NonByteAlignedBitmask: Equatable, Sendable, BaselineParsable {
public typealias RawBitsInteger = UInt16

@mask(bitCount: 3)
public var first: UInt8

Expand Down
6 changes: 2 additions & 4 deletions Benchmarks/BenchmarkTypes/UInt16+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import BinaryParseKit
import Foundation

extension UInt16: ExpressibleByRawBits {
public typealias RawBitsInteger = UInt16

public init(bits: RawBitsInteger) throws {
self = bits
public init(bits: borrowing RawBitsSpan) throws {
self = try bits.load(as: Self.self)
}
}

Expand Down
10 changes: 10 additions & 0 deletions Benchmarks/BenchmarkTypes/Utils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Benchmark

public extension Benchmark {
@inlinable
func context(_ body: () -> Void) {
startMeasurement()
body()
stopMeasurement()
}
}
60 changes: 35 additions & 25 deletions Benchmarks/BenchmarkTypesTests/BenchmarkTypesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import BenchmarkTypes
import BinaryParseKit
import Foundation
import Testing

Expand Down Expand Up @@ -113,28 +114,33 @@ struct BenchmarkTypesTests {

@Test("Parse simple bitmask")
func parseSimpleBitmask() throws {
let bits: UInt8 = 0b1010_0011
let parsed = try BenchmarkBitmaskSimple(bits: bits)
let baseline = BenchmarkBitmaskSimple.parseBaseline(Data([bits]))
#expect(parsed.flag == 1)
#expect(parsed.value == 0x23)
#expect(parsed == baseline)
let data = Data([0b1010_0011])
try data.withParserSpan { parserSpan in
let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8)
let parsed = try BenchmarkBitmaskSimple(bits: rawBits)
let baseline = BenchmarkBitmaskSimple.parseBaseline(data)
#expect(parsed.flag == 1)
#expect(parsed.value == 0x23)
#expect(parsed == baseline)
}
}

// MARK: - Complex Bitmask Tests

@Test("Parse complex bitmask")
func parseComplexBitmask() throws {
let data = Data([0xAB, 0xCD, 0xEF, 0x12])
let bits: UInt32 = 0xABCD_EF12
let parsed = try BenchmarkBitmaskComplex(bits: bits)
let baseline = BenchmarkBitmaskComplex.parseBaseline(data)
#expect(parsed.flag1 == 1)
#expect(parsed.priority == 2)
#expect(parsed.nibble == 11)
#expect(parsed.byte == 0xCD)
#expect(parsed.word == 0xEF12)
#expect(parsed == baseline)
try data.withParserSpan { parserSpan in
let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 32)
let parsed = try BenchmarkBitmaskComplex(bits: rawBits)
let baseline = BenchmarkBitmaskComplex.parseBaseline(data)
#expect(parsed.flag1 == 1)
#expect(parsed.priority == 2)
#expect(parsed.nibble == 11)
#expect(parsed.byte == 0xCD)
#expect(parsed.word == 0xEF12)
#expect(parsed == baseline)
}
}

// MARK: - Endianness Tests
Expand Down Expand Up @@ -164,13 +170,15 @@ struct BenchmarkTypesTests {
@Test("Parse non-byte-aligned bitmask")
func parseNonByteAlignedBitmask() throws {
let data = Data([0xAC, 0xC0])
let bits: UInt16 = 0b1010_1100_1100_0000
let parsed = try NonByteAlignedBitmask(bits: bits)
let baseline = NonByteAlignedBitmask.parseBaseline(data)
#expect(parsed.first == 5) // 101
#expect(parsed.second == 12) // 01100
#expect(parsed.third == 3) // 11
#expect(parsed == baseline)
try data.withParserSpan { parserSpan in
let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 10)
let parsed = try NonByteAlignedBitmask(bits: rawBits)
let baseline = NonByteAlignedBitmask.parseBaseline(data)
#expect(parsed.first == 5) // 101
#expect(parsed.second == 12) // 01100
#expect(parsed.third == 3) // 11
#expect(parsed == baseline)
}
}

// MARK: - Round-Trip Tests
Expand All @@ -187,8 +195,10 @@ struct BenchmarkTypesTests {
func roundTripSimpleBitmask() throws {
let original = BenchmarkBitmaskSimple(flag: 1, value: 0x23)
let printed = try original.printParsed(printer: .data)
let bits = printed[0]
let reparsed = try BenchmarkBitmaskSimple(bits: bits)
#expect(original == reparsed)
try printed.withParserSpan { parserSpan in
let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8)
let reparsed = try BenchmarkBitmaskSimple(bits: rawBits)
#expect(original == reparsed)
}
}
}
30 changes: 21 additions & 9 deletions Benchmarks/ParsingBenchmarks/ParsingBenchmarks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,16 @@ let benchmarks: @Sendable () -> Void = {
// MARK: - Bitmask Parsing Benchmarks

let simpleBitmaskData = Data([0xA3])
let simpleBitmaskBits: UInt8 = 0b1010_0011
let complexBitmaskData = Data([0xAB, 0xCD, 0xEF, 0x12])
let complexBitmaskBits: UInt32 = 0xABCD_EF12

Benchmark("Parse Simple Bitmask") { benchmark in
for _ in benchmark.scaledIterations {
blackHole(try! BenchmarkBitmaskSimple(bits: simpleBitmaskBits))
simpleBitmaskData.withParserSpan { parserSpan in
let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8)
benchmark.context {
for _ in benchmark.scaledIterations {
blackHole(try! BenchmarkBitmaskSimple(bits: rawBits))
}
}
}
}

Expand All @@ -105,8 +108,13 @@ let benchmarks: @Sendable () -> Void = {
}

Benchmark("Parse Complex Bitmask") { benchmark in
for _ in benchmark.scaledIterations {
blackHole(try! BenchmarkBitmaskComplex(bits: complexBitmaskBits))
complexBitmaskData.withParserSpan { parserSpan in
let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 32)
benchmark.context {
for _ in benchmark.scaledIterations {
blackHole(try! BenchmarkBitmaskComplex(bits: rawBits))
}
}
}
}

Expand Down Expand Up @@ -165,11 +173,15 @@ let benchmarks: @Sendable () -> Void = {
// MARK: - Non-Byte-Aligned Bitmask Benchmarks

let nonAlignedData = Data([0xAC, 0xC0])
let nonAlignedBits: UInt16 = 0b1010_1100_1100_0000

Benchmark("Parse Non-Byte-Aligned Bitmask (10 bits)") { benchmark in
for _ in benchmark.scaledIterations {
blackHole(try! NonByteAlignedBitmask(bits: nonAlignedBits))
nonAlignedData.withParserSpan { parserSpan in
let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 10)
benchmark.context {
for _ in benchmark.scaledIterations {
blackHole(try! NonByteAlignedBitmask(bits: rawBits))
}
}
}
}

Expand Down
13 changes: 9 additions & 4 deletions Benchmarks/PrintingBenchmarks/PrintingBenchmarks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ let benchmarks: @Sendable () -> Void = {

let roundTripEnumData = Data([0x03, 0x12, 0x34, 0x56, 0x78])
let roundTripStructData = Data([0x12, 0x34, 0x56, 0x78])
let roundTripBitmaskBits: UInt8 = 0b1010_0011
let roundTripBitmaskData = Data([0b1010_0011])

Benchmark("Round-Trip Enum (Parse + Print)") { benchmark in
for _ in benchmark.scaledIterations {
Expand All @@ -154,9 +154,14 @@ let benchmarks: @Sendable () -> Void = {
}

Benchmark("Round-Trip Bitmask (Parse + Print)") { benchmark in
for _ in benchmark.scaledIterations {
let parsed = try! BenchmarkBitmaskSimple(bits: roundTripBitmaskBits)
blackHole(try! parsed.printParsed(printer: .data))
roundTripBitmaskData.withParserSpan { parserSpan in
let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8)
benchmark.context {
for _ in benchmark.scaledIterations {
let parsed = try! BenchmarkBitmaskSimple(bits: rawBits)
blackHole(try! parsed.printParsed(printer: .data))
}
}
}
}

Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ let package = Package(
name: "BenchmarkTypes",
dependencies: [
"BinaryParseKit",
.product(name: "Benchmark", package: "package-benchmark"),
],
path: "Benchmarks/BenchmarkTypes",
),
Expand Down
6 changes: 6 additions & 0 deletions Sources/BinaryParseKit/BinaryParseKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ public macro parseRest(endianness: Endianness) = #externalMacro(
/// - A ``Printable`` conformance
///
/// - Parameters:
/// - bitEndian: The bit ordering for bitmask parsing. Use `.big` for MSB-first (default) or `.little` for LSB-first.
/// - parsingAccessor: The accessor level for the generated `Parsable` conformance (default is `.follow`)
/// - printingAccessor: The accessor level for the generated `Printable` conformance (default is `.follow`)
///
Expand All @@ -282,6 +283,7 @@ public macro parseRest(endianness: Endianness) = #externalMacro(
/// ```
@attached(extension, conformances: BinaryParseKit.Parsable, BinaryParseKit.Printable, names: arbitrary)
public macro ParseStruct(
bitEndian: Endianness = .big,
parsingAccessor: ExtensionAccessor = .follow,
printingAccessor: ExtensionAccessor = .follow,
) = #externalMacro(
Expand All @@ -302,6 +304,7 @@ public macro ParseStruct(
/// - A ``Printable`` conformance
///
/// - Parameters:
/// - bitEndian: The bit ordering for bitmask parsing. Use `.big` for MSB-first (default) or `.little` for LSB-first.
/// - parsingAccessor: The accessor level for the generated `Parsable` conformance (default is `.follow`)
/// - printingAccessor: The accessor level for the generated `Printable` conformance (default is `.follow`)
///
Expand All @@ -311,6 +314,7 @@ public macro ParseStruct(
/// - Note: any `match` macro has to proceed `parse` and `skip` macros.
@attached(extension, conformances: BinaryParseKit.Parsable, BinaryParseKit.Printable, names: arbitrary)
public macro ParseEnum(
bitEndian: Endianness = .big,
parsingAccessor: ExtensionAccessor = .follow,
printingAccessor: ExtensionAccessor = .follow,
) = #externalMacro(
Expand Down Expand Up @@ -646,6 +650,7 @@ public macro mask() = #externalMacro(
/// - An `init(bits: RawBits) throws` initializer
///
/// - Parameters:
/// - bitEndian: The bit ordering for parsing. Use `.big` for MSB-first (default) or `.little` for LSB-first.
/// - parsingAccessor: The accessor level for the generated initializer (default is `.follow`)
/// - printingAccessor: The accessor level for the generated `bitCount` property (default is `.follow`)
///
Expand Down Expand Up @@ -682,6 +687,7 @@ public macro mask() = #externalMacro(
names: arbitrary
)
public macro ParseBitmask(
bitEndian: Endianness = .big,
parsingAccessor: ExtensionAccessor = .follow,
printingAccessor: ExtensionAccessor = .follow,
) = #externalMacro(
Expand Down
28 changes: 15 additions & 13 deletions Sources/BinaryParseKit/Extensions/ExpressibleByRawBits+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import Foundation
// MARK: - Bool Conformance

extension Bool: ExpressibleByRawBits {
public typealias RawBitsInteger = UInt8

public init(bits: RawBitsInteger) throws {
// bits is right-aligned, check LSB
self = (bits & 0x01) != 0
public init(bits: borrowing RawBitsSpan) throws {
// Extract the first bit from the span
precondition(bits.bitCount >= 1, "Bool requires at least 1 bit")
let firstByte = bits.bytes[0]
// Check the bit at bitOffset position (MSB-first)
let mask: UInt8 = 0x80 >> bits.bitStartIndex
self = (firstByte & mask) != 0
}
}

Expand All @@ -30,10 +32,10 @@ extension Bool: RawBitsConvertible {
// MARK: - Integer Conformances

extension UInt8: ExpressibleByRawBits {
public typealias RawBitsInteger = UInt8

public init(bits: RawBitsInteger) throws {
self = bits
public init(bits: borrowing RawBitsSpan) throws {
// Extract up to 8 bits from the span and convert to UInt8
precondition(bits.bitCount <= 8, "UInt8 can hold at most 8 bits")
self = try bits.load(as: UInt8.self)
}
}

Expand All @@ -48,10 +50,10 @@ extension UInt8: RawBitsConvertible {
}

extension Int8: ExpressibleByRawBits {
public typealias RawBitsInteger = UInt8

public init(bits: RawBitsInteger) throws {
self = Int8(bitPattern: bits)
public init(bits: borrowing RawBitsSpan) throws {
// Extract up to 8 bits from the span and convert to Int8
let unsigned = try UInt8(bits: bits)
self = Int8(bitPattern: unsigned)
}
}

Expand Down
Loading
Loading