Limitations of property wrappers or macros for range-limited integers

Continuing the tangential discussion on range- or value-constrained integers, that started in [Pitch] 128 bit Integer Types:

It's a compiler error for the native integer types, when the value is a literal, and it's otherwise impossible at runtime because simple integer assignment - when not casting - cannot fail. And if you're casting, you have to write that explicitly so you know you're doing it and can take appropriate precautions.

Similarly you lose access to the safe arithmetic operators and functions - e.g. wrapping arithmetic, and addingReportingOverflow(_:). You'd have to make an actual new FixedWidthInteger type in order to [re]implement those with your custom range, not merely using a property wrapper or macro.

I use init(clamping:) quite a lot, actually - and not as a hack, but because it's actually often semantically correct [enough] a lot of the time. Could be just me, of course - I expect it varies by problem domain. My point is just that I wouldn't necessarily dismiss those safe initialisers.

1 Like

A wrapper approach could let you specify the overflow policy in the property itself:

  @LimitedRange(clamping: 0...255) var red, green, blue: Int

which has benefits over expecting every client to bounds-check the value in a domain-appropriate way.

1 Like

But a limited-range type - e.g. typealias PositiveInteger = Integer in 1... - would be usable in more places, providing more consistent enforcement and catching errors sooner. It'd also avoid the need for property wrappers and macros, which have their own overheads and limitations.

1 Like

I also think that should be a type level enforcement. Pseudocode:

typealias X = Integer<min: 1, max: 100>

Folks can disagree, but to me that seems like a misfeature. You don't want to catch these kinds of errors sooner, because they aren't actually errors until the value lands in the place with the range constraint. You should be able to write = ( + / 2 and not have to worry about the intermediate addition result overflowing the limited-range type. And a proliferation of number types in APIs encourages more code to become generic in order to deal with all of those number types, with all the overhead and added complexity that entails, instead of favoring a canonical interchange type.


Can you have @LimitedRange(clamping: 0...255) as a function parameter? result value? tuple component?

1 Like

Oh for sure - I'm a huge proponent of unlimited-precision intermediaries, as you know. But I don't see that at odds with what I suggested - I was merely saying that I think it's useful to have a type rather than a variable where you specify the constraint. So that I can have many variables that are PositiveIntegers, with ensured consistency in their behaviour, rather than having to copy-paste a property wrapper or macro onto every one (and inevitably miss some, causing bugs, or change the constraint one day and make a real mess of it with a half-updated codebase, etc).

And alluding to @tera's next comment, having it be a custom type means you can use it practically everywhere you'd want to, without any special syntax (nor the verbosity thereof) for all the different site types, e.g. function arguments & results, tuple members, etc.

We do allow property wrappers on parameters, where it would induce a precondition in the function call IIUC. With Swift as it is today, it would be of limited utility to propagate it further than that, since you're at best moving the precondition checks around by imposing the need to convert to Int and back on the consumer of the API.

How would it look?

func call(
    @LimitedRange(clamping: 3...100) x: Int,
    @LimitedRange(clamping: 3...100) y: Int,
    @LimitedRange(clamping: 3...100) z: Int
) {

Types would be more ergonomic:

typealias I3_100 = Integer<min: 3, max: 100> // or something to that effect

func call(x: I3_100, y: I3_100, z: I3_100) {
1 Like