Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework binary integer description error-handling #134

Merged
merged 2 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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