[Discussion] Arbitrary precision integer and float literal protocols

Hello swift-evolution,

I’m working on a proposal for arbitrary precision integer and float literal protocols to complement ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral. I’m looking for feedback and help in verifying the logic - I have doubts about endianness.

Fire away!

Arbitrary precision integer and float literal procotols

Proposal: SE-XXXX <https://github.com/hartbit/swift-evolution/blob/arbitrary-precision-integer-float-protocol/proposals/XXXX-arbitrary-precision-integer-float-literal-protocols.md&gt;
Authors: David Hart <https://github.com/hartbit&gt;
Review Manager: TBD
Status: TBD
<GitHub - hartbit/swift-evolution at arbitrary-precision-integer-float-protocol

We propose a pair of protocols extending ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral to allow initializing conforming types with arbitrary precision integer and floating-point literals.

<GitHub - hartbit/swift-evolution at arbitrary-precision-integer-float-protocol

The current protocols ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral are simple and work well but don't support arbitrary precision literal values. Replacing those protocols is a non-goal as they provide a simple interface for work well for most cases. Therefore, to support initializing big integer and big float types with literal values, we need new protocols.

<GitHub - hartbit/swift-evolution at arbitrary-precision-integer-float-protocol solution

We propose:

renaming FloatingPointSign into Sign,
introducing a ExpressibleByArbitraryPrecisionIntegerLiteral protocol that extends ExpressibleByIntegerLiteral and provides a new arbitrary precision initializer,
introducing a ExpressibleByArbitraryPrecisionFloatLiteral protocol that extends ExpressibleByFloatLiteral and provides a new arbitrary precision initializer.
Here is the corresponding code:

/// The sign of a number.
public enum Sign : Int {
    
    /// The sign for a positive value.
    case plus
    
    /// The sign for a negative value.
    case minus
}

/// A type that can be initialized with an arbitrary precision integer literal.
public protocol ExpressibleByArbitraryPrecisionIntegerLiteral:
    ExpressibleByIntegerLiteral {
    
    /// Creates an instance initialized with the sign and memory value of the
    /// arbitrary precision integer literal.
    ///
    /// Do not call this initializer directly. Instead, initialize a variable or
    /// constant using an arbitrary precision integer literal. For example:
    ///
    /// let x = 123_456_789_123_456_789_123_456_789
    ///
    /// In this example, the assignment to the `x` constant calls this integer
    /// literal initializer behind the scenes with `sign` as `.plus` and
    /// `buffer` as the memory [0x0000000000661EFD, 0xF2E3B19F7C045F15].
    ///
    /// - Parameters:
    /// sign: The sign of the integer value.
    /// buffer: The memory value of the integer.
    init(sign: Sign, buffer: UnsafeBufferPointer<UInt64>)
}

/// A type that can be initialized with an arbitrary precision float literal.
public protocol ExpressibleByArbitraryPrecisionFloatLiteral:
    ExpressibleByFloatLiteral {
    
    /// Creates an instance initialized with the sign, exponent and
    /// significand of the arbitrary precision float literal.
    ///
    /// Do not call this initializer directly. Instead, initialize a variable or
    /// constant using an arbitrary precision integer literal. For example:
    ///
    /// let x = 123_456_789_123_456.789_123_456_789
    ///
    /// In this example, the assignment to the `x` constant calls this float
    /// literal initializer behind the scenes with `sign` as `.plus`, `exponent`
    /// as the memory [0x0000000000661EFD, 0xF2E3B19F7C045F15] and `significand`
    /// with value [0x000000000000000C].
    ///
    /// - Parameters:
    /// sign: The sign of the integer value.
    /// exponent: The memory value of the exponent.
    /// significand: The memory value of the significand.
    init(
        sign: Sign,
        exponent: UnsafeBufferPointer<UInt64>,
        significand: UnsafeBufferPointer<UInt64>)
}

1 Like

IIRC, some branches of math don't consider 0 to be positive or negative, so I might add a `zero` case in `Sign`.

Also, I'm not sure new protocols are actually necessary. I think I remember hearing somewhere that LLVM supports integer literals up to something like 400 bits? If so, we'd merely need a type that exposes those bytes to set the relevant types `IntegerLiteralType` associated type.

- Dave Sweeris

···

Sent from my iPhone

On Mar 30, 2017, at 14:56, David Hart via swift-evolution <swift-evolution@swift.org> wrote:

Hello swift-evolution,

I’m working on a proposal for arbitrary precision integer and float literal protocols to complement ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral. I’m looking for feedback and help in verifying the logic - I have doubts about endianness.

Fire away!

https://github.com/hartbit/swift-evolution/blob/arbitrary-precision-integer-float-protocol/proposals/XXXX-arbitrary-precision-integer-float-literal-protocols.md

Arbitrary precision integer and float literal procotols
Proposal: SE-XXXX
Authors: David Hart
Review Manager: TBD
Status: TBD
Introduction

We propose a pair of protocols extending ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral to allow initializing conforming types with arbitrary precision integer and floating-point literals.

Motivation

The current protocols ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral are simple and work well but don't support arbitrary precision literal values. Replacing those protocols is a non-goal as they provide a simple interface for work well for most cases. Therefore, to support initializing big integer and big float types with literal values, we need new protocols.

Proposed solution

We propose:

renaming FloatingPointSign into Sign,
introducing a ExpressibleByArbitraryPrecisionIntegerLiteral protocol that extends ExpressibleByIntegerLiteral and provides a new arbitrary precision initializer,
introducing a ExpressibleByArbitraryPrecisionFloatLiteral protocol that extends ExpressibleByFloatLiteral and provides a new arbitrary precision initializer.
Here is the corresponding code:

/// The sign of a number.
public enum Sign : Int {
    
    /// The sign for a positive value.
    case plus
    
    /// The sign for a negative value.
    case minus
}

/// A type that can be initialized with an arbitrary precision integer literal.
public protocol ExpressibleByArbitraryPrecisionIntegerLiteral:
    ExpressibleByIntegerLiteral {
    
    /// Creates an instance initialized with the sign and memory value of the
    /// arbitrary precision integer literal.
    ///
    /// Do not call this initializer directly. Instead, initialize a variable or
    /// constant using an arbitrary precision integer literal. For example:
    ///
    /// let x = 123_456_789_123_456_789_123_456_789
    ///
    /// In this example, the assignment to the `x` constant calls this integer
    /// literal initializer behind the scenes with `sign` as `.plus` and
    /// `buffer` as the memory [0x0000000000661EFD, 0xF2E3B19F7C045F15].
    ///
    /// - Parameters:
    /// sign: The sign of the integer value.
    /// buffer: The memory value of the integer.
    init(sign: Sign, buffer: UnsafeBufferPointer<UInt64>)
}

/// A type that can be initialized with an arbitrary precision float literal.
public protocol ExpressibleByArbitraryPrecisionFloatLiteral:
    ExpressibleByFloatLiteral {
    
    /// Creates an instance initialized with the sign, exponent and
    /// significand of the arbitrary precision float literal.
    ///
    /// Do not call this initializer directly. Instead, initialize a variable or
    /// constant using an arbitrary precision integer literal. For example:
    ///
    /// let x = 123_456_789_123_456.789_123_456_789
    ///
    /// In this example, the assignment to the `x` constant calls this float
    /// literal initializer behind the scenes with `sign` as `.plus`, `exponent`
    /// as the memory [0x0000000000661EFD, 0xF2E3B19F7C045F15] and `significand`
    /// with value [0x000000000000000C].
    ///
    /// - Parameters:
    /// sign: The sign of the integer value.
    /// exponent: The memory value of the exponent.
    /// significand: The memory value of the significand.
    init(
        sign: Sign,
        exponent: UnsafeBufferPointer<UInt64>,
        significand: UnsafeBufferPointer<UInt64>)
}

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Honestly, I don't think I agree with this. I see no particular reason to like our current protocols; they break down as soon as your type gets larger than the largest standard library integer/float type, which undermines one of their main use cases.

I've been toying with a different approach in my head for a few weeks. The `BinaryInteger` protocol contains the concept of a `words` collection, which expresses any integer type as a collection of `UInt`s containing a signed two's-compliment representation of the integer. That means any `BinaryInteger` already contains code to handle a `words` collection. If we made this more exposed in some way, then `ExpressibleByIntegerLiteral` could leverage that conformance.

One approach would be to extract the `words` collection into a higher-level protocol:

  protocol BinaryIntegerSource {
    associatedtype Words: Collection where Iterator.Element == UInt
    var words: Words { get }
  }

Then we could modify `BinaryInteger` to accept this:

  protocol BinaryInteger: BinaryIntegerSource {
    ...
    init<T : BinaryIntegerSource>(_ source: T)
    ...
  }

And introduce a new `IntegerLiteral` type which is a `BinaryIntegerSource`, but not a `BinaryInteger` (so you can't do arithmetic with it):

  struct IntegerLiteral: BinaryIntegerSource {
    associatedtype Words = …
    var words: Words { … }
  }

And now, you can say something like:

  struct Int128: ExpressibleByIntegerLiteral {
    fileprivate var _value: DoubleWidth<Int64>
    
    init(integerLiteral value: IntegerLiteral) {
      _value = DoubleWidth(value)
    }
  }

And everything ought to do what it's supposed to. You could still use a different type if you didn't need anything larger than, say, `Int`. I don't believe this would require any changes to the compiler; `IntegerLiteral` could conform to `_ExpressibleByBuiltinIntegerLiteral`, which would allow it to represent integers up to the current limit of 1024 bits + 1 sign bit.

(There are a few similar approaches we could take, like exposing an `init(words:)` constructor in `BinaryInteger` and having the `IntegerLiteral` behave as a `Words` collection, but all of them basically involve bootstrapping into `BinaryInteger` through the `Words` type.)

I *think* that the not-yet-implemented `BinaryFloatingPoint.init<Source: BinaryFloatingPoint>(_ value: Source)` initializers could be leveraged in a similar way—create a `BinaryFloatingPointSource` protocol and a `BinaryFloatLiteral` type that conforms to it—but I'm less certain of that because I don't really understand how this universal float conversion is supposed to work. Plus, the universal float conversion is still just a TODO comment right now.

(This would leave non-binary floats in the lurch, but we're pretty much doing that already—try initializing `Decimal` through its `ExpressibleByFloatLiteral` conformance sometime and you'll see what I mean. I would support changing its name to `ExpressibleByBinaryFloatLiteral`.)

These leave our current integer and floating-point literal size limits (1025-bit signed integers and 80-bit floats) in place, but those are implementation details and could be changed. In practice, I very much hope the compiler will try to optimize initialization from literals aggressively.

···

On Mar 30, 2017, at 2:56 PM, David Hart via swift-evolution <swift-evolution@swift.org> wrote:

The current protocols ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral are simple and work well but don't support arbitrary precision literal values. Replacing those protocols is a non-goal as they provide a simple interface for work well for most cases.

--
Brent Royal-Gordon
Architechies

I think you forgot to consider hexadecimal floating point literals. Those have a power-of-2 significand (instead of power-of-10). They can't be expressed by the `ExpressibleByArbitraryPrecisionFloatLiteral` protocol you're proposing.

···

Le 30 mars 2017 à 17:56, David Hart via swift-evolution <swift-evolution@swift.org> a écrit :

Hello swift-evolution,

I’m working on a proposal for arbitrary precision integer and float literal protocols to complement ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral. I’m looking for feedback and help in verifying the logic - I have doubts about endianness.

Fire away!

https://github.com/hartbit/swift-evolution/blob/arbitrary-precision-integer-float-protocol/proposals/XXXX-arbitrary-precision-integer-float-literal-protocols.md

Arbitrary precision integer and float literal procotols

Proposal: SE-XXXX <https://github.com/hartbit/swift-evolution/blob/arbitrary-precision-integer-float-protocol/proposals/XXXX-arbitrary-precision-integer-float-literal-protocols.md&gt;
Authors: David Hart <https://github.com/hartbit&gt;
Review Manager: TBD
Status: TBD
<GitHub - hartbit/swift-evolution at arbitrary-precision-integer-float-protocol

We propose a pair of protocols extending ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral to allow initializing conforming types with arbitrary precision integer and floating-point literals.

<GitHub - hartbit/swift-evolution at arbitrary-precision-integer-float-protocol

The current protocols ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral are simple and work well but don't support arbitrary precision literal values. Replacing those protocols is a non-goal as they provide a simple interface for work well for most cases. Therefore, to support initializing big integer and big float types with literal values, we need new protocols.

<GitHub - hartbit/swift-evolution at arbitrary-precision-integer-float-protocol solution

We propose:

renaming FloatingPointSign into Sign,
introducing a ExpressibleByArbitraryPrecisionIntegerLiteral protocol that extends ExpressibleByIntegerLiteral and provides a new arbitrary precision initializer,
introducing a ExpressibleByArbitraryPrecisionFloatLiteral protocol that extends ExpressibleByFloatLiteral and provides a new arbitrary precision initializer.
Here is the corresponding code:

/// The sign of a number.
public enum Sign : Int {
    
    /// The sign for a positive value.
    case plus
    
    /// The sign for a negative value.
    case minus
}

/// A type that can be initialized with an arbitrary precision integer literal.
public protocol ExpressibleByArbitraryPrecisionIntegerLiteral:
    ExpressibleByIntegerLiteral {
    
    /// Creates an instance initialized with the sign and memory value of the
    /// arbitrary precision integer literal.
    ///
    /// Do not call this initializer directly. Instead, initialize a variable or
    /// constant using an arbitrary precision integer literal. For example:
    ///
    /// let x = 123_456_789_123_456_789_123_456_789
    ///
    /// In this example, the assignment to the `x` constant calls this integer
    /// literal initializer behind the scenes with `sign` as `.plus` and
    /// `buffer` as the memory [0x0000000000661EFD, 0xF2E3B19F7C045F15].
    ///
    /// - Parameters:
    /// sign: The sign of the integer value.
    /// buffer: The memory value of the integer.
    init(sign: Sign, buffer: UnsafeBufferPointer<UInt64>)
}

/// A type that can be initialized with an arbitrary precision float literal.
public protocol ExpressibleByArbitraryPrecisionFloatLiteral:
    ExpressibleByFloatLiteral {
    
    /// Creates an instance initialized with the sign, exponent and
    /// significand of the arbitrary precision float literal.
    ///
    /// Do not call this initializer directly. Instead, initialize a variable or
    /// constant using an arbitrary precision integer literal. For example:
    ///
    /// let x = 123_456_789_123_456.789_123_456_789
    ///
    /// In this example, the assignment to the `x` constant calls this float
    /// literal initializer behind the scenes with `sign` as `.plus`, `exponent`
    /// as the memory [0x0000000000661EFD, 0xF2E3B19F7C045F15] and `significand`
    /// with value [0x000000000000000C].
    ///
    /// - Parameters:
    /// sign: The sign of the integer value.
    /// exponent: The memory value of the exponent.
    /// significand: The memory value of the significand.
    init(
        sign: Sign,
        exponent: UnsafeBufferPointer<UInt64>,
        significand: UnsafeBufferPointer<UInt64>)
}

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Michel Fortin
https://michelf.ca

Sent from my iPhone

Hello swift-evolution,

I’m working on a proposal for arbitrary precision integer and float literal protocols to complement ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral. I’m looking for feedback and help in verifying the logic - I have doubts about endianness.

Fire away!

https://github.com/hartbit/swift-evolution/blob/arbitrary-precision-integer-float-protocol/proposals/XXXX-arbitrary-precision-integer-float-literal-protocols.md

Arbitrary precision integer and float literal procotols
Proposal: SE-XXXX
Authors: David Hart
Review Manager: TBD
Status: TBD
Introduction

We propose a pair of protocols extending ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral to allow initializing conforming types with arbitrary precision integer and floating-point literals.

Motivation

The current protocols ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral are simple and work well but don't support arbitrary precision literal values. Replacing those protocols is a non-goal as they provide a simple interface for work well for most cases. Therefore, to support initializing big integer and big float types with literal values, we need new protocols.

Proposed solution

We propose:

renaming FloatingPointSign into Sign,
introducing a ExpressibleByArbitraryPrecisionIntegerLiteral protocol that extends ExpressibleByIntegerLiteral and provides a new arbitrary precision initializer,
introducing a ExpressibleByArbitraryPrecisionFloatLiteral protocol that extends ExpressibleByFloatLiteral and provides a new arbitrary precision initializer.
Here is the corresponding code:

/// The sign of a number.
public enum Sign : Int {
    
    /// The sign for a positive value.
    case plus
    
    /// The sign for a negative value.
    case minus
}

/// A type that can be initialized with an arbitrary precision integer literal.
public protocol ExpressibleByArbitraryPrecisionIntegerLiteral:
    ExpressibleByIntegerLiteral {
    
    /// Creates an instance initialized with the sign and memory value of the
    /// arbitrary precision integer literal.
    ///
    /// Do not call this initializer directly. Instead, initialize a variable or
    /// constant using an arbitrary precision integer literal. For example:
    ///
    /// let x = 123_456_789_123_456_789_123_456_789
    ///
    /// In this example, the assignment to the `x` constant calls this integer
    /// literal initializer behind the scenes with `sign` as `.plus` and
    /// `buffer` as the memory [0x0000000000661EFD, 0xF2E3B19F7C045F15].
    ///
    /// - Parameters:
    /// sign: The sign of the integer value.
    /// buffer: The memory value of the integer.
    init(sign: Sign, buffer: UnsafeBufferPointer<UInt64>)
}

/// A type that can be initialized with an arbitrary precision float literal.
public protocol ExpressibleByArbitraryPrecisionFloatLiteral:
    ExpressibleByFloatLiteral {
    
    /// Creates an instance initialized with the sign, exponent and
    /// significand of the arbitrary precision float literal.
    ///
    /// Do not call this initializer directly. Instead, initialize a variable or
    /// constant using an arbitrary precision integer literal. For example:
    ///
    /// let x = 123_456_789_123_456.789_123_456_789
    ///
    /// In this example, the assignment to the `x` constant calls this float
    /// literal initializer behind the scenes with `sign` as `.plus`, `exponent`
    /// as the memory [0x0000000000661EFD, 0xF2E3B19F7C045F15] and `significand`
    /// with value [0x000000000000000C].
    ///
    /// - Parameters:
    /// sign: The sign of the integer value.
    /// exponent: The memory value of the exponent.
    /// significand: The memory value of the significand.
    init(
        sign: Sign,
        exponent: UnsafeBufferPointer<UInt64>,
        significand: UnsafeBufferPointer<UInt64>)
}

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

IIRC, some branches of math don't consider 0 to be positive or negative, so I might add a `zero` case in `Sign`.

That will never happen: the literal value 0 on a type conforming to one of those protocols will call the super-protocol's initializer because it is representable with all built-in types.

Also, I'm not sure new protocols are actually necessary. I think I remember hearing somewhere that LLVM supports integer literals up to something like 400 bits? If so, we'd merely need a type that exposes those bytes to set the relevant types `IntegerLiteralType` associated type.

I am clueless in this regard so please let me know if you find more information.

···

On 31 Mar 2017, at 04:21, David Sweeris <davesweeris@mac.com> wrote:
On Mar 30, 2017, at 14:56, David Hart via swift-evolution <swift-evolution@swift.org> wrote:

- Dave Sweeris

The current protocols ExpressibleByIntegerLiteral and ExpressibleByFloat
Literal are simple and work well but don't support arbitrary precision
literal values. Replacing those protocols is a non-goal as they provide a
simple interface for work well for most cases.

Honestly, I don't think I agree with this. I see no particular reason to
like our current protocols; they break down as soon as your type gets
larger than the largest standard library integer/float type, which
undermines one of their main use cases.

Right. I think the existing ones should, if at all possible, be revised to
support arbitrary precision literal values--or at least very, very large
precision literal values (as it can be argued that, even for a BigInt, the
ability to specify a value of arbitrarily many digits as a _literal_ would
be rarely used).

I've been toying with a different approach in my head for a few weeks. The
`BinaryInteger` protocol contains the concept of a `words` collection,
which expresses any integer type as a collection of `UInt`s containing a
signed two's-compliment representation of the integer. That means any
`BinaryInteger` already contains code to handle a `words` collection. If we
made this more exposed in some way, then `ExpressibleByIntegerLiteral`
could leverage that conformance.

One approach would be to extract the `words` collection into a
higher-level protocol:

protocol BinaryIntegerSource {
associatedtype Words: Collection where Iterator.Element == UInt
var words: Words { get }
}

Then we could modify `BinaryInteger` to accept this:

protocol BinaryInteger: BinaryIntegerSource {
...
init<T : BinaryIntegerSource>(_ source: T)
...
}

And introduce a new `IntegerLiteral` type which is a
`BinaryIntegerSource`, but not a `BinaryInteger` (so you can't do
arithmetic with it):

struct IntegerLiteral: BinaryIntegerSource {
associatedtype Words = …
var words: Words { … }
}

And now, you can say something like:

struct Int128: ExpressibleByIntegerLiteral {
fileprivate var _value: DoubleWidth<Int64>
init(integerLiteral value: IntegerLiteral) {
_value = DoubleWidth(value)
}
}

And everything ought to do what it's supposed to. You could still use a
different type if you didn't need anything larger than, say, `Int`. I don't
believe this would require any changes to the compiler; `IntegerLiteral`
could conform to `_ExpressibleByBuiltinIntegerLiteral`, which would allow
it to represent integers up to the current limit of 1024 bits + 1 sign bit.

(There are a few similar approaches we could take, like exposing an
`init(words:)` constructor in `BinaryInteger` and having the
`IntegerLiteral` behave as a `Words` collection, but all of them basically
involve bootstrapping into `BinaryInteger` through the `Words` type.)

I *think* that the not-yet-implemented `BinaryFloatingPoint.init<
Source: BinaryFloatingPoint>(_ value: Source)` initializers could be
leveraged in a similar way—create a `BinaryFloatingPointSource` protocol
and a `BinaryFloatLiteral` type that conforms to it—but I'm less certain of
that because I don't really understand how this universal float conversion
is supposed to work. Plus, the universal float conversion is still just a
TODO comment right now.

Hmm, I wonder if less is more.

First, we will soon have DoubleWidth types in the stdlib, which I would
hope means that they will be useable out of the box as integer literal
types. This would cover a lot of use cases, I'd imagine, as it would
trivially get you 128-bit and 256-bit types. Though they might be less
efficient for arithmetic, they should be perfectly suitable for
initializing a value from a literal.

For larger than 256 bits, could we not recover almost all of the benefits
by exposing Int2048 as an integer literal type? Given that Float80 has not
been abused, I don't think it's the case that offering Int2048 means people
will reach for it to do arithmetic when a smaller type will do.

(This would leave non-binary floats in the lurch, but we're pretty much

doing that already—try initializing `Decimal` through its
`ExpressibleByFloatLiteral` conformance sometime and you'll see what I
mean. I would support changing its name to `ExpressibleByBinaryFloatLitera
l`.)

This is actually one of the greatest deficiencies I see in
`ExpressibleByFloatLiteral`. Here, I would disagree with you and say that
I'd like to see `ExpressibleByFloatLiteral` improved precisely because of
its very poor functionality for `Decimal`. IMO, while saying that floating
point literals are only meant to work in binary would mean that the current
design is "correct," it's unfortunate and unjustifiable that `0.1` doesn't
mean 0.1.

Honestly, I'd even prefer to allow `String` as a floating point literal
type (because after all that's what Decimal is really doing under the hood)
than to just give up on this aspect of float literals altogether. I don't
have a good answer here but I would hate to see this opportunity lost to
fix the deficiency for real.

These leave our current integer and floating-point literal size limits

···

On Fri, Mar 31, 2017 at 3:45 AM, Brent Royal-Gordon via swift-evolution < swift-evolution@swift.org> wrote:

On Mar 30, 2017, at 2:56 PM, David Hart via swift-evolution < > swift-evolution@swift.org> wrote:
(1025-bit signed integers and 80-bit floats) in place, but those are
implementation details and could be changed. In practice, I very much hope
the compiler will try to optimize initialization from literals aggressively.

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

The current protocols ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral are simple and work well but don't support arbitrary precision literal values. Replacing those protocols is a non-goal as they provide a simple interface for work well for most cases.

Honestly, I don't think I agree with this. I see no particular reason to like our current protocols; they break down as soon as your type gets larger than the largest standard library integer/float type, which undermines one of their main use cases.

The two major reason I see is their simplicity and their type safety.

Simplicity
I recently conformed a type to both protocols and could not support arbitrary precision because the backing type was CGFloat. It would have been less obvious how to implement those conformances with the arbitrary precision initializers. I can easily see another developer be confused by them. For me, they are specialty initializers.

Type-safety
The protocols currently in the Standard Library allow you to implement them using any valid built-in type, providing type-safety: if you try to initialize a conforming type with a literal value outside the bounds of the picked built-in type, you will get a compiler error.

I've been toying with a different approach in my head for a few weeks. The `BinaryInteger` protocol contains the concept of a `words` collection, which expresses any integer type as a collection of `UInt`s containing a signed two's-compliment representation of the integer. That means any `BinaryInteger` already contains code to handle a `words` collection. If we made this more exposed in some way, then `ExpressibleByIntegerLiteral` could leverage that conformance.

One approach would be to extract the `words` collection into a higher-level protocol:

  protocol BinaryIntegerSource {
    associatedtype Words: Collection where Iterator.Element == UInt
    var words: Words { get }
  }

Then we could modify `BinaryInteger` to accept this:

  protocol BinaryInteger: BinaryIntegerSource {
    ...
    init<T : BinaryIntegerSource>(_ source: T)
    ...
  }

And introduce a new `IntegerLiteral` type which is a `BinaryIntegerSource`, but not a `BinaryInteger` (so you can't do arithmetic with it):

  struct IntegerLiteral: BinaryIntegerSource {
    associatedtype Words = …
    var words: Words { … }
  }

And now, you can say something like:

  struct Int128: ExpressibleByIntegerLiteral {
    fileprivate var _value: DoubleWidth<Int64>
    
    init(integerLiteral value: IntegerLiteral) {
      _value = DoubleWidth(value)
    }
  }

And everything ought to do what it's supposed to. You could still use a different type if you didn't need anything larger than, say, `Int`. I don't believe this would require any changes to the compiler; `IntegerLiteral` could conform to `_ExpressibleByBuiltinIntegerLiteral`, which would allow it to represent integers up to the current limit of 1024 bits + 1 sign bit.

Looks like a nice solution! It would allow us to keep one ExpressibleBy* protocol.

Any reason we need to burden ourselves with the two's complement representation?

(There are a few similar approaches we could take, like exposing an `init(words:)` constructor in `BinaryInteger` and having the `IntegerLiteral` behave as a `Words` collection, but all of them basically involve bootstrapping into `BinaryInteger` through the `Words` type.)

I *think* that the not-yet-implemented `BinaryFloatingPoint.init<Source: BinaryFloatingPoint>(_ value: Source)` initializers could be leveraged in a similar way—create a `BinaryFloatingPointSource` protocol and a `BinaryFloatLiteral` type that conforms to it—but I'm less certain of that because I don't really understand how this universal float conversion is supposed to work. Plus, the universal float conversion is still just a TODO comment right now.

What do you mean by the universal float conversion?

···

On 31 Mar 2017, at 10:45, Brent Royal-Gordon <brent@architechies.com> wrote:
On Mar 30, 2017, at 2:56 PM, David Hart via swift-evolution <swift-evolution@swift.org> wrote:

(This would leave non-binary floats in the lurch, but we're pretty much doing that already—try initializing `Decimal` through its `ExpressibleByFloatLiteral` conformance sometime and you'll see what I mean. I would support changing its name to `ExpressibleByBinaryFloatLiteral`.)

These leave our current integer and floating-point literal size limits (1025-bit signed integers and 80-bit floats) in place, but those are implementation details and could be changed. In practice, I very much hope the compiler will try to optimize initialization from literals aggressively.

--
Brent Royal-Gordon
Architechies

Any reason we need to burden ourselves with the two's complement representation?

The `words` collection and its two's-complement representation come straight out of the `BinaryInteger` protocol; all integer types already "speak" in variable-length two's-complement words. So having our literal protocol also speak the same language seems like a good way to leverage code that's already in the standard library. Besides, I can't think of a better universal format—the world has largely settled on two's complement for signed integers.

(There are a few similar approaches we could take, like exposing an `init(words:)` constructor in `BinaryInteger` and having the `IntegerLiteral` behave as a `Words` collection, but all of them basically involve bootstrapping into `BinaryInteger` through the `Words` type.)

I *think* that the not-yet-implemented `BinaryFloatingPoint.init<Source: BinaryFloatingPoint>(_ value: Source)` initializers could be leveraged in a similar way—create a `BinaryFloatingPointSource` protocol and a `BinaryFloatLiteral` type that conforms to it—but I'm less certain of that because I don't really understand how this universal float conversion is supposed to work. Plus, the universal float conversion is still just a TODO comment right now.

What do you mean by the universal float conversion?

SE-0067 includes these `BinaryFloatingPoint` members which aren't in Swift yet:

    // NOTE: --------------------------------------------------------------------
    // The next two APIs are not implementable without a revised integer
    // protocol. Nonetheless, I believe that it makes sense to consider them
    // with the rest of this proposal, with the understanding that they will
    // be implemented when it becomes possible to do so.
  
    /// `value` rounded to the closest representable value.
    init<Source: BinaryFloatingPoint>(_ value: Source)
  
    /// Fails if `value` cannot be represented exactly as `Self`.
    init?<Source: BinaryFloatingPoint>(exactly value: Source)
    // --------------------------------------------------------------------------

These are what I refer to as "universal float conversion": they allow you to convert from any `BinaryFloatingPoint` type to any other `BinaryFloatingPoint` type, even if the two types aren't aware of each other's existence.

In principle, I believe we could do something similar to what I proposed for `BinaryInteger`: extract a `BinaryFloatingPointSource` super-protocol from `BinaryFloatingPoint` containing the members these initializers rely upon, modify these initializers to constrain their parameter to that new protocol, and use that to make a `BinaryFloatingPointLiteral` type. However, since these calls have not been implemented yet and I don't know which parts of `BinaryFloatingPoint` they're supposed to use, I can't really give a design for that.

···

On Apr 2, 2017, at 11:19 PM, David Hart <david@hartbit.com> wrote:

--
Brent Royal-Gordon
Architechies

I don't suggest giving up on base-ten floating-point literals, but I think that what we have is ideal (or nearly so) for binary types and we should invent a different mechanism for decimal types. The first step in doing that is to rename the binary protocol so its function is clear.

···

On Mar 31, 2017, at 10:08 AM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

(This would leave non-binary floats in the lurch, but we're pretty much doing that already—try initializing `Decimal` through its `ExpressibleByFloatLiteral` conformance sometime and you'll see what I mean. I would support changing its name to `ExpressibleByBinaryFloatLiteral`.)

This is actually one of the greatest deficiencies I see in `ExpressibleByFloatLiteral`. Here, I would disagree with you and say that I'd like to see `ExpressibleByFloatLiteral` improved precisely because of its very poor functionality for `Decimal`. IMO, while saying that floating point literals are only meant to work in binary would mean that the current design is "correct," it's unfortunate and unjustifiable that `0.1` doesn't mean 0.1.

Honestly, I'd even prefer to allow `String` as a floating point literal type (because after all that's what Decimal is really doing under the hood) than to just give up on this aspect of float literals altogether. I don't have a good answer here but I would hate to see this opportunity lost to fix the deficiency for real.

--
Brent Royal-Gordon
Architechies

1 Like