Skip to content

Commit

Permalink
Rework binary integer description error-handling using (#95).
Browse files Browse the repository at this point in the history
  • Loading branch information
oscbyspro committed Nov 20, 2024
1 parent b0a7cc3 commit bbe0de5
Show file tree
Hide file tree
Showing 25 changed files with 1,406 additions and 989 deletions.
43 changes: 24 additions & 19 deletions Sources/CoreKit/BinaryInteger+Text.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,41 @@
// MARK: Initializers
//=------------------------------------------------------------------------=

/// Decodes the `description`, if possible.
/// Returns the `value` of `description`, or `nil`.
///
/// ```swift
/// format.decode(description)?.optional()
/// ```
///
/// ### Binary Integer Description
///
/// - Note: The default format is `TextInt.decimal`.
/// - Note: The `error` is set if the operation is `lossy`.
///
/// - Note: Decoding failures throw `TextInt.Error`.
/// - Note: It produces `nil` if the `description` is `invalid`.
///
@inlinable public init?(_ description: String) {
always: do {
self = try TextInt.decimal.decode(description)
} catch {
return nil
}
/// - Note: The default `format` is `TextInt.decimal`.
///
@inlinable public init?(_ description: consuming String) {
self.init(description, using: TextInt.decimal)
}

/// Decodes the `description` using the given `format`, if possible.
/// Returns the `value` of `description`, or `nil`.
///
/// ```swift
/// format.decode(description)?.optional()
/// ```
///
/// ### Binary Integer Description
///
/// - Note: The default format is `TextInt.decimal`.
/// - Note: The `error` is set if the operation is `lossy`.
///
/// - Note: It produces `nil` if the `description` is `invalid`.
///
/// - Note: Decoding failures throw `TextInt.Error`.
/// - Note: The default `format` is `TextInt.decimal`.
///
@inlinable public init(_ description: some StringProtocol, using format: TextInt) throws {
self = try format.decode(description)
@inlinable public init?(_ description: consuming String, using format: borrowing TextInt) {
guard let instance: Self = format.decode(description)?.optional() else { return nil }
self = ((instance))
}
//=------------------------------------------------------------------------=
Expand All @@ -58,8 +67,6 @@
///
/// - Note: The default format is `TextInt.decimal`.
///
/// - Note: Decoding failures throw `TextInt.Error`.
///
@inlinable public var description: String {
self.description(using: TextInt.decimal)
}
Expand All @@ -70,9 +77,7 @@
///
/// - Note: The default format is `TextInt.decimal`.
///
/// - Note: Decoding failures throw `TextInt.Error`.
///
@inlinable public func description(using format: TextInt) -> String {
@inlinable public func description(using format: borrowing TextInt) -> String {
format.encode(self)
}
}
215 changes: 109 additions & 106 deletions Sources/CoreKit/Models/TextInt+Decoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,51 +17,62 @@ extension TextInt {
// MARK: Utilities
//=------------------------------------------------------------------------=

@inlinable public func decode<T: BinaryInteger>(_ description: StaticString) -> T {
description.withUTF8Buffer {
try! self.decode($0)
}
}

@inlinable public func decode<T: BinaryInteger>(_ description: some StringProtocol) throws -> T {
var description = String(description)
return try description.withUTF8 {
try self.decode($0)
/// Returns the `value` of `description` and an `error` indicator, or `nil`.
///
/// ### Binary Integer Description
///
/// - Note: The `error` is set if the operation is `lossy`.
///
/// - Note: It produces `nil` if the `description` is `invalid`.
///
/// - Note: The default `format` is `TextInt.decimal`.
///
@inlinable public func decode<Integer>(
_ description: consuming String,
as type: Integer.Type = Integer.self
) -> Optional<Fallible<Integer>> where Integer: BinaryInteger {

description.withUTF8 {
self.decode($0, as: Integer.self)
}
}

@inlinable public func decode<T: BinaryInteger>(_ description: UnsafeBufferPointer<UInt8>) throws -> T {
var magnitude = Fallible(T.Magnitude.zero, error: true)
/// Returns the `value` of `description` and an `error` indicator, or `nil`.
///
/// ### Binary Integer Description
///
/// - Note: The `error` is set if the operation is `lossy`.
///
/// - Note: It produces `nil` if the `description` is `invalid`.
///
/// - Note: The default `format` is `TextInt.decimal`.
///
@inlinable public func decode<Integer>(
_ description: UnsafeBufferPointer<UInt8>,
as type: Integer.Type = Integer.self
) -> Optional<Fallible<Integer>> where Integer: BinaryInteger {

var body = description[...]
let sign = TextInt.remove(from: &body, prefix: Self.sign) ?? Sign.plus
let mask = TextInt.remove(from: &body, prefix: Self.mask) != nil

// TODO: consider simpler error handling, only checking mask if infinite
var magnitude: Optional<Fallible<Integer.Magnitude>> = nil

if self.power.div == 1 {
try self.words16(numerals: UnsafeBufferPointer(rebasing: body)) {
magnitude = T.Magnitude.exactly($0, mode: .unsigned)
}

} else {
try self.words10(numerals: UnsafeBufferPointer(rebasing: body)) {
magnitude = T.Magnitude.exactly($0, mode: .unsigned)
}
self.decode(body: UnsafeBufferPointer(rebasing: body)) {
magnitude = Integer.Magnitude.exactly($0, mode: Signedness.unsigned)
}

if magnitude.error {
throw Error.lossy
}

if mask, T.isFinite {
throw Error.lossy
}
guard var magnitude = consume magnitude else { return nil }

if mask {
magnitude.value.toggle()
}

return try T.exactly(sign: sign, magnitude: magnitude.value).prune(Error.lossy)
if mask, Integer.isFinite {
magnitude.error = true
}

return Integer.exactly(sign: sign, magnitude: magnitude.value).veto(magnitude.error)
}
}

Expand All @@ -75,118 +86,110 @@ extension TextInt {
// MARK: Utilities
//=------------------------------------------------------------------------=

@usableFromInline package func words10(
numerals: consuming UnsafeBufferPointer<UInt8>, success: (DataInt<UX>) -> Void
) throws {
//=--------------------------------------=
Swift.assert(self.power.div != 1)
//=--------------------------------------=
// text must contain at least one numeral
//=--------------------------------------=
if numerals.isEmpty {
throw Error.invalid
@inlinable package func decode(
body: consuming UnsafeBufferPointer<UInt8>,
callback: (DataInt<UX>) -> Void
) -> Void {

if self.power.div == 1 {
self.decode16(body: body, callback: callback)

} else {
numerals = UnsafeBufferPointer(rebasing: numerals.drop(while:{ $0 == 48 }))
self.decode10(body: body, callback: callback)
}
}

//=------------------------------------------------------------------------=
// MARK: Utilities x Non-generic & Non-inlinable
//=------------------------------------------------------------------------=

@usableFromInline package func decode10(
body: consuming UnsafeBufferPointer<UInt8>,
callback: (DataInt<UX>) -> Void
) -> Void {

Swift.assert(self.power.div != 1)
//=--------------------------------------=
// capacity is measured in radix powers
if body.isEmpty { return }
//=--------------------------------------=
body = UnsafeBufferPointer(rebasing: body.drop(while:{ $0 == 48 }))
//=--------------------------------------=
let divisor = Nonzero(unchecked: self.exponent)
var (stride): IX = IX(numerals.count).remainder(divisor)
var capacity: IX = IX(numerals.count).quotient (divisor).unchecked()
var steps: IX = IX(body.count).remainder(divisor)
var capacity: IX = IX(body.count).quotient (divisor).unchecked()

if (stride).isZero {
(stride) = self.exponent
if steps.isZero {
steps = self.exponent
} else {
capacity = capacity.incremented().unchecked()
}
//=--------------------------------------=
return try Swift.withUnsafeTemporaryAllocation(of: UX.self, capacity: Int(capacity)) {
Swift.withUnsafeTemporaryAllocation(of: UX.self, capacity: Int(capacity)) {
let words = MutableDataInt<UX>.Body($0)![unchecked: ..<capacity]
var index = IX.zero
//=----------------------------------=
// pointee: deferred deinitialization
//=----------------------------------=

defer {

words[unchecked: ..<index].deinitialize()

}
//=----------------------------------=
// pointee: initialization
//=----------------------------------=
forwards: while !numerals.isEmpty {
let part = UnsafeBufferPointer(rebasing: numerals[..<Int(stride)])
numerals = UnsafeBufferPointer(rebasing: numerals[Int(stride)...])
let element = try self.numerals.load(part, as: UX.self)
// note that the index advances faster than the product

forwards: while !body.isEmpty {
let part = UnsafeBufferPointer(rebasing: body[..<Int(steps)])
((body)) = UnsafeBufferPointer(rebasing: body[Int(steps)...])
guard let element: UX = self.numerals.load(part) else { return }
words[unchecked: index] = words[unchecked: ..<index].multiply(by: self.power.div, add: element)
stride = self.exponent
index = index.incremented().unchecked()
steps = (self.exponent)
index = index.incremented().unchecked()

}
//=----------------------------------=
// path: success
//=----------------------------------=
Swift.assert(numerals.isEmpty)
Swift.assert(index == (words).count)
return success(DataInt(words))

Swift.assert(body.isEmpty)
Swift.assert(index == words.count)
callback(DataInt((words)))
}
}

@usableFromInline package func words16(
numerals: consuming UnsafeBufferPointer<UInt8>, success: (DataInt<UX>) -> Void
) throws {
//=--------------------------------------=
@usableFromInline package func decode16(
body: consuming UnsafeBufferPointer<UInt8>,
callback: (DataInt<UX>) -> Void
) -> Void {

Swift.assert(self.power.div == 1)
Swift.assert(self.exponent.count(Bit.one) == Count(1))
//=--------------------------------------=
// text must contain at least one numeral
//=--------------------------------------=
if numerals.isEmpty {
throw Error.invalid
} else {
numerals = UnsafeBufferPointer(rebasing: numerals.drop(while:{ $0 == 48 }))
}
if body.isEmpty { return }
//=--------------------------------------=
// capacity is measured in radix powers
body = UnsafeBufferPointer(rebasing: body.drop(while:{ $0 == 48 }))
//=--------------------------------------=
let divisor = Nonzero(unchecked: self.exponent)
var (stride): IX = IX(numerals.count).remainder(divisor)
var capacity: IX = IX(numerals.count).quotient (divisor).unchecked()
var steps: IX = IX(body.count).remainder(divisor)
var capacity: IX = IX(body.count).quotient (divisor).unchecked()

if (stride).isZero {
(stride) = self.exponent
if steps.isZero {
steps = self.exponent
} else {
capacity = capacity.incremented().unchecked()
}
//=--------------------------------------=
return try Swift.withUnsafeTemporaryAllocation(of: UX.self, capacity: Int(capacity)) {
let words = MutableDataInt<UX>.Body($0)![unchecked: ..<capacity]
Swift.withUnsafeTemporaryAllocation(of: UX.self, capacity: Swift.Int(capacity)) {
let words = MutableDataInt<UX>.Body($0)![unchecked: ..<capacity]
var index = words.count
//=----------------------------------=
// pointee: deferred deinitialization
//=----------------------------------=

defer {

words[unchecked: index...].deinitialize()

}
//=----------------------------------=
// pointee: initialization
//=----------------------------------=
backwards: while !numerals.isEmpty {
let part = UnsafeBufferPointer(rebasing: numerals[..<Int(stride)])
numerals = UnsafeBufferPointer(rebasing: numerals[Int(stride)...])
index = index.decremented().unchecked()
words[unchecked: index] = try self.numerals.load(part, as: UX.self)
stride = self.exponent

backwards: while !body.isEmpty {
let part = UnsafeBufferPointer(rebasing: body[..<Int(steps)])
((body)) = UnsafeBufferPointer(rebasing: body[Int(steps)...])
guard let element: UX = self.numerals.load(part) else { return }
steps = (self.exponent)
index = index.decremented().unchecked()
words[unchecked: index] = element
}
//=----------------------------------=
// path: success
//=----------------------------------=
Swift.assert(numerals.isEmpty)
Swift.assert(index == IX.zero)
return success(DataInt(words))

Swift.assert(body.isEmpty)
Swift.assert(index.isZero)
callback(DataInt((words)))
}
}
}
Loading

0 comments on commit bbe0de5

Please sign in to comment.