The current ABI looks similar to the _BitInt Clang ABI. One difference is that a "signed _BitInt must be at least two bits wide," while Builtin.IntLiteral only needs one bit for values -1 and 0.
Please could we discuss some design changes. The proposed numeric APIs are:
extension StaticBigInt {
public func signum() -> Int {
Bool(Builtin.isNegative_IntLiteral(_value)) ? -1 : (bitWidth == 1) ? 0 : +1
}
public var bitWidth: Int {
Int(Builtin.bitWidth_IntLiteral(_value))
}
public var words: UnsafeBufferPointer<UInt> {
let start = UnsafePointer<UInt>(Builtin.startOfWords_IntLiteral(_value))
let count = (bitWidth + UInt.bitWidth - 1) / UInt.bitWidth
return UnsafeBufferPointer<UInt>(start: start, count: count)
}
}
I think overflow diagnostics for fixed-width integers would only need the signum() and bitWidth APIs, which can hopefully be evaluated at compile time. Can we rely on always having those flags? Would something like the experimental #assert be used?
If the words property returns a nested Words type, should we have builtin functions for both the count and subscript of a random-access collection? Will the stored chunk type be UInt on all platforms?
I think you're right that overflow diagnostics for fixed-width integers only need signum and bitWidth. If we want to do more compile-time evaluation than that, though, it might be tricky to have an API built around the use of an unsafe pointer, which generally won't support constant-evaluation.
We can easily support both constant and dynamic evaluation of an operation like Builtin.literalWordAtIndex(_: Builtin.IntLiteral, _: Int) -> UInt. I'm happy to leave it to you how best to surface that as actual API.
Sure, we can discuss the bikeshedding part sometime; that wasn't entirely the point I was trying to make.
Rather, I was trying to raise the point that this type (however it is named) could be the first of possibly a whole family of types which cannotâand, moreover, we deliberately do not desire toâconform to numeric protocols. For example, as I mentioned above, IEEE 754 defines families of "interchange formats" to "support the exchange of floating-point data" that don't also have to be supported "arithmetic formats."
It would be niceâ˘, therefore, if we could have a term (maybe not "interchange") to describe this family of numeric types as distinct from the standard types we support, so that it is clearâas in your answer to @Saklad5, quoted belowâthat it is not some design shortcoming or temporary implementation limit that causes the numeric APIs not to be there, but rather that it's inherent to the purpose for which the type exists.
My claim is that the "non-arithmetic interchange-ness" of StaticBigInt distinguishes this type from Int along a distinct axis from its static-ness (Ă la StaticString), which I agree also distinguishes this type from Int. I am less interested in debating the precise name of the type at this point, or even whether the "static-ness" of the type is more salient to its name than the "non-arithmetic interchange-ness," only to put it out there that there is this second axis along which the proposed type here is distinct from Int.
This is getting away from the pitch at hand, I think, but I don't understand why you've come to this conclusion. We don't ban people from expressing binary floating-point values with literals in decimal base, and it is not clear to me why we "have to" ban people from expressing decimal floating-point values with literals in hexadecimal or binary base.
In fact, a long time ago now but on this very list, we clarified that what the floating-point types in Swift model the uncountably many real numbers, not the countably many exactly representable values. Note how Int(x) traps when x is not representable as an integer, but Double(x) does not trap when x cannot be represented exactly. Many IEEE arithmetic operations, moreover, have semantics that demand notionally infinite precision that is rounded at the last step.
To remain consistent with the existing design, I'm in fact strongly of the opinion that it's actively undesirable to ban people from expressing decimal floating-point values in other bases. After all, for a Decimal type, if users need to know that their literal value is represented exactly without rounding, they can invoke init(exactly:).
Thatâs exactly what I consider a design shortcoming: it is unreasonable to conflate those ânumeric APIsâ with being numeric. These APIs shouldnât be there, and the relevant protocols shouldnât require them. The types are fine, itâs the protocols they canât use that are flawed.
In other words, a protocol named FloatingPoint should not require arithmetic support, as there are well-established examples of floating-point types that do not have them.
Protocol inheritance should be reserved for cases where all requirements in the inherited protocol are relevant to the inheriting protocol. Furthermore, protocols should not have unrelated requirements: if thereâs a scenario that calls for âpartial conformance,â that indicates that the protocol should have been split up such that you can express that directly.
Iâm not sure what you mean; the type pitched hereâand similar types like it meant for interchangeâare not meant for use in numeric operations. That is the point that Iâm stressing here: the protocols arenât flawed; rather, these types arenât intended to conform to them.
It should not require support for arithmetic operations in the first place. Interchange types are floating-point numeric types, but do not support those operations.
My point is that, by construction, they wouldnât be numeric types. We choose to define numeric types as those that support numeric operations. And Iâm suggesting that we clarify this point with a term (maybe âinterchange,â maybe not) that indicates this concept: a type used to represent numeric values that isnât (necessarily) a ânumericâ type.
I think maybe ultimately youâre getting at the same point, which is that there should be a concept in the language for all such types which share common requirementsânamely, that they store a numeric value. Whether there should be a named protocol for such a concept is another matter, which hinges on whether there are useful algorithms that could be made generic over all such types.
My point is that it doesnât make sense to define ânumericâ that way, and the disconnect leads to all of these problems.
As is, Numeric can be described as three unrelated capabilities: the potential to be initialized exactly from a type conforming to BinaryInteger, support for the modulus operation, and support for multiplication.
I donât think the average person or mathematicians define ânumericâ that way, nor do I think they should be all or nothing (that is, a single protocol) in terms of conformance.
Exactly. As for whether useful algorithms could be made generic over a protocol, consider that it need not be useful in isolation. Even marker protocols can provide valuable information for an interface: I doubt many people use Sendablealone as a constraint, and it obviously doesnât add any functionality. Sure, itâll be required by the compiler in the future, but simply being able to explicitly require that guarantee now is invaluable.
I canât really conceive of a good description for a protocol called Numeric, though: storing ânumeric valuesâ is a recursive definition. Thatâs probably for the best, as itâs easier to deprecate it in a future language mode than it is to change meaning and keep the name.
A protocol for having a binary representation (maybe just a typealiased specialization of RawRepresentable), combined with a protocol for representing an IEEE 754 floating point, would be much easier to work with. Arithmetic support could be implemented in distinct (not floating-point specific) protocols, then protocol composition and a typealias (or an actual protocol with inheritance if there are additional requirements specific to them) could be used to describe the arithmetic formats.
Well, I donât agree; and in any case, disagreement is immaterial as the ship has sailed.
Salient to this pitch is how we distinguish types that arenât Numeric but store numeric values from those that areâredesigning the hierarchy of numeric protocols in ways already rejected in the past isnât one of the options.
Conversions (between formats, to and from strings, etc.)
Scaling and (for decimal) quantizing.
Copying and manipulating the sign (abs, negate, etc.)
Comparisons and total ordering
Classification and testing for NaNs, etc.
...
(All the required clause 5 operations).
For that matter, "numeric" in that sentence references the Numeric protocol, which also requires a number of arithmetic operations. If/when we added a protocol for non-arithmetic formats, we would make that explicit (FloatingPointStorageFormat or whatever), as that's by far the less useful thing to have a name for.
Numeric as it exists in the standard library binds roughly to the notion of a ring. It's not intended to be a precise mathematical abstraction; there are some compromises to how "normal people" needed to work with the protocols. If we were going to redo it, we might tweak some things, but such a change would be massively source and binary breaking, so here we are, let's move on.
(Note that the far, far bigger issues with the numeric protocol hierarchy are (a) the existence of magnitude on Numeric (b) Stridable's stride type conforming to Numeric and (c) the way signed- and unsignedness work in the integer protocols. The relatively minor quibbles with floating-point don't even register, and we definitely will not break source or binary compatibility for them.)
In that case the documentation simply needs to be updated to specify that.
OK.
Could we introduce a new protocol for requirements shared by both arithmetic and interchange formats, move said requirements from FloatingPoint into this protocol, then make FloatingPoint inherit them?
The change to make FloatingPoint refine the new protocol is a breaking change (because existing types conforming to FloatingPoint in compiled binaries will not have the witness for it).
No, it is not possible to add protocols above the existing hierarchy in an ABI-stable way. If there is a demonstrated use case for algorithms generic over storage-only formats and arithmetic formats, then separate protocols can be made with duplicative requirements that existing types can also conform to. It remains to be seen if there is such a needâas @scanon alludes to above.
To be clear, it is not and has never been the goal to dice up numeric protocols (or any other protocol hierarchy) into the smallest quantums of shared functionality; flexibility and usability are balanced against each other and the protocols that exist reflect whatâs judged to be the most useful groupings of APIs.
Is there a formal resource for what constitutes an ABI break? Iâve tried poking around the repository, but most of the resources I find are old manifestos for ABI stability and calling conventions.
I feel that made more sense before Swift 3 added the ability to make type aliases for protocol composition. The smallest meaningful quantums of functionality can be composed into larger groupings that actually see use. You cannot do the reverse.
Double has init(bitPattern:) and bitPattern APIs, where the "binary interchange format" is stored as UInt64. I presume that "decimal interchange formats" could also use unsigned integers: UInt32 for Decimal32, etc. Are you referring to these formats, or something else?
No, something else. I mean that the IEEE standard contemplates implementations supporting a Float128 type, for instance (or Float256, etc., or Float192 for that matter), that can store 128-bit floating-point data losslessly without necessarily offering floating-point computation. These types wouldnât be just a sequence of bits (which is what UInt64 is to Double); they would offer at minimum conversions to and from other binary floating-point types, and the standard further specifies that it âshouldâ (but not âshallâ) provide certain other non-computational operations such as isFinite and isNaN. (Notably, floating-point comparison of values is not required.) For these purposes, Double is its own IEEE interchange format.
As I said above, there are some commonalities here where StaticBigInt can provide some of the non-computational numeric APIsâitâs not just a sequence of words like UnsafeMutablePointer<UInt>âbut not all.
If we were to decide that being able to store 128-bit floating-point values is important without necessarily having 128-bit floating-point operations, we might want to make the type something other than bare Float128âwhich would make the type seem defective since it canât do math. If, say, we namespaced it as Interchange.Float128, then the distinction is clear. How (or if, but I think so) the design and naming of StaticBigInt could facilitate or save room for a family of such types (or at least not make future additions inconsistent or awkward) is the thought I want to raise.